From 71c148c61ea399993e4738ad30b600aa04da4544 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 11 Jan 2024 05:46:02 -0700 Subject: [PATCH] Update StoryController, add parental controls support --- .../Controllers/StoryComposeController.php | 767 +++++++++--------- app/Http/Controllers/StoryController.php | 492 +++++------ 2 files changed, 645 insertions(+), 614 deletions(-) diff --git a/app/Http/Controllers/StoryComposeController.php b/app/Http/Controllers/StoryComposeController.php index 8f9358b74..eb2d859c0 100644 --- a/app/Http/Controllers/StoryComposeController.php +++ b/app/Http/Controllers/StoryComposeController.php @@ -29,306 +29,315 @@ use App\Jobs\StoryPipeline\StoryFanout; use App\Jobs\StoryPipeline\StoryDelete; use ImageOptimizer; use App\Models\Conversation; +use App\Services\UserRoleService; class StoryComposeController extends Controller { public function apiV1Add(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - $this->validate($request, [ - 'file' => function() { - return [ - 'required', - 'mimetypes:image/jpeg,image/png,video/mp4', - 'max:' . config_cache('pixelfed.max_photo_size'), - ]; - }, - ]); + $this->validate($request, [ + 'file' => function() { + return [ + 'required', + 'mimetypes:image/jpeg,image/png,video/mp4', + 'max:' . config_cache('pixelfed.max_photo_size'), + ]; + }, + ]); - $user = $request->user(); + $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); + $count = Story::whereProfileId($user->profile_id) + ->whereActive(true) + ->where('expires_at', '>', now()) + ->count(); - $count = Story::whereProfileId($user->profile_id) - ->whereActive(true) - ->where('expires_at', '>', now()) - ->count(); + if($count >= Story::MAX_PER_DAY) { + abort(418, 'You have reached your limit for new Stories today.'); + } - if($count >= Story::MAX_PER_DAY) { - abort(418, 'You have reached your limit for new Stories today.'); - } + $photo = $request->file('file'); + $path = $this->storePhoto($photo, $user); - $photo = $request->file('file'); - $path = $this->storePhoto($photo, $user); + $story = new Story(); + $story->duration = 3; + $story->profile_id = $user->profile_id; + $story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' :'photo'; + $story->mime = $photo->getMimeType(); + $story->path = $path; + $story->local = true; + $story->size = $photo->getSize(); + $story->bearcap_token = str_random(64); + $story->expires_at = now()->addMinutes(1440); + $story->save(); - $story = new Story(); - $story->duration = 3; - $story->profile_id = $user->profile_id; - $story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' :'photo'; - $story->mime = $photo->getMimeType(); - $story->path = $path; - $story->local = true; - $story->size = $photo->getSize(); - $story->bearcap_token = str_random(64); - $story->expires_at = now()->addMinutes(1440); - $story->save(); + $url = $story->path; - $url = $story->path; + $res = [ + 'code' => 200, + 'msg' => 'Successfully added', + 'media_id' => (string) $story->id, + 'media_url' => url(Storage::url($url)) . '?v=' . time(), + 'media_type' => $story->type + ]; - $res = [ - 'code' => 200, - 'msg' => 'Successfully added', - 'media_id' => (string) $story->id, - 'media_url' => url(Storage::url($url)) . '?v=' . time(), - 'media_type' => $story->type - ]; + if($story->type === 'video') { + $video = FFMpeg::open($path); + $duration = $video->getDurationInSeconds(); + $res['media_duration'] = $duration; + if($duration > 500) { + Storage::delete($story->path); + $story->delete(); + return response()->json([ + 'message' => 'Video duration cannot exceed 60 seconds' + ], 422); + } + } - if($story->type === 'video') { - $video = FFMpeg::open($path); - $duration = $video->getDurationInSeconds(); - $res['media_duration'] = $duration; - if($duration > 500) { - Storage::delete($story->path); - $story->delete(); - return response()->json([ - 'message' => 'Video duration cannot exceed 60 seconds' - ], 422); - } - } + return $res; + } - return $res; - } + protected function storePhoto($photo, $user) + { + $mimes = explode(',', config_cache('pixelfed.media_types')); + if(in_array($photo->getMimeType(), [ + 'image/jpeg', + 'image/png', + 'video/mp4' + ]) == false) { + abort(400, 'Invalid media type'); + return; + } - protected function storePhoto($photo, $user) - { - $mimes = explode(',', config_cache('pixelfed.media_types')); - if(in_array($photo->getMimeType(), [ - 'image/jpeg', - 'image/png', - 'video/mp4' - ]) == false) { - abort(400, 'Invalid media type'); - return; - } + $storagePath = MediaPathService::story($user->profile); + $path = $photo->storePubliclyAs($storagePath, Str::random(random_int(2, 12)) . '_' . Str::random(random_int(32, 35)) . '_' . Str::random(random_int(1, 14)) . '.' . $photo->extension()); + if(in_array($photo->getMimeType(), ['image/jpeg','image/png'])) { + $fpath = storage_path('app/' . $path); + $img = Intervention::make($fpath); + $img->orientate(); + $img->save($fpath, config_cache('pixelfed.image_quality')); + $img->destroy(); + } + return $path; + } - $storagePath = MediaPathService::story($user->profile); - $path = $photo->storePubliclyAs($storagePath, Str::random(random_int(2, 12)) . '_' . Str::random(random_int(32, 35)) . '_' . Str::random(random_int(1, 14)) . '.' . $photo->extension()); - if(in_array($photo->getMimeType(), ['image/jpeg','image/png'])) { - $fpath = storage_path('app/' . $path); - $img = Intervention::make($fpath); - $img->orientate(); - $img->save($fpath, config_cache('pixelfed.image_quality')); - $img->destroy(); - } - return $path; - } + public function cropPhoto(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - public function cropPhoto(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $this->validate($request, [ + 'media_id' => 'required|integer|min:1', + 'width' => 'required', + 'height' => 'required', + 'x' => 'required', + 'y' => 'required' + ]); - $this->validate($request, [ - 'media_id' => 'required|integer|min:1', - 'width' => 'required', - 'height' => 'required', - 'x' => 'required', - 'y' => 'required' - ]); + $user = $request->user(); + $id = $request->input('media_id'); + $width = round($request->input('width')); + $height = round($request->input('height')); + $x = round($request->input('x')); + $y = round($request->input('y')); - $user = $request->user(); - $id = $request->input('media_id'); - $width = round($request->input('width')); - $height = round($request->input('height')); - $x = round($request->input('x')); - $y = round($request->input('y')); + $story = Story::whereProfileId($user->profile_id)->findOrFail($id); - $story = Story::whereProfileId($user->profile_id)->findOrFail($id); + $path = storage_path('app/' . $story->path); - $path = storage_path('app/' . $story->path); + if(!is_file($path)) { + abort(400, 'Invalid or missing media.'); + } - if(!is_file($path)) { - abort(400, 'Invalid or missing media.'); - } + if($story->type === 'photo') { + $img = Intervention::make($path); + $img->crop($width, $height, $x, $y); + $img->resize(1080, 1920, function ($constraint) { + $constraint->aspectRatio(); + }); + $img->save($path, config_cache('pixelfed.image_quality')); + } - if($story->type === 'photo') { - $img = Intervention::make($path); - $img->crop($width, $height, $x, $y); - $img->resize(1080, 1920, function ($constraint) { - $constraint->aspectRatio(); - }); - $img->save($path, config_cache('pixelfed.image_quality')); - } + return [ + 'code' => 200, + 'msg' => 'Successfully cropped', + ]; + } - return [ - 'code' => 200, - 'msg' => 'Successfully cropped', - ]; - } + public function publishStory(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - public function publishStory(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $this->validate($request, [ + 'media_id' => 'required', + 'duration' => 'required|integer|min:3|max:120', + 'can_reply' => 'required|boolean', + 'can_react' => 'required|boolean' + ]); - $this->validate($request, [ - 'media_id' => 'required', - 'duration' => 'required|integer|min:3|max:120', - 'can_reply' => 'required|boolean', - 'can_react' => 'required|boolean' - ]); + $id = $request->input('media_id'); + $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); + $story = Story::whereProfileId($user->profile_id) + ->findOrFail($id); - $id = $request->input('media_id'); - $user = $request->user(); - $story = Story::whereProfileId($user->profile_id) - ->findOrFail($id); + $story->active = true; + $story->duration = $request->input('duration', 10); + $story->can_reply = $request->input('can_reply'); + $story->can_react = $request->input('can_react'); + $story->save(); - $story->active = true; - $story->duration = $request->input('duration', 10); - $story->can_reply = $request->input('can_reply'); - $story->can_react = $request->input('can_react'); - $story->save(); + StoryService::delLatest($story->profile_id); + StoryFanout::dispatch($story)->onQueue('story'); + StoryService::addRotateQueue($story->id); - StoryService::delLatest($story->profile_id); - StoryFanout::dispatch($story)->onQueue('story'); - StoryService::addRotateQueue($story->id); + return [ + 'code' => 200, + 'msg' => 'Successfully published', + ]; + } - return [ - 'code' => 200, - 'msg' => 'Successfully published', - ]; - } + public function apiV1Delete(Request $request, $id) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - public function apiV1Delete(Request $request, $id) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $user = $request->user(); - $user = $request->user(); + $story = Story::whereProfileId($user->profile_id) + ->findOrFail($id); + $story->active = false; + $story->save(); - $story = Story::whereProfileId($user->profile_id) - ->findOrFail($id); - $story->active = false; - $story->save(); + StoryDelete::dispatch($story)->onQueue('story'); - StoryDelete::dispatch($story)->onQueue('story'); + return [ + 'code' => 200, + 'msg' => 'Successfully deleted' + ]; + } - return [ - 'code' => 200, - 'msg' => 'Successfully deleted' - ]; - } + public function compose(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); - public function compose(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + return view('stories.compose'); + } - return view('stories.compose'); - } + public function createPoll(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + abort_if(!config_cache('instance.polls.enabled'), 404); - public function createPoll(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - abort_if(!config_cache('instance.polls.enabled'), 404); + return $request->all(); + } - return $request->all(); - } + public function publishStoryPoll(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - public function publishStoryPoll(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $this->validate($request, [ + 'question' => 'required|string|min:6|max:140', + 'options' => 'required|array|min:2|max:4', + 'can_reply' => 'required|boolean', + 'can_react' => 'required|boolean' + ]); - $this->validate($request, [ - 'question' => 'required|string|min:6|max:140', - 'options' => 'required|array|min:2|max:4', - 'can_reply' => 'required|boolean', - 'can_react' => 'required|boolean' - ]); + $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); + $pid = $request->user()->profile_id; - $pid = $request->user()->profile_id; + $count = Story::whereProfileId($pid) + ->whereActive(true) + ->where('expires_at', '>', now()) + ->count(); - $count = Story::whereProfileId($pid) - ->whereActive(true) - ->where('expires_at', '>', now()) - ->count(); + if($count >= Story::MAX_PER_DAY) { + abort(418, 'You have reached your limit for new Stories today.'); + } - if($count >= Story::MAX_PER_DAY) { - abort(418, 'You have reached your limit for new Stories today.'); - } + $story = new Story; + $story->type = 'poll'; + $story->story = json_encode([ + 'question' => $request->input('question'), + 'options' => $request->input('options') + ]); + $story->public = false; + $story->local = true; + $story->profile_id = $pid; + $story->expires_at = now()->addMinutes(1440); + $story->duration = 30; + $story->can_reply = false; + $story->can_react = false; + $story->save(); - $story = new Story; - $story->type = 'poll'; - $story->story = json_encode([ - 'question' => $request->input('question'), - 'options' => $request->input('options') - ]); - $story->public = false; - $story->local = true; - $story->profile_id = $pid; - $story->expires_at = now()->addMinutes(1440); - $story->duration = 30; - $story->can_reply = false; - $story->can_react = false; - $story->save(); + $poll = new Poll; + $poll->story_id = $story->id; + $poll->profile_id = $pid; + $poll->poll_options = $request->input('options'); + $poll->expires_at = $story->expires_at; + $poll->cached_tallies = collect($poll->poll_options)->map(function($o) { + return 0; + })->toArray(); + $poll->save(); - $poll = new Poll; - $poll->story_id = $story->id; - $poll->profile_id = $pid; - $poll->poll_options = $request->input('options'); - $poll->expires_at = $story->expires_at; - $poll->cached_tallies = collect($poll->poll_options)->map(function($o) { - return 0; - })->toArray(); - $poll->save(); + $story->active = true; + $story->save(); - $story->active = true; - $story->save(); + StoryService::delLatest($story->profile_id); - StoryService::delLatest($story->profile_id); + return [ + 'code' => 200, + 'msg' => 'Successfully published', + ]; + } - return [ - 'code' => 200, - 'msg' => 'Successfully published', - ]; - } + public function storyPollVote(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - public function storyPollVote(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $this->validate($request, [ + 'sid' => 'required', + 'ci' => 'required|integer|min:0|max:3' + ]); - $this->validate($request, [ - 'sid' => 'required', - 'ci' => 'required|integer|min:0|max:3' - ]); + $pid = $request->user()->profile_id; + $ci = $request->input('ci'); + $story = Story::findOrFail($request->input('sid')); + abort_if(!FollowerService::follows($pid, $story->profile_id), 403); + $poll = Poll::whereStoryId($story->id)->firstOrFail(); - $pid = $request->user()->profile_id; - $ci = $request->input('ci'); - $story = Story::findOrFail($request->input('sid')); - abort_if(!FollowerService::follows($pid, $story->profile_id), 403); - $poll = Poll::whereStoryId($story->id)->firstOrFail(); + $vote = new PollVote; + $vote->profile_id = $pid; + $vote->poll_id = $poll->id; + $vote->story_id = $story->id; + $vote->status_id = null; + $vote->choice = $ci; + $vote->save(); - $vote = new PollVote; - $vote->profile_id = $pid; - $vote->poll_id = $poll->id; - $vote->story_id = $story->id; - $vote->status_id = null; - $vote->choice = $ci; - $vote->save(); + $poll->votes_count = $poll->votes_count + 1; + $poll->cached_tallies = collect($poll->getTallies())->map(function($tally, $key) use($ci) { + return $ci == $key ? $tally + 1 : $tally; + })->toArray(); + $poll->save(); - $poll->votes_count = $poll->votes_count + 1; - $poll->cached_tallies = collect($poll->getTallies())->map(function($tally, $key) use($ci) { - return $ci == $key ? $tally + 1 : $tally; - })->toArray(); - $poll->save(); + return 200; + } - return 200; - } + public function storeReport(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - public function storeReport(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - - $this->validate($request, [ + $this->validate($request, [ 'type' => 'required|alpha_dash', 'id' => 'required|integer|min:1', ]); + $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); + $pid = $request->user()->profile_id; $sid = $request->input('id'); $type = $request->input('type'); @@ -355,17 +364,17 @@ class StoryComposeController extends Controller abort_if(!FollowerService::follows($pid, $story->profile_id), 422, 'Cannot report a story from an account you do not follow'); if( Report::whereProfileId($pid) - ->whereObjectType('App\Story') - ->whereObjectId($story->id) - ->exists() + ->whereObjectType('App\Story') + ->whereObjectId($story->id) + ->exists() ) { - return response()->json(['error' => [ - 'code' => 409, - 'message' => 'Cannot report the same story again' - ]], 409); + return response()->json(['error' => [ + 'code' => 409, + 'message' => 'Cannot report the same story again' + ]], 409); } - $report = new Report; + $report = new Report; $report->profile_id = $pid; $report->user_id = $request->user()->id; $report->object_id = $story->id; @@ -376,149 +385,151 @@ class StoryComposeController extends Controller $report->save(); return [200]; - } + } - public function react(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - $this->validate($request, [ - 'sid' => 'required', - 'reaction' => 'required|string' - ]); - $pid = $request->user()->profile_id; - $text = $request->input('reaction'); + public function react(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $this->validate($request, [ + 'sid' => 'required', + 'reaction' => 'required|string' + ]); + $pid = $request->user()->profile_id; + $text = $request->input('reaction'); + $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); + $story = Story::findOrFail($request->input('sid')); - $story = Story::findOrFail($request->input('sid')); + abort_if(!$story->can_react, 422); + abort_if(StoryService::reactCounter($story->id, $pid) >= 5, 422, 'You have already reacted to this story'); - abort_if(!$story->can_react, 422); - abort_if(StoryService::reactCounter($story->id, $pid) >= 5, 422, 'You have already reacted to this story'); + $status = new Status; + $status->profile_id = $pid; + $status->type = 'story:reaction'; + $status->caption = $text; + $status->rendered = $text; + $status->scope = 'direct'; + $status->visibility = 'direct'; + $status->in_reply_to_profile_id = $story->profile_id; + $status->entities = json_encode([ + 'story_id' => $story->id, + 'reaction' => $text + ]); + $status->save(); - $status = new Status; - $status->profile_id = $pid; - $status->type = 'story:reaction'; - $status->caption = $text; - $status->rendered = $text; - $status->scope = 'direct'; - $status->visibility = 'direct'; - $status->in_reply_to_profile_id = $story->profile_id; - $status->entities = json_encode([ - 'story_id' => $story->id, - 'reaction' => $text - ]); - $status->save(); + $dm = new DirectMessage; + $dm->to_id = $story->profile_id; + $dm->from_id = $pid; + $dm->type = 'story:react'; + $dm->status_id = $status->id; + $dm->meta = json_encode([ + 'story_username' => $story->profile->username, + 'story_actor_username' => $request->user()->username, + 'story_id' => $story->id, + 'story_media_url' => url(Storage::url($story->path)), + 'reaction' => $text + ]); + $dm->save(); - $dm = new DirectMessage; - $dm->to_id = $story->profile_id; - $dm->from_id = $pid; - $dm->type = 'story:react'; - $dm->status_id = $status->id; - $dm->meta = json_encode([ - 'story_username' => $story->profile->username, - 'story_actor_username' => $request->user()->username, - 'story_id' => $story->id, - 'story_media_url' => url(Storage::url($story->path)), - 'reaction' => $text - ]); - $dm->save(); + Conversation::updateOrInsert( + [ + 'to_id' => $story->profile_id, + 'from_id' => $pid + ], + [ + 'type' => 'story:react', + 'status_id' => $status->id, + 'dm_id' => $dm->id, + 'is_hidden' => false + ] + ); - Conversation::updateOrInsert( - [ - 'to_id' => $story->profile_id, - 'from_id' => $pid - ], - [ - 'type' => 'story:react', - 'status_id' => $status->id, - 'dm_id' => $dm->id, - 'is_hidden' => false - ] - ); + if($story->local) { + // generate notification + $n = new Notification; + $n->profile_id = $dm->to_id; + $n->actor_id = $dm->from_id; + $n->item_id = $dm->id; + $n->item_type = 'App\DirectMessage'; + $n->action = 'story:react'; + $n->save(); + } else { + StoryReactionDeliver::dispatch($story, $status)->onQueue('story'); + } - if($story->local) { - // generate notification - $n = new Notification; - $n->profile_id = $dm->to_id; - $n->actor_id = $dm->from_id; - $n->item_id = $dm->id; - $n->item_type = 'App\DirectMessage'; - $n->action = 'story:react'; - $n->save(); - } else { - StoryReactionDeliver::dispatch($story, $status)->onQueue('story'); - } + StoryService::reactIncrement($story->id, $pid); - StoryService::reactIncrement($story->id, $pid); + return 200; + } - return 200; - } + public function comment(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $this->validate($request, [ + 'sid' => 'required', + 'caption' => 'required|string' + ]); + $pid = $request->user()->profile_id; + $text = $request->input('caption'); + $user = $request->user(); + abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); + $story = Story::findOrFail($request->input('sid')); - public function comment(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - $this->validate($request, [ - 'sid' => 'required', - 'caption' => 'required|string' - ]); - $pid = $request->user()->profile_id; - $text = $request->input('caption'); + abort_if(!$story->can_reply, 422); - $story = Story::findOrFail($request->input('sid')); + $status = new Status; + $status->type = 'story:reply'; + $status->profile_id = $pid; + $status->caption = $text; + $status->rendered = $text; + $status->scope = 'direct'; + $status->visibility = 'direct'; + $status->in_reply_to_profile_id = $story->profile_id; + $status->entities = json_encode([ + 'story_id' => $story->id + ]); + $status->save(); - abort_if(!$story->can_reply, 422); + $dm = new DirectMessage; + $dm->to_id = $story->profile_id; + $dm->from_id = $pid; + $dm->type = 'story:comment'; + $dm->status_id = $status->id; + $dm->meta = json_encode([ + 'story_username' => $story->profile->username, + 'story_actor_username' => $request->user()->username, + 'story_id' => $story->id, + 'story_media_url' => url(Storage::url($story->path)), + 'caption' => $text + ]); + $dm->save(); - $status = new Status; - $status->type = 'story:reply'; - $status->profile_id = $pid; - $status->caption = $text; - $status->rendered = $text; - $status->scope = 'direct'; - $status->visibility = 'direct'; - $status->in_reply_to_profile_id = $story->profile_id; - $status->entities = json_encode([ - 'story_id' => $story->id - ]); - $status->save(); + Conversation::updateOrInsert( + [ + 'to_id' => $story->profile_id, + 'from_id' => $pid + ], + [ + 'type' => 'story:comment', + 'status_id' => $status->id, + 'dm_id' => $dm->id, + 'is_hidden' => false + ] + ); - $dm = new DirectMessage; - $dm->to_id = $story->profile_id; - $dm->from_id = $pid; - $dm->type = 'story:comment'; - $dm->status_id = $status->id; - $dm->meta = json_encode([ - 'story_username' => $story->profile->username, - 'story_actor_username' => $request->user()->username, - 'story_id' => $story->id, - 'story_media_url' => url(Storage::url($story->path)), - 'caption' => $text - ]); - $dm->save(); + if($story->local) { + // generate notification + $n = new Notification; + $n->profile_id = $dm->to_id; + $n->actor_id = $dm->from_id; + $n->item_id = $dm->id; + $n->item_type = 'App\DirectMessage'; + $n->action = 'story:comment'; + $n->save(); + } else { + StoryReplyDeliver::dispatch($story, $status)->onQueue('story'); + } - Conversation::updateOrInsert( - [ - 'to_id' => $story->profile_id, - 'from_id' => $pid - ], - [ - 'type' => 'story:comment', - 'status_id' => $status->id, - 'dm_id' => $dm->id, - 'is_hidden' => false - ] - ); - - if($story->local) { - // generate notification - $n = new Notification; - $n->profile_id = $dm->to_id; - $n->actor_id = $dm->from_id; - $n->item_id = $dm->id; - $n->item_type = 'App\DirectMessage'; - $n->action = 'story:comment'; - $n->save(); - } else { - StoryReplyDeliver::dispatch($story, $status)->onQueue('story'); - } - - return 200; - } + return 200; + } } diff --git a/app/Http/Controllers/StoryController.php b/app/Http/Controllers/StoryController.php index 5a9fb5530..692e27961 100644 --- a/app/Http/Controllers/StoryController.php +++ b/app/Http/Controllers/StoryController.php @@ -28,288 +28,308 @@ use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Resource\Item; use App\Transformer\ActivityPub\Verb\StoryVerb; use App\Jobs\StoryPipeline\StoryViewDeliver; +use App\Services\UserRoleService; class StoryController extends StoryComposeController { - public function recent(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - $pid = $request->user()->profile_id; + public function recent(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $user = $request->user(); + if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) { + return []; + } + $pid = $user->profile_id; - if(config('database.default') == 'pgsql') { - $s = Cache::remember('pf:stories:recent-by-id:' . $pid, 900, function() use($pid) { - return Story::select('stories.*', 'followers.following_id') - ->leftJoin('followers', 'followers.following_id', 'stories.profile_id') - ->where('followers.profile_id', $pid) - ->where('stories.active', true) - ->get() - ->map(function($s) { - $r = new \StdClass; - $r->id = $s->id; - $r->profile_id = $s->profile_id; - $r->type = $s->type; - $r->path = $s->path; - return $r; - }) - ->unique('profile_id'); - }); + if(config('database.default') == 'pgsql') { + $s = Cache::remember('pf:stories:recent-by-id:' . $pid, 900, function() use($pid) { + return Story::select('stories.*', 'followers.following_id') + ->leftJoin('followers', 'followers.following_id', 'stories.profile_id') + ->where('followers.profile_id', $pid) + ->where('stories.active', true) + ->get() + ->map(function($s) { + $r = new \StdClass; + $r->id = $s->id; + $r->profile_id = $s->profile_id; + $r->type = $s->type; + $r->path = $s->path; + return $r; + }) + ->unique('profile_id'); + }); - } else { - $s = Cache::remember('pf:stories:recent-by-id:' . $pid, 900, function() use($pid) { - return Story::select('stories.*', 'followers.following_id') - ->leftJoin('followers', 'followers.following_id', 'stories.profile_id') - ->where('followers.profile_id', $pid) - ->where('stories.active', true) - ->groupBy('followers.following_id') - ->orderByDesc('id') - ->get(); - }); - } + } else { + $s = Cache::remember('pf:stories:recent-by-id:' . $pid, 900, function() use($pid) { + return Story::select('stories.*', 'followers.following_id') + ->leftJoin('followers', 'followers.following_id', 'stories.profile_id') + ->where('followers.profile_id', $pid) + ->where('stories.active', true) + ->groupBy('followers.following_id') + ->orderByDesc('id') + ->get(); + }); + } - $self = Cache::remember('pf:stories:recent-self:' . $pid, 21600, function() use($pid) { - return Story::whereProfileId($pid) - ->whereActive(true) - ->orderByDesc('id') - ->limit(1) - ->get() - ->map(function($s) use($pid) { - $r = new \StdClass; - $r->id = $s->id; - $r->profile_id = $pid; - $r->type = $s->type; - $r->path = $s->path; - return $r; - }); - }); + $self = Cache::remember('pf:stories:recent-self:' . $pid, 21600, function() use($pid) { + return Story::whereProfileId($pid) + ->whereActive(true) + ->orderByDesc('id') + ->limit(1) + ->get() + ->map(function($s) use($pid) { + $r = new \StdClass; + $r->id = $s->id; + $r->profile_id = $pid; + $r->type = $s->type; + $r->path = $s->path; + return $r; + }); + }); - if($self->count()) { - $s->prepend($self->first()); - } + if($self->count()) { + $s->prepend($self->first()); + } - $res = $s->map(function($s) use($pid) { - $profile = AccountService::get($s->profile_id); - $url = $profile['local'] ? url("/stories/{$profile['username']}") : - url("/i/rs/{$profile['id']}"); - return [ - 'pid' => $profile['id'], - 'avatar' => $profile['avatar'], - 'local' => $profile['local'], - 'username' => $profile['acct'], - 'latest' => [ - 'id' => $s->id, - 'type' => $s->type, - 'preview_url' => url(Storage::url($s->path)) - ], - 'url' => $url, - 'seen' => StoryService::hasSeen($pid, StoryService::latest($s->profile_id)), - 'sid' => $s->id - ]; - }) - ->sortBy('seen') - ->values(); - return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); - } + $res = $s->map(function($s) use($pid) { + $profile = AccountService::get($s->profile_id); + $url = $profile['local'] ? url("/stories/{$profile['username']}") : + url("/i/rs/{$profile['id']}"); + return [ + 'pid' => $profile['id'], + 'avatar' => $profile['avatar'], + 'local' => $profile['local'], + 'username' => $profile['acct'], + 'latest' => [ + 'id' => $s->id, + 'type' => $s->type, + 'preview_url' => url(Storage::url($s->path)) + ], + 'url' => $url, + 'seen' => StoryService::hasSeen($pid, StoryService::latest($s->profile_id)), + 'sid' => $s->id + ]; + }) + ->sortBy('seen') + ->values(); + return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } - public function profile(Request $request, $id) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + public function profile(Request $request, $id) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - $authed = $request->user()->profile_id; - $profile = Profile::findOrFail($id); + $user = $request->user(); + if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) { + return []; + } + $authed = $user->profile_id; + $profile = Profile::findOrFail($id); - if($authed != $profile->id && !FollowerService::follows($authed, $profile->id)) { - return abort([], 403); - } + if($authed != $profile->id && !FollowerService::follows($authed, $profile->id)) { + return abort([], 403); + } - $stories = Story::whereProfileId($profile->id) - ->whereActive(true) - ->orderBy('expires_at') - ->get() - ->map(function($s, $k) use($authed) { - $seen = StoryService::hasSeen($authed, $s->id); - $res = [ - 'id' => (string) $s->id, - 'type' => $s->type, - 'duration' => $s->duration, - 'src' => url(Storage::url($s->path)), - 'created_at' => $s->created_at->toAtomString(), - 'expires_at' => $s->expires_at->toAtomString(), - 'view_count' => ($authed == $s->profile_id) ? ($s->view_count ?? 0) : null, - 'seen' => $seen, - 'progress' => $seen ? 100 : 0, - 'can_reply' => (bool) $s->can_reply, - 'can_react' => (bool) $s->can_react - ]; + $stories = Story::whereProfileId($profile->id) + ->whereActive(true) + ->orderBy('expires_at') + ->get() + ->map(function($s, $k) use($authed) { + $seen = StoryService::hasSeen($authed, $s->id); + $res = [ + 'id' => (string) $s->id, + 'type' => $s->type, + 'duration' => $s->duration, + 'src' => url(Storage::url($s->path)), + 'created_at' => $s->created_at->toAtomString(), + 'expires_at' => $s->expires_at->toAtomString(), + 'view_count' => ($authed == $s->profile_id) ? ($s->view_count ?? 0) : null, + 'seen' => $seen, + 'progress' => $seen ? 100 : 0, + 'can_reply' => (bool) $s->can_reply, + 'can_react' => (bool) $s->can_react + ]; - if($s->type == 'poll') { - $res['question'] = json_decode($s->story, true)['question']; - $res['options'] = json_decode($s->story, true)['options']; - $res['voted'] = PollService::votedStory($s->id, $authed); - if($res['voted']) { - $res['voted_index'] = PollService::storyChoice($s->id, $authed); - } - } + if($s->type == 'poll') { + $res['question'] = json_decode($s->story, true)['question']; + $res['options'] = json_decode($s->story, true)['options']; + $res['voted'] = PollService::votedStory($s->id, $authed); + if($res['voted']) { + $res['voted_index'] = PollService::storyChoice($s->id, $authed); + } + } - return $res; - })->toArray(); - if(count($stories) == 0) { - return []; - } - $cursor = count($stories) - 1; - $stories = [[ - 'id' => (string) $stories[$cursor]['id'], - 'nodes' => $stories, - 'account' => AccountService::get($profile->id), - 'pid' => (string) $profile->id - ]]; - return response()->json($stories, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); - } + return $res; + })->toArray(); + if(count($stories) == 0) { + return []; + } + $cursor = count($stories) - 1; + $stories = [[ + 'id' => (string) $stories[$cursor]['id'], + 'nodes' => $stories, + 'account' => AccountService::get($profile->id), + 'pid' => (string) $profile->id + ]]; + return response()->json($stories, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } - public function viewed(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + public function viewed(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - $this->validate($request, [ - 'id' => 'required|min:1', - ]); - $id = $request->input('id'); + $this->validate($request, [ + 'id' => 'required|min:1', + ]); + $id = $request->input('id'); + $user = $request->user(); + if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) { + return []; + } + $authed = $user->profile; - $authed = $request->user()->profile; + $story = Story::with('profile') + ->findOrFail($id); + $exp = $story->expires_at; - $story = Story::with('profile') - ->findOrFail($id); - $exp = $story->expires_at; + $profile = $story->profile; - $profile = $story->profile; + if($story->profile_id == $authed->id) { + return []; + } - if($story->profile_id == $authed->id) { - return []; - } + $publicOnly = (bool) $profile->followedBy($authed); + abort_if(!$publicOnly, 403); - $publicOnly = (bool) $profile->followedBy($authed); - abort_if(!$publicOnly, 403); + $v = StoryView::firstOrCreate([ + 'story_id' => $id, + 'profile_id' => $authed->id + ]); - $v = StoryView::firstOrCreate([ - 'story_id' => $id, - 'profile_id' => $authed->id - ]); + if($v->wasRecentlyCreated) { + Story::findOrFail($story->id)->increment('view_count'); - if($v->wasRecentlyCreated) { - Story::findOrFail($story->id)->increment('view_count'); + if($story->local == false) { + StoryViewDeliver::dispatch($story, $authed)->onQueue('story'); + } + } - if($story->local == false) { - StoryViewDeliver::dispatch($story, $authed)->onQueue('story'); - } - } + Cache::forget('stories:recent:by_id:' . $authed->id); + StoryService::addSeen($authed->id, $story->id); + return ['code' => 200]; + } - Cache::forget('stories:recent:by_id:' . $authed->id); - StoryService::addSeen($authed->id, $story->id); - return ['code' => 200]; - } + public function exists(Request $request, $id) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $user = $request->user(); + if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) { + return response()->json(false); + } + return response()->json(Story::whereProfileId($id) + ->whereActive(true) + ->exists()); + } - public function exists(Request $request, $id) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + public function iRedirect(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - return response()->json(Story::whereProfileId($id) - ->whereActive(true) - ->exists()); - } + $user = $request->user(); + abort_if(!$user, 404); + $username = $user->username; + return redirect("/stories/{$username}"); + } - public function iRedirect(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + public function viewers(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - $user = $request->user(); - abort_if(!$user, 404); - $username = $user->username; - return redirect("/stories/{$username}"); - } + $this->validate($request, [ + 'sid' => 'required|string' + ]); - public function viewers(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $user = $request->user(); + if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) { + return response()->json([]); + } - $this->validate($request, [ - 'sid' => 'required|string' - ]); + $pid = $request->user()->profile_id; + $sid = $request->input('sid'); - $pid = $request->user()->profile_id; - $sid = $request->input('sid'); + $story = Story::whereProfileId($pid) + ->whereActive(true) + ->findOrFail($sid); - $story = Story::whereProfileId($pid) - ->whereActive(true) - ->findOrFail($sid); + $viewers = StoryView::whereStoryId($story->id) + ->latest() + ->simplePaginate(10) + ->map(function($view) { + return AccountService::get($view->profile_id); + }) + ->values(); - $viewers = StoryView::whereStoryId($story->id) - ->latest() - ->simplePaginate(10) - ->map(function($view) { - return AccountService::get($view->profile_id); - }) - ->values(); + return response()->json($viewers, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } - return response()->json($viewers, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); - } + public function remoteStory(Request $request, $id) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - public function remoteStory(Request $request, $id) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $profile = Profile::findOrFail($id); + if($profile->user_id != null || $profile->domain == null) { + return redirect('/stories/' . $profile->username); + } + $pid = $profile->id; + return view('stories.show_remote', compact('pid')); + } - $profile = Profile::findOrFail($id); - if($profile->user_id != null || $profile->domain == null) { - return redirect('/stories/' . $profile->username); - } - $pid = $profile->id; - return view('stories.show_remote', compact('pid')); - } + public function pollResults(Request $request) + { + abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); - public function pollResults(Request $request) - { - abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); + $this->validate($request, [ + 'sid' => 'required|string' + ]); - $this->validate($request, [ - 'sid' => 'required|string' - ]); + $pid = $request->user()->profile_id; + $sid = $request->input('sid'); - $pid = $request->user()->profile_id; - $sid = $request->input('sid'); + $story = Story::whereProfileId($pid) + ->whereActive(true) + ->findOrFail($sid); - $story = Story::whereProfileId($pid) - ->whereActive(true) - ->findOrFail($sid); + return PollService::storyResults($sid); + } - return PollService::storyResults($sid); - } + public function getActivityObject(Request $request, $username, $id) + { + abort_if(!config_cache('instance.stories.enabled'), 404); - public function getActivityObject(Request $request, $username, $id) - { - abort_if(!config_cache('instance.stories.enabled'), 404); + if(!$request->wantsJson()) { + return redirect('/stories/' . $username); + } - if(!$request->wantsJson()) { - return redirect('/stories/' . $username); - } + abort_if(!$request->hasHeader('Authorization'), 404); - abort_if(!$request->hasHeader('Authorization'), 404); + $profile = Profile::whereUsername($username)->whereNull('domain')->firstOrFail(); + $story = Story::whereActive(true)->whereProfileId($profile->id)->findOrFail($id); - $profile = Profile::whereUsername($username)->whereNull('domain')->firstOrFail(); - $story = Story::whereActive(true)->whereProfileId($profile->id)->findOrFail($id); + abort_if($story->bearcap_token == null, 404); + abort_if(now()->gt($story->expires_at), 404); + $token = substr($request->header('Authorization'), 7); + abort_if(hash_equals($story->bearcap_token, $token) === false, 404); + abort_if($story->created_at->lt(now()->subMinutes(20)), 404); - abort_if($story->bearcap_token == null, 404); - abort_if(now()->gt($story->expires_at), 404); - $token = substr($request->header('Authorization'), 7); - abort_if(hash_equals($story->bearcap_token, $token) === false, 404); - abort_if($story->created_at->lt(now()->subMinutes(20)), 404); + $fractal = new Manager(); + $fractal->setSerializer(new ArraySerializer()); + $resource = new Item($story, new StoryVerb()); + $res = $fractal->createData($resource)->toArray(); + return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } - $fractal = new Manager(); - $fractal->setSerializer(new ArraySerializer()); - $resource = new Item($story, new StoryVerb()); - $res = $fractal->createData($resource)->toArray(); - return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); - } - - public function showSystemStory() - { - // return view('stories.system'); - } + public function showSystemStory() + { + // return view('stories.system'); + } }