From c53894fe16a6a147d4d6710f3a04c0780d350b4a Mon Sep 17 00:00:00 2001
From: Daniel Supernault
Date: Thu, 11 Jan 2024 01:35:15 -0700
Subject: [PATCH 01/16] Add Parental Controls feature
---
.../Controllers/Auth/RegisterController.php | 4 +-
.../ParentalControlsController.php | 207 ++++++++++++++++++
.../DispatchChildInvitePipeline.php | 38 ++++
app/Mail/ParentChildInvite.php | 49 +++++
app/Models/ParentalControls.php | 55 +++++
app/Services/UserRoleService.php | 72 ++++++
config/instance.php | 12 +-
..._052419_create_parental_controls_table.php | 45 ++++
resources/views/components/collapse.blade.php | 12 +
.../emails/parental-controls/invite.blade.php | 18 ++
.../settings/parental-controls/add.blade.php | 59 +++++
.../parental-controls/checkbox.blade.php | 7 +
.../parental-controls/child-status.blade.php | 44 ++++
.../parental-controls/delete-invite.blade.php | 32 +++
.../parental-controls/index.blade.php | 62 ++++++
.../invite-register-form.blade.php | 115 ++++++++++
.../parental-controls/manage.blade.php | 119 ++++++++++
.../parental-controls/stop-managing.blade.php | 32 +++
.../site/help/parental-controls.blade.php | 47 ++++
routes/web.php | 13 ++
20 files changed, 1039 insertions(+), 3 deletions(-)
create mode 100644 app/Http/Controllers/ParentalControlsController.php
create mode 100644 app/Jobs/ParentalControlsPipeline/DispatchChildInvitePipeline.php
create mode 100644 app/Mail/ParentChildInvite.php
create mode 100644 app/Models/ParentalControls.php
create mode 100644 database/migrations/2024_01_09_052419_create_parental_controls_table.php
create mode 100644 resources/views/components/collapse.blade.php
create mode 100644 resources/views/emails/parental-controls/invite.blade.php
create mode 100644 resources/views/settings/parental-controls/add.blade.php
create mode 100644 resources/views/settings/parental-controls/checkbox.blade.php
create mode 100644 resources/views/settings/parental-controls/child-status.blade.php
create mode 100644 resources/views/settings/parental-controls/delete-invite.blade.php
create mode 100644 resources/views/settings/parental-controls/index.blade.php
create mode 100644 resources/views/settings/parental-controls/invite-register-form.blade.php
create mode 100644 resources/views/settings/parental-controls/manage.blade.php
create mode 100644 resources/views/settings/parental-controls/stop-managing.blade.php
create mode 100644 resources/views/site/help/parental-controls.blade.php
diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php
index 5eb1159fe..8c10e5d0c 100644
--- a/app/Http/Controllers/Auth/RegisterController.php
+++ b/app/Http/Controllers/Auth/RegisterController.php
@@ -60,7 +60,7 @@ class RegisterController extends Controller
*
* @return \Illuminate\Contracts\Validation\Validator
*/
- protected function validator(array $data)
+ public function validator(array $data)
{
if(config('database.default') == 'pgsql') {
$data['username'] = strtolower($data['username']);
@@ -151,7 +151,7 @@ class RegisterController extends Controller
*
* @return \App\User
*/
- protected function create(array $data)
+ public function create(array $data)
{
if(config('database.default') == 'pgsql') {
$data['username'] = strtolower($data['username']);
diff --git a/app/Http/Controllers/ParentalControlsController.php b/app/Http/Controllers/ParentalControlsController.php
new file mode 100644
index 000000000..5c60cfae2
--- /dev/null
+++ b/app/Http/Controllers/ParentalControlsController.php
@@ -0,0 +1,207 @@
+user(), 404);
+ }
+ abort_unless(config('instance.parental_controls.enabled'), 404);
+ if(config_cache('pixelfed.open_registration') == false) {
+ abort_if(config('instance.parental_controls.limits.respect_open_registration'), 404);
+ }
+ if($maxUserCheck == true) {
+ $hasLimit = config('pixelfed.enforce_max_users');
+ if($hasLimit) {
+ $count = User::where(function($q){ return $q->whereNull('status')->orWhereNotIn('status', ['deleted','delete']); })->count();
+ $limit = (int) config('pixelfed.max_users');
+
+ abort_if($limit && $limit <= $count, 404);
+ }
+ }
+ }
+
+ public function index(Request $request)
+ {
+ $this->authPreflight($request);
+ $children = ParentalControls::whereParentId($request->user()->id)->latest()->paginate(5);
+ return view('settings.parental-controls.index', compact('children'));
+ }
+
+ public function add(Request $request)
+ {
+ $this->authPreflight($request, true);
+ return view('settings.parental-controls.add');
+ }
+
+ public function view(Request $request, $id)
+ {
+ $this->authPreflight($request);
+ $uid = $request->user()->id;
+ $pc = ParentalControls::whereParentId($uid)->findOrFail($id);
+ return view('settings.parental-controls.manage', compact('pc'));
+ }
+
+ public function update(Request $request, $id)
+ {
+ $this->authPreflight($request);
+ $uid = $request->user()->id;
+ $pc = ParentalControls::whereParentId($uid)->findOrFail($id);
+ $pc->permissions = $this->requestFormFields($request);
+ $pc->save();
+ return redirect($pc->manageUrl() . '?permissions');
+ }
+
+ public function store(Request $request)
+ {
+ $this->authPreflight($request, true);
+ $this->validate($request, [
+ 'email' => 'required|email|unique:parental_controls,email|unique:users,email',
+ ]);
+
+ $state = $this->requestFormFields($request);
+
+ $pc = new ParentalControls;
+ $pc->parent_id = $request->user()->id;
+ $pc->email = $request->input('email');
+ $pc->verify_code = str_random(32);
+ $pc->permissions = $state;
+ $pc->save();
+
+ DispatchChildInvitePipeline::dispatch($pc);
+ return redirect($pc->manageUrl());
+ }
+
+ public function inviteRegister(Request $request, $id, $code)
+ {
+ $this->authPreflight($request, true, false);
+ $pc = ParentalControls::whereRaw('verify_code = BINARY ?', $code)->whereNull(['email_verified_at', 'child_id'])->findOrFail($id);
+ abort_unless(User::whereId($pc->parent_id)->exists(), 404);
+ return view('settings.parental-controls.invite-register-form', compact('pc'));
+ }
+
+ public function inviteRegisterStore(Request $request, $id, $code)
+ {
+ $this->authPreflight($request, true, false);
+
+ $pc = ParentalControls::whereRaw('verify_code = BINARY ?', $code)->whereNull('email_verified_at')->findOrFail($id);
+
+ $fields = $request->all();
+ $fields['email'] = $pc->email;
+ $defaults = UserRoleService::defaultRoles();
+ $validator = (new RegisterController)->validator($fields);
+ $valid = $validator->validate();
+ abort_if(!$valid, 404);
+ event(new Registered($user = (new RegisterController)->create($fields)));
+ sleep(5);
+ $user->has_roles = true;
+ $user->parent_id = $pc->parent_id;
+ if(config('instance.parental_controls.limits.auto_verify_email')) {
+ $user->email_verified_at = now();
+ $user->save();
+ sleep(3);
+ } else {
+ $user->save();
+ sleep(3);
+ }
+ $ur = UserRoles::updateOrCreate([
+ 'user_id' => $user->id,
+ ],[
+ 'roles' => UserRoleService::mapInvite($user->id, $pc->permissions)
+ ]);
+ $pc->email_verified_at = now();
+ $pc->child_id = $user->id;
+ $pc->save();
+ sleep(2);
+ Auth::guard()->login($user);
+
+ return redirect('/i/web');
+ }
+
+ public function cancelInvite(Request $request, $id)
+ {
+ $this->authPreflight($request);
+ $pc = ParentalControls::whereParentId($request->user()->id)
+ ->whereNull(['email_verified_at', 'child_id'])
+ ->findOrFail($id);
+
+ return view('settings.parental-controls.delete-invite', compact('pc'));
+ }
+
+ public function cancelInviteHandle(Request $request, $id)
+ {
+ $this->authPreflight($request);
+ $pc = ParentalControls::whereParentId($request->user()->id)
+ ->whereNull(['email_verified_at', 'child_id'])
+ ->findOrFail($id);
+
+ $pc->delete();
+
+ return redirect('/settings/parental-controls');
+ }
+
+ public function stopManaging(Request $request, $id)
+ {
+ $this->authPreflight($request);
+ $pc = ParentalControls::whereParentId($request->user()->id)
+ ->whereNotNull(['email_verified_at', 'child_id'])
+ ->findOrFail($id);
+
+ return view('settings.parental-controls.stop-managing', compact('pc'));
+ }
+
+ public function stopManagingHandle(Request $request, $id)
+ {
+ $this->authPreflight($request);
+ $pc = ParentalControls::whereParentId($request->user()->id)
+ ->whereNotNull(['email_verified_at', 'child_id'])
+ ->findOrFail($id);
+ $pc->child()->update([
+ 'has_roles' => false,
+ 'parent_id' => null,
+ ]);
+ $pc->delete();
+
+ return redirect('/settings/parental-controls');
+ }
+
+ protected function requestFormFields($request)
+ {
+ $state = [];
+ $fields = [
+ 'post',
+ 'comment',
+ 'like',
+ 'share',
+ 'follow',
+ 'bookmark',
+ 'story',
+ 'collection',
+ 'discovery_feeds',
+ 'dms',
+ 'federation',
+ 'hide_network',
+ 'private',
+ 'hide_cw'
+ ];
+
+ foreach ($fields as $field) {
+ $state[$field] = $request->input($field) == 'on';
+ }
+
+ return $state;
+ }
+}
diff --git a/app/Jobs/ParentalControlsPipeline/DispatchChildInvitePipeline.php b/app/Jobs/ParentalControlsPipeline/DispatchChildInvitePipeline.php
new file mode 100644
index 000000000..a67f4e444
--- /dev/null
+++ b/app/Jobs/ParentalControlsPipeline/DispatchChildInvitePipeline.php
@@ -0,0 +1,38 @@
+pc = $pc;
+ }
+
+ /**
+ * Execute the job.
+ */
+ public function handle(): void
+ {
+ $pc = $this->pc;
+
+ Mail::to($pc->email)->send(new ParentChildInvite($pc));
+ }
+}
diff --git a/app/Mail/ParentChildInvite.php b/app/Mail/ParentChildInvite.php
new file mode 100644
index 000000000..843ea472d
--- /dev/null
+++ b/app/Mail/ParentChildInvite.php
@@ -0,0 +1,49 @@
+
+ */
+ public function attachments(): array
+ {
+ return [];
+ }
+}
diff --git a/app/Models/ParentalControls.php b/app/Models/ParentalControls.php
new file mode 100644
index 000000000..83d47c18a
--- /dev/null
+++ b/app/Models/ParentalControls.php
@@ -0,0 +1,55 @@
+ 'array',
+ 'email_sent_at' => 'datetime',
+ 'email_verified_at' => 'datetime'
+ ];
+
+ protected $guarded = [];
+
+ public function parent()
+ {
+ return $this->belongsTo(User::class, 'parent_id');
+ }
+
+ public function child()
+ {
+ return $this->belongsTo(User::class, 'child_id');
+ }
+
+ public function childAccount()
+ {
+ if($u = $this->child) {
+ if($u->profile_id) {
+ return AccountService::get($u->profile_id, true);
+ } else {
+ return [];
+ }
+ } else {
+ return [];
+ }
+ }
+
+ public function manageUrl()
+ {
+ return url('/settings/parental-controls/manage/' . $this->id);
+ }
+
+ public function inviteUrl()
+ {
+ return url('/auth/pci/' . $this->id . '/' . $this->verify_code);
+ }
+}
diff --git a/app/Services/UserRoleService.php b/app/Services/UserRoleService.php
index 500a4666e..a18810bf0 100644
--- a/app/Services/UserRoleService.php
+++ b/app/Services/UserRoleService.php
@@ -52,6 +52,13 @@ class UserRoleService
'can-follow' => false,
'can-make-public' => false,
+
+ 'can-direct-message' => false,
+ 'can-use-stories' => false,
+ 'can-view-sensitive' => false,
+ 'can-bookmark' => false,
+ 'can-collections' => false,
+ 'can-federation' => false,
];
}
@@ -114,6 +121,71 @@ class UserRoleService
'title' => 'Can make account public',
'action' => 'Allows the ability to make account public'
],
+
+ 'can-direct-message' => [
+ 'title' => '',
+ 'action' => ''
+ ],
+ 'can-use-stories' => [
+ 'title' => '',
+ 'action' => ''
+ ],
+ 'can-view-sensitive' => [
+ 'title' => '',
+ 'action' => ''
+ ],
+ 'can-bookmark' => [
+ 'title' => '',
+ 'action' => ''
+ ],
+ 'can-collections' => [
+ 'title' => '',
+ 'action' => ''
+ ],
+ 'can-federation' => [
+ 'title' => '',
+ 'action' => ''
+ ],
];
}
+
+ public static function mapInvite($id, $data = [])
+ {
+ $roles = self::get($id);
+
+ $map = [
+ 'account-force-private' => 'private',
+ 'account-ignore-follow-requests' => 'private',
+
+ 'can-view-public-feed' => 'discovery_feeds',
+ 'can-view-network-feed' => 'discovery_feeds',
+ 'can-view-discover' => 'discovery_feeds',
+ 'can-view-hashtag-feed' => 'discovery_feeds',
+
+ 'can-post' => 'post',
+ 'can-comment' => 'comment',
+ 'can-like' => 'like',
+ 'can-share' => 'share',
+
+ 'can-follow' => 'follow',
+ 'can-make-public' => '!private',
+
+ 'can-direct-message' => 'dms',
+ 'can-use-stories' => 'story',
+ 'can-view-sensitive' => '!hide_cw',
+ 'can-bookmark' => 'bookmark',
+ 'can-collections' => 'collection',
+ 'can-federation' => 'federation',
+ ];
+
+ foreach ($map as $key => $value) {
+ if(!isset($data[$value], $data[substr($value, 1)])) {
+ $map[$key] = false;
+ continue;
+ }
+ $map[$key] = str_starts_with($value, '!') ? !$data[substr($value, 1)] : $data[$value];
+ }
+
+ return $map;
+ }
}
diff --git a/config/instance.php b/config/instance.php
index 6357afe63..5e173684c 100644
--- a/config/instance.php
+++ b/config/instance.php
@@ -129,5 +129,15 @@ return [
'banner' => [
'blurhash' => env('INSTANCE_BANNER_BLURHASH', 'UzJR]l{wHZRjM}R%XRkCH?X9xaWEjZj]kAjt')
- ]
+ ],
+
+ 'parental_controls' => [
+ 'enabled' => env('INSTANCE_PARENTAL_CONTROLS', true),
+
+ 'limits' => [
+ 'respect_open_registration' => env('INSTANCE_PARENTAL_CONTROLS_RESPECT_OPENREG', true),
+ 'max_children' => env('INSTANCE_PARENTAL_CONTROLS_MAX_CHILDREN', 10),
+ 'auto_verify_email' => true,
+ ],
+ ]
];
diff --git a/database/migrations/2024_01_09_052419_create_parental_controls_table.php b/database/migrations/2024_01_09_052419_create_parental_controls_table.php
new file mode 100644
index 000000000..4ef7fd2c7
--- /dev/null
+++ b/database/migrations/2024_01_09_052419_create_parental_controls_table.php
@@ -0,0 +1,45 @@
+id();
+ $table->unsignedInteger('parent_id')->index();
+ $table->unsignedInteger('child_id')->unique()->index()->nullable();
+ $table->string('email')->unique()->nullable();
+ $table->string('verify_code')->nullable();
+ $table->timestamp('email_sent_at')->nullable();
+ $table->timestamp('email_verified_at')->nullable();
+ $table->json('permissions')->nullable();
+ $table->softDeletes();
+ $table->timestamps();
+ });
+
+ Schema::table('user_roles', function (Blueprint $table) {
+ $table->dropIndex('user_roles_profile_id_unique');
+ $table->unsignedBigInteger('profile_id')->unique()->nullable()->index()->change();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('parental_controls');
+
+ Schema::table('user_roles', function (Blueprint $table) {
+ $table->dropIndex('user_roles_profile_id_unique');
+ $table->unsignedBigInteger('profile_id')->unique()->index()->change();
+ });
+ }
+};
diff --git a/resources/views/components/collapse.blade.php b/resources/views/components/collapse.blade.php
new file mode 100644
index 000000000..144579366
--- /dev/null
+++ b/resources/views/components/collapse.blade.php
@@ -0,0 +1,12 @@
+@php
+$cid = 'col' . str_random(6);
+@endphp
+
+
+
+ {{ $title }}
+
+
+ {{ $slot }}
+
+
diff --git a/resources/views/emails/parental-controls/invite.blade.php b/resources/views/emails/parental-controls/invite.blade.php
new file mode 100644
index 000000000..bece5b1bb
--- /dev/null
+++ b/resources/views/emails/parental-controls/invite.blade.php
@@ -0,0 +1,18 @@
+
+# You've been invited to join Pixelfed!
+
+
+A parent account with the username **{{ $verify->parent->username }}** has invited you to join Pixelfed with a special youth account managed by them.
+
+If you do not recognize this account as your parents or a trusted guardian, please check with them first.
+
+
+
+Accept Invite
+
+
+Thanks,
+Pixelfed
+
+This email is automatically generated. Please do not reply to this message.
+
diff --git a/resources/views/settings/parental-controls/add.blade.php b/resources/views/settings/parental-controls/add.blade.php
new file mode 100644
index 000000000..b7ca4c7ab
--- /dev/null
+++ b/resources/views/settings/parental-controls/add.blade.php
@@ -0,0 +1,59 @@
+@extends('settings.template-vue')
+
+@section('section')
+
+@endsection
+
diff --git a/resources/views/settings/parental-controls/checkbox.blade.php b/resources/views/settings/parental-controls/checkbox.blade.php
new file mode 100644
index 000000000..b6cedbe92
--- /dev/null
+++ b/resources/views/settings/parental-controls/checkbox.blade.php
@@ -0,0 +1,7 @@
+@php
+$id = str_random(6) . '_' . str_slug($name);
+$defaultChecked = isset($checked) && $checked ? 'checked=""' : '';
+@endphp
+
+ {{ $title }}
+
diff --git a/resources/views/settings/parental-controls/child-status.blade.php b/resources/views/settings/parental-controls/child-status.blade.php
new file mode 100644
index 000000000..9852dacd7
--- /dev/null
+++ b/resources/views/settings/parental-controls/child-status.blade.php
@@ -0,0 +1,44 @@
+@if($state)
+
+ @if($state === 'sent_invite')
+
+
+
Child Invite Sent!
+
+
+
Created child invite
+
Sent invite email to child
+
Child joined via invite
+
Child account is active
+
+
+ @elseif($state === 'awaiting_email_confirmation')
+
+
+
Child Invite Sent!
+
+
+
Created child invite
+
Sent invite email to child
+
Child joined via invite
+
Child account is active
+
+
+ @elseif($state === 'active')
+
+
+
Child Account Active
+
+
+
Created child invite
+
Sent invite email to child
+
Child joined via invite
+
Child account is active
+
+
+
View Account
+
+ @endif
+
+@else
+@endif
diff --git a/resources/views/settings/parental-controls/delete-invite.blade.php b/resources/views/settings/parental-controls/delete-invite.blade.php
new file mode 100644
index 000000000..36b66ab15
--- /dev/null
+++ b/resources/views/settings/parental-controls/delete-invite.blade.php
@@ -0,0 +1,32 @@
+@extends('settings.template-vue')
+
+@section('section')
+
+
+@endsection
diff --git a/resources/views/settings/parental-controls/index.blade.php b/resources/views/settings/parental-controls/index.blade.php
new file mode 100644
index 000000000..a99093f33
--- /dev/null
+++ b/resources/views/settings/parental-controls/index.blade.php
@@ -0,0 +1,62 @@
+@extends('settings.template-vue')
+
+@section('section')
+
+
+
+
+
+ @if($children->count())
+
+
+
+
+ {{ $children->links() }}
+
+
+ @else
+
+
You are not managing any children accounts.
+
+ @endif
+
+
+
+ Add Child
+
+
+
+ {{ $children->total() }}/{{ config('instance.parental_controls.limits.max_children') }}
+ children added
+
+
+
+
+@endsection
+
diff --git a/resources/views/settings/parental-controls/invite-register-form.blade.php b/resources/views/settings/parental-controls/invite-register-form.blade.php
new file mode 100644
index 000000000..5b894e8d2
--- /dev/null
+++ b/resources/views/settings/parental-controls/invite-register-form.blade.php
@@ -0,0 +1,115 @@
+@extends('layouts.app')
+
+@section('content')
+
+@endsection
diff --git a/resources/views/settings/parental-controls/manage.blade.php b/resources/views/settings/parental-controls/manage.blade.php
new file mode 100644
index 000000000..a6cc62119
--- /dev/null
+++ b/resources/views/settings/parental-controls/manage.blade.php
@@ -0,0 +1,119 @@
+@extends('settings.template-vue')
+
+@section('section')
+
+@endsection
+
+@push('scripts')
+
+@endpush
diff --git a/resources/views/settings/parental-controls/stop-managing.blade.php b/resources/views/settings/parental-controls/stop-managing.blade.php
new file mode 100644
index 000000000..747339fd8
--- /dev/null
+++ b/resources/views/settings/parental-controls/stop-managing.blade.php
@@ -0,0 +1,32 @@
+@extends('settings.template-vue')
+
+@section('section')
+
+
+@endsection
diff --git a/resources/views/site/help/parental-controls.blade.php b/resources/views/site/help/parental-controls.blade.php
new file mode 100644
index 000000000..d7b9710dd
--- /dev/null
+++ b/resources/views/site/help/parental-controls.blade.php
@@ -0,0 +1,47 @@
+@extends('site.help.partial.template', ['breadcrumb'=>'Parental Controls'])
+
+@section('section')
+
+
Parental Controls
+
+
+
+ In the digital age, ensuring your children's online safety is paramount. Designed with both fun and safety in mind, this feature allows parents to create child accounts, tailor-made for a worry-free social media experience.
+
+ Key Features:
+
+
+ Child Account Creation : Easily set up a child account with just a few clicks. This account is linked to your own, giving you complete oversight.
+ Post Control : Decide if your child can post content. This allows you to ensure they're only sharing what's appropriate and safe.
+ Comment Management : Control whether your child can comment on posts. This helps in safeguarding them from unwanted interactions and maintaining a positive online environment.
+ Like & Share Restrictions : You have the power to enable or disable the ability to like and share posts. This feature helps in controlling the extent of your child's social media engagement.
+ Disable Federation : For added safety, you can choose to disable federation for your child's account, limiting their interaction to a more controlled environment.
+
+
+
+
+
+ @if(config('instance.parental_controls.enabled'))
+
+ Click here and tap on the Add Child button in the bottom left corner
+ Select the Allowed Actions, Enabled features and Preferences
+ Enter your childs email address
+ Press the Add Child buttton
+ Open your childs email and tap on the Accept Invite button in the email, ensure your parent username is present in the email
+ Fill out the child display name, username and password
+ Press Register and your child account will be active!
+
+ @else
+
This feature has been disabled by server admins.
+ @endif
+
+
+
+@if(config('instance.parental_controls.enabled'))
+
+
+ You can create and manage up to {{ config('instance.parental_controls.limits.max_children') }} child accounts.
+
+
+@endif
+@endsection
diff --git a/routes/web.php b/routes/web.php
index b8149a605..dffd5e271 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -200,6 +200,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::post('auth/raw/mastodon/s/account-to-id', 'RemoteAuthController@accountToId');
Route::post('auth/raw/mastodon/s/finish-up', 'RemoteAuthController@finishUp');
Route::post('auth/raw/mastodon/s/login', 'RemoteAuthController@handleLogin');
+ Route::get('auth/pci/{id}/{code}', 'ParentalControlsController@inviteRegister');
+ Route::post('auth/pci/{id}/{code}', 'ParentalControlsController@inviteRegisterStore');
Route::get('discover', 'DiscoverController@home')->name('discover');
@@ -534,6 +536,16 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
});
+ Route::get('parental-controls', 'ParentalControlsController@index');
+ Route::get('parental-controls/add', 'ParentalControlsController@add')->name('settings.pc.add');
+ Route::post('parental-controls/add', 'ParentalControlsController@store');
+ Route::get('parental-controls/manage/{id}', 'ParentalControlsController@view');
+ Route::post('parental-controls/manage/{id}', 'ParentalControlsController@update');
+ Route::get('parental-controls/manage/{id}/cancel-invite', 'ParentalControlsController@cancelInvite')->name('settings.pc.cancel-invite');
+ Route::post('parental-controls/manage/{id}/cancel-invite', 'ParentalControlsController@cancelInviteHandle');
+ Route::get('parental-controls/manage/{id}/stop-managing', 'ParentalControlsController@stopManaging')->name('settings.pc.stop-managing');
+ Route::post('parental-controls/manage/{id}/stop-managing', 'ParentalControlsController@stopManagingHandle');
+
Route::get('applications', 'SettingsController@applications')->name('settings.applications')->middleware('dangerzone');
Route::get('data-export', 'SettingsController@dataExport')->name('settings.dataexport')->middleware('dangerzone');
Route::post('data-export/following', 'SettingsController@exportFollowing')->middleware('dangerzone');
@@ -618,6 +630,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::view('licenses', 'site.help.licenses')->name('help.licenses');
Route::view('instance-max-users-limit', 'site.help.instance-max-users')->name('help.instance-max-users-limit');
Route::view('import', 'site.help.import')->name('help.import');
+ Route::view('parental-controls', 'site.help.parental-controls');
});
Route::get('newsroom/{year}/{month}/{slug}', 'NewsroomController@show');
Route::get('newsroom/archive', 'NewsroomController@archive');
From ef57d471e56264090bdaa0e019db02912b002c87 Mon Sep 17 00:00:00 2001
From: Daniel Supernault
Date: Thu, 11 Jan 2024 01:50:51 -0700
Subject: [PATCH 02/16] Update migration
---
...3_12_27_082024_add_has_roles_to_users_table.php | 14 +++++++++++---
1 file changed, 11 insertions(+), 3 deletions(-)
diff --git a/database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php b/database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php
index 09246e37b..f32fe599c 100644
--- a/database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php
+++ b/database/migrations/2023_12_27_082024_add_has_roles_to_users_table.php
@@ -24,9 +24,17 @@ return new class extends Migration
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
- $table->dropColumn('has_roles');
- $table->dropColumn('parent_id');
- $table->dropColumn('role_id');
+ if (Schema::hasColumn('users', 'has_roles')) {
+ $table->dropColumn('has_roles');
+ }
+
+ if (Schema::hasColumn('users', 'role_id')) {
+ $table->dropColumn('role_id');
+ }
+
+ if (Schema::hasColumn('users', 'parent_id')) {
+ $table->dropColumn('parent_id');
+ }
});
}
};
From 319a20b473eff60b00fe6e30a99c2bb9fa38a20e Mon Sep 17 00:00:00 2001
From: Daniel Supernault
Date: Thu, 11 Jan 2024 02:12:54 -0700
Subject: [PATCH 03/16] Update ParentalControlsController, redirect to new
custom error page on active session when attempting to use child invite link
so as to not overwrite parent active session with child session
---
app/Http/Controllers/ParentalControlsController.php | 13 +++++++++++++
resources/views/errors/custom.blade.php | 10 ++++++++++
2 files changed, 23 insertions(+)
create mode 100644 resources/views/errors/custom.blade.php
diff --git a/app/Http/Controllers/ParentalControlsController.php b/app/Http/Controllers/ParentalControlsController.php
index 5c60cfae2..1dc2f578f 100644
--- a/app/Http/Controllers/ParentalControlsController.php
+++ b/app/Http/Controllers/ParentalControlsController.php
@@ -87,7 +87,14 @@ class ParentalControlsController extends Controller
public function inviteRegister(Request $request, $id, $code)
{
+ if($request->user()) {
+ $title = 'You cannot complete this action on this device.';
+ $body = 'Please log out or use a different device or browser to complete the invitation registration.';
+ return view('errors.custom', compact('title', 'body'));
+ }
+
$this->authPreflight($request, true, false);
+
$pc = ParentalControls::whereRaw('verify_code = BINARY ?', $code)->whereNull(['email_verified_at', 'child_id'])->findOrFail($id);
abort_unless(User::whereId($pc->parent_id)->exists(), 404);
return view('settings.parental-controls.invite-register-form', compact('pc'));
@@ -95,6 +102,12 @@ class ParentalControlsController extends Controller
public function inviteRegisterStore(Request $request, $id, $code)
{
+ if($request->user()) {
+ $title = 'You cannot complete this action on this device.';
+ $body = 'Please log out or use a different device or browser to complete the invitation registration.';
+ return view('errors.custom', compact('title', 'body'));
+ }
+
$this->authPreflight($request, true, false);
$pc = ParentalControls::whereRaw('verify_code = BINARY ?', $code)->whereNull('email_verified_at')->findOrFail($id);
diff --git a/resources/views/errors/custom.blade.php b/resources/views/errors/custom.blade.php
new file mode 100644
index 000000000..932292b6b
--- /dev/null
+++ b/resources/views/errors/custom.blade.php
@@ -0,0 +1,10 @@
+@extends('layouts.app')
+
+@section('content')
+
+
+
{!! $title ?? config('instance.page.404.header')!!}
+
{!! $body ?? config('instance.page.404.body')!!}
+
+
+@endsection
From 5f6ed85770a51591cf15d503afbf865fbac30c5f Mon Sep 17 00:00:00 2001
From: Daniel Supernault
Date: Thu, 11 Jan 2024 02:34:43 -0700
Subject: [PATCH 04/16] Update settings sidebar
---
.../Controllers/Settings/HomeSettings.php | 312 +++++++++---------
resources/views/settings/email.blade.php | 85 ++---
.../views/settings/partial/sidebar.blade.php | 153 ++++-----
3 files changed, 262 insertions(+), 288 deletions(-)
diff --git a/app/Http/Controllers/Settings/HomeSettings.php b/app/Http/Controllers/Settings/HomeSettings.php
index 082a72af0..99326c097 100644
--- a/app/Http/Controllers/Settings/HomeSettings.php
+++ b/app/Http/Controllers/Settings/HomeSettings.php
@@ -22,189 +22,189 @@ use App\Services\PronounService;
trait HomeSettings
{
- public function home()
- {
- $id = Auth::user()->profile->id;
- $storage = [];
- $used = Media::whereProfileId($id)->sum('size');
- $storage['limit'] = config_cache('pixelfed.max_account_size') * 1024;
- $storage['used'] = $used;
- $storage['percentUsed'] = ceil($storage['used'] / $storage['limit'] * 100);
- $storage['limitPretty'] = PrettyNumber::size($storage['limit']);
- $storage['usedPretty'] = PrettyNumber::size($storage['used']);
- $pronouns = PronounService::get($id);
+ public function home()
+ {
+ $id = Auth::user()->profile->id;
+ $storage = [];
+ $used = Media::whereProfileId($id)->sum('size');
+ $storage['limit'] = config_cache('pixelfed.max_account_size') * 1024;
+ $storage['used'] = $used;
+ $storage['percentUsed'] = ceil($storage['used'] / $storage['limit'] * 100);
+ $storage['limitPretty'] = PrettyNumber::size($storage['limit']);
+ $storage['usedPretty'] = PrettyNumber::size($storage['used']);
+ $pronouns = PronounService::get($id);
- return view('settings.home', compact('storage', 'pronouns'));
- }
+ return view('settings.home', compact('storage', 'pronouns'));
+ }
- public function homeUpdate(Request $request)
- {
- $this->validate($request, [
- 'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'),
- 'bio' => 'nullable|string|max:'.config('pixelfed.max_bio_length'),
- 'website' => 'nullable|url',
- 'language' => 'nullable|string|min:2|max:5',
- 'pronouns' => 'nullable|array|max:4'
- ]);
+ public function homeUpdate(Request $request)
+ {
+ $this->validate($request, [
+ 'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'),
+ 'bio' => 'nullable|string|max:'.config('pixelfed.max_bio_length'),
+ 'website' => 'nullable|url',
+ 'language' => 'nullable|string|min:2|max:5',
+ 'pronouns' => 'nullable|array|max:4'
+ ]);
- $changes = false;
- $name = strip_tags(Purify::clean($request->input('name')));
- $bio = $request->filled('bio') ? strip_tags(Purify::clean($request->input('bio'))) : null;
- $website = $request->input('website');
- $language = $request->input('language');
- $user = Auth::user();
- $profile = $user->profile;
- $pronouns = $request->input('pronouns');
- $existingPronouns = PronounService::get($profile->id);
- $layout = $request->input('profile_layout');
- if($layout) {
- $layout = !in_array($layout, ['metro', 'moment']) ? 'metro' : $layout;
- }
+ $changes = false;
+ $name = strip_tags(Purify::clean($request->input('name')));
+ $bio = $request->filled('bio') ? strip_tags(Purify::clean($request->input('bio'))) : null;
+ $website = $request->input('website');
+ $language = $request->input('language');
+ $user = Auth::user();
+ $profile = $user->profile;
+ $pronouns = $request->input('pronouns');
+ $existingPronouns = PronounService::get($profile->id);
+ $layout = $request->input('profile_layout');
+ if($layout) {
+ $layout = !in_array($layout, ['metro', 'moment']) ? 'metro' : $layout;
+ }
- $enforceEmailVerification = config_cache('pixelfed.enforce_email_verification');
+ $enforceEmailVerification = config_cache('pixelfed.enforce_email_verification');
- // Only allow email to be updated if not yet verified
- if (!$enforceEmailVerification || !$changes && $user->email_verified_at) {
- if ($profile->name != $name) {
- $changes = true;
- $user->name = $name;
- $profile->name = $name;
- }
+ // Only allow email to be updated if not yet verified
+ if (!$enforceEmailVerification || !$changes && $user->email_verified_at) {
+ if ($profile->name != $name) {
+ $changes = true;
+ $user->name = $name;
+ $profile->name = $name;
+ }
- if ($profile->website != $website) {
- $changes = true;
- $profile->website = $website;
- }
+ if ($profile->website != $website) {
+ $changes = true;
+ $profile->website = $website;
+ }
- if (strip_tags($profile->bio) != $bio) {
- $changes = true;
- $profile->bio = Autolink::create()->autolink($bio);
- }
+ if (strip_tags($profile->bio) != $bio) {
+ $changes = true;
+ $profile->bio = Autolink::create()->autolink($bio);
+ }
- if($user->language != $language &&
- in_array($language, \App\Util\Localization\Localization::languages())
- ) {
- $changes = true;
- $user->language = $language;
- session()->put('locale', $language);
- }
+ if($user->language != $language &&
+ in_array($language, \App\Util\Localization\Localization::languages())
+ ) {
+ $changes = true;
+ $user->language = $language;
+ session()->put('locale', $language);
+ }
- if($existingPronouns != $pronouns) {
- if($pronouns && in_array('Select Pronoun(s)', $pronouns)) {
- PronounService::clear($profile->id);
- } else {
- PronounService::put($profile->id, $pronouns);
- }
- }
- }
+ if($existingPronouns != $pronouns) {
+ if($pronouns && in_array('Select Pronoun(s)', $pronouns)) {
+ PronounService::clear($profile->id);
+ } else {
+ PronounService::put($profile->id, $pronouns);
+ }
+ }
+ }
- if ($changes === true) {
- $user->save();
- $profile->save();
- Cache::forget('user:account:id:'.$user->id);
- AccountService::del($profile->id);
- return redirect('/settings/home')->with('status', 'Profile successfully updated!');
- }
+ if ($changes === true) {
+ $user->save();
+ $profile->save();
+ Cache::forget('user:account:id:'.$user->id);
+ AccountService::del($profile->id);
+ return redirect('/settings/home')->with('status', 'Profile successfully updated!');
+ }
- return redirect('/settings/home');
- }
+ return redirect('/settings/home');
+ }
- public function password()
- {
- return view('settings.password');
- }
+ public function password()
+ {
+ return view('settings.password');
+ }
- public function passwordUpdate(Request $request)
- {
- $this->validate($request, [
- 'current' => 'required|string',
- 'password' => 'required|string',
- 'password_confirmation' => 'required|string',
- ]);
+ public function passwordUpdate(Request $request)
+ {
+ $this->validate($request, [
+ 'current' => 'required|string',
+ 'password' => 'required|string',
+ 'password_confirmation' => 'required|string',
+ ]);
- $current = $request->input('current');
- $new = $request->input('password');
- $confirm = $request->input('password_confirmation');
+ $current = $request->input('current');
+ $new = $request->input('password');
+ $confirm = $request->input('password_confirmation');
- $user = Auth::user();
+ $user = Auth::user();
- if (password_verify($current, $user->password) && $new === $confirm) {
- $user->password = bcrypt($new);
- $user->save();
+ if (password_verify($current, $user->password) && $new === $confirm) {
+ $user->password = bcrypt($new);
+ $user->save();
- $log = new AccountLog();
- $log->user_id = $user->id;
- $log->item_id = $user->id;
- $log->item_type = 'App\User';
- $log->action = 'account.edit.password';
- $log->message = 'Password changed';
- $log->link = null;
- $log->ip_address = $request->ip();
- $log->user_agent = $request->userAgent();
- $log->save();
+ $log = new AccountLog();
+ $log->user_id = $user->id;
+ $log->item_id = $user->id;
+ $log->item_type = 'App\User';
+ $log->action = 'account.edit.password';
+ $log->message = 'Password changed';
+ $log->link = null;
+ $log->ip_address = $request->ip();
+ $log->user_agent = $request->userAgent();
+ $log->save();
- Mail::to($request->user())->send(new PasswordChange($user));
- return redirect('/settings/home')->with('status', 'Password successfully updated!');
- } else {
- return redirect()->back()->with('error', 'There was an error with your request! Please try again.');
- }
+ Mail::to($request->user())->send(new PasswordChange($user));
+ return redirect('/settings/home')->with('status', 'Password successfully updated!');
+ } else {
+ return redirect()->back()->with('error', 'There was an error with your request! Please try again.');
+ }
- }
+ }
- public function email()
- {
- return view('settings.email');
- }
+ public function email()
+ {
+ return view('settings.email');
+ }
- public function emailUpdate(Request $request)
- {
- $this->validate($request, [
- 'email' => 'required|email|unique:users,email',
- ]);
- $changes = false;
- $email = $request->input('email');
- $user = Auth::user();
- $profile = $user->profile;
+ public function emailUpdate(Request $request)
+ {
+ $this->validate($request, [
+ 'email' => 'required|email|unique:users,email',
+ ]);
+ $changes = false;
+ $email = $request->input('email');
+ $user = Auth::user();
+ $profile = $user->profile;
- $validate = config_cache('pixelfed.enforce_email_verification');
+ $validate = config_cache('pixelfed.enforce_email_verification');
- if ($user->email != $email) {
- $changes = true;
- $user->email = $email;
+ if ($user->email != $email) {
+ $changes = true;
+ $user->email = $email;
- if ($validate) {
- $user->email_verified_at = null;
- // Prevent old verifications from working
- EmailVerification::whereUserId($user->id)->delete();
- }
+ if ($validate) {
+ // auto verify admin email addresses
+ $user->email_verified_at = $user->is_admin == true ? now() : null;
+ // Prevent old verifications from working
+ EmailVerification::whereUserId($user->id)->delete();
+ }
- $log = new AccountLog();
- $log->user_id = $user->id;
- $log->item_id = $user->id;
- $log->item_type = 'App\User';
- $log->action = 'account.edit.email';
- $log->message = 'Email changed';
- $log->link = null;
- $log->ip_address = $request->ip();
- $log->user_agent = $request->userAgent();
- $log->save();
- }
+ $log = new AccountLog();
+ $log->user_id = $user->id;
+ $log->item_id = $user->id;
+ $log->item_type = 'App\User';
+ $log->action = 'account.edit.email';
+ $log->message = 'Email changed';
+ $log->link = null;
+ $log->ip_address = $request->ip();
+ $log->user_agent = $request->userAgent();
+ $log->save();
+ }
- if ($changes === true) {
- Cache::forget('user:account:id:'.$user->id);
- $user->save();
- $profile->save();
+ if ($changes === true) {
+ Cache::forget('user:account:id:'.$user->id);
+ $user->save();
+ $profile->save();
- return redirect('/settings/home')->with('status', 'Email successfully updated!');
- } else {
- return redirect('/settings/email');
- }
+ return redirect('/settings/email')->with('status', 'Email successfully updated!');
+ } else {
+ return redirect('/settings/email');
+ }
- }
-
- public function avatar()
- {
- return view('settings.avatar');
- }
+ }
+ public function avatar()
+ {
+ return view('settings.avatar');
+ }
}
diff --git a/resources/views/settings/email.blade.php b/resources/views/settings/email.blade.php
index 7286896b1..4b7ddd677 100644
--- a/resources/views/settings/email.blade.php
+++ b/resources/views/settings/email.blade.php
@@ -1,63 +1,36 @@
-@extends('layouts.app')
+@extends('settings.template')
-@section('content')
-@if (session('status'))
-
- {{ session('status') }}
-
-@endif
-@if ($errors->any())
-
- @foreach($errors->all() as $error)
-
{{ $error }}
- @endforeach
-
-@endif
-@if (session('error'))
-
- {{ session('error') }}
-
-@endif
+@section('section')
-
-
-
-
-
-
-
-
Email Settings
-
-
-
-
-
-
+
+
+
@endsection
diff --git a/resources/views/settings/partial/sidebar.blade.php b/resources/views/settings/partial/sidebar.blade.php
index a3837066a..2d5913550 100644
--- a/resources/views/settings/partial/sidebar.blade.php
+++ b/resources/views/settings/partial/sidebar.blade.php
@@ -1,79 +1,80 @@
-
+
+ @push('styles')
+
+ @endpush
From 58745a88081fae04d9057492e8fb5e04a645cdb1 Mon Sep 17 00:00:00 2001
From: Daniel Supernault
Date: Thu, 11 Jan 2024 02:37:13 -0700
Subject: [PATCH 05/16] Update settings sidebar
---
resources/views/settings/partial/sidebar.blade.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/resources/views/settings/partial/sidebar.blade.php b/resources/views/settings/partial/sidebar.blade.php
index 2d5913550..b971e1f5d 100644
--- a/resources/views/settings/partial/sidebar.blade.php
+++ b/resources/views/settings/partial/sidebar.blade.php
@@ -17,9 +17,9 @@
Media
-
+ {{--
Notifications
-
+ --}}
Password
From 42298a2e9ccf14679cf18de0c757daa85f282e5f Mon Sep 17 00:00:00 2001
From: Daniel Supernault
Date: Thu, 11 Jan 2024 02:40:52 -0700
Subject: [PATCH 06/16] Apply dangerZone middleware to parental controls routes
---
routes/web.php | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/routes/web.php b/routes/web.php
index dffd5e271..e71e6fd9e 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -536,15 +536,15 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
});
- Route::get('parental-controls', 'ParentalControlsController@index');
- Route::get('parental-controls/add', 'ParentalControlsController@add')->name('settings.pc.add');
- Route::post('parental-controls/add', 'ParentalControlsController@store');
- Route::get('parental-controls/manage/{id}', 'ParentalControlsController@view');
- Route::post('parental-controls/manage/{id}', 'ParentalControlsController@update');
- Route::get('parental-controls/manage/{id}/cancel-invite', 'ParentalControlsController@cancelInvite')->name('settings.pc.cancel-invite');
- Route::post('parental-controls/manage/{id}/cancel-invite', 'ParentalControlsController@cancelInviteHandle');
- Route::get('parental-controls/manage/{id}/stop-managing', 'ParentalControlsController@stopManaging')->name('settings.pc.stop-managing');
- Route::post('parental-controls/manage/{id}/stop-managing', 'ParentalControlsController@stopManagingHandle');
+ Route::get('parental-controls', 'ParentalControlsController@index')->name('settings.parental-controls')->middleware('dangerzone');
+ Route::get('parental-controls/add', 'ParentalControlsController@add')->name('settings.pc.add')->middleware('dangerzone');
+ Route::post('parental-controls/add', 'ParentalControlsController@store')->middleware('dangerzone');
+ Route::get('parental-controls/manage/{id}', 'ParentalControlsController@view')->middleware('dangerzone');
+ Route::post('parental-controls/manage/{id}', 'ParentalControlsController@update')->middleware('dangerzone');
+ Route::get('parental-controls/manage/{id}/cancel-invite', 'ParentalControlsController@cancelInvite')->name('settings.pc.cancel-invite')->middleware('dangerzone');
+ Route::post('parental-controls/manage/{id}/cancel-invite', 'ParentalControlsController@cancelInviteHandle')->middleware('dangerzone');
+ Route::get('parental-controls/manage/{id}/stop-managing', 'ParentalControlsController@stopManaging')->name('settings.pc.stop-managing')->middleware('dangerzone');
+ Route::post('parental-controls/manage/{id}/stop-managing', 'ParentalControlsController@stopManagingHandle')->middleware('dangerzone');
Route::get('applications', 'SettingsController@applications')->name('settings.applications')->middleware('dangerzone');
Route::get('data-export', 'SettingsController@dataExport')->name('settings.dataexport')->middleware('dangerzone');
From 1a16ec2078db594d19dea77f59371af65600498c Mon Sep 17 00:00:00 2001
From: Daniel Supernault
Date: Thu, 11 Jan 2024 03:22:35 -0700
Subject: [PATCH 07/16] Update BookmarkController, add parental control support
---
app/Http/Controllers/Api/ApiV1Controller.php | 2 +
app/Http/Controllers/BookmarkController.php | 86 ++++++++++----------
2 files changed, 43 insertions(+), 45 deletions(-)
diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php
index 993df3555..8f41b38fb 100644
--- a/app/Http/Controllers/Api/ApiV1Controller.php
+++ b/app/Http/Controllers/Api/ApiV1Controller.php
@@ -3438,6 +3438,7 @@ class ApiV1Controller extends Controller
$status = Status::findOrFail($id);
$pid = $request->user()->profile_id;
+ abort_if($user->has_roles && !UserRoleService::can('can-bookmark', $user->id), 403, 'Invalid permissions for this action');
abort_if($status->in_reply_to_id || $status->reblog_of_id, 404);
abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404);
abort_if(!in_array($status->type, ['photo','photo:album', 'video', 'video:album', 'photo:video:album']), 404);
@@ -3477,6 +3478,7 @@ class ApiV1Controller extends Controller
$status = Status::findOrFail($id);
$pid = $request->user()->profile_id;
+ abort_if($user->has_roles && !UserRoleService::can('can-bookmark', $user->id), 403, 'Invalid permissions for this action');
abort_if($status->in_reply_to_id || $status->reblog_of_id, 404);
abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404);
abort_if(!in_array($status->type, ['photo','photo:album', 'video', 'video:album', 'photo:video:album']), 404);
diff --git a/app/Http/Controllers/BookmarkController.php b/app/Http/Controllers/BookmarkController.php
index a24520d64..d1d793dd2 100644
--- a/app/Http/Controllers/BookmarkController.php
+++ b/app/Http/Controllers/BookmarkController.php
@@ -8,60 +8,56 @@ use Auth;
use Illuminate\Http\Request;
use App\Services\BookmarkService;
use App\Services\FollowerService;
+use App\Services\UserRoleService;
class BookmarkController extends Controller
{
- public function __construct()
- {
- $this->middleware('auth');
- }
+ public function __construct()
+ {
+ $this->middleware('auth');
+ }
- public function store(Request $request)
- {
- $this->validate($request, [
- 'item' => 'required|integer|min:1',
- ]);
+ public function store(Request $request)
+ {
+ $this->validate($request, [
+ 'item' => 'required|integer|min:1',
+ ]);
- $profile = Auth::user()->profile;
- $status = Status::findOrFail($request->input('item'));
+ $user = $request->user();
+ $status = Status::findOrFail($request->input('item'));
- abort_if($status->in_reply_to_id || $status->reblog_of_id, 404);
- abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404);
- abort_if(!in_array($status->type, ['photo','photo:album', 'video', 'video:album', 'photo:video:album']), 404);
+ abort_if($user->has_roles && !UserRoleService::can('can-bookmark', $user->id), 403, 'Invalid permissions for this action');
+ abort_if($status->in_reply_to_id || $status->reblog_of_id, 404);
+ abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404);
+ abort_if(!in_array($status->type, ['photo','photo:album', 'video', 'video:album', 'photo:video:album']), 404);
- if($status->scope == 'private') {
- if($profile->id !== $status->profile_id && !FollowerService::follows($profile->id, $status->profile_id)) {
- if($exists = Bookmark::whereStatusId($status->id)->whereProfileId($profile->id)->first()) {
- BookmarkService::del($profile->id, $status->id);
- $exists->delete();
+ if($status->scope == 'private') {
+ if($user->profile_id !== $status->profile_id && !FollowerService::follows($user->profile_id, $status->profile_id)) {
+ if($exists = Bookmark::whereStatusId($status->id)->whereProfileId($user->profile_id)->first()) {
+ BookmarkService::del($user->profile_id, $status->id);
+ $exists->delete();
- if ($request->ajax()) {
- return ['code' => 200, 'msg' => 'Bookmark removed!'];
- } else {
- return redirect()->back();
- }
- }
- abort(404, 'Error: You cannot bookmark private posts from accounts you do not follow.');
- }
- }
+ if ($request->ajax()) {
+ return ['code' => 200, 'msg' => 'Bookmark removed!'];
+ } else {
+ return redirect()->back();
+ }
+ }
+ abort(404, 'Error: You cannot bookmark private posts from accounts you do not follow.');
+ }
+ }
- $bookmark = Bookmark::firstOrCreate(
- ['status_id' => $status->id], ['profile_id' => $profile->id]
- );
+ $bookmark = Bookmark::firstOrCreate(
+ ['status_id' => $status->id], ['profile_id' => $user->profile_id]
+ );
- if (!$bookmark->wasRecentlyCreated) {
- BookmarkService::del($profile->id, $status->id);
- $bookmark->delete();
- } else {
- BookmarkService::add($profile->id, $status->id);
- }
+ if (!$bookmark->wasRecentlyCreated) {
+ BookmarkService::del($user->profile_id, $status->id);
+ $bookmark->delete();
+ } else {
+ BookmarkService::add($user->profile_id, $status->id);
+ }
- if ($request->ajax()) {
- $response = ['code' => 200, 'msg' => 'Bookmark saved!'];
- } else {
- $response = redirect()->back();
- }
-
- return $response;
- }
+ return $request->expectsJson() ? ['code' => 200, 'msg' => 'Bookmark saved!'] : redirect()->back();
+ }
}
From 2dcfc814958c9ebb623f876338bea35e0e689c5e Mon Sep 17 00:00:00 2001
From: Daniel Supernault
Date: Thu, 11 Jan 2024 04:40:25 -0700
Subject: [PATCH 08/16] Update ComposeController, add parental controls support
---
app/Http/Controllers/Api/ApiV1Controller.php | 11 +
app/Http/Controllers/ComposeController.php | 1280 +++++++++---------
2 files changed, 661 insertions(+), 630 deletions(-)
diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php
index 8f41b38fb..dde416064 100644
--- a/app/Http/Controllers/Api/ApiV1Controller.php
+++ b/app/Http/Controllers/Api/ApiV1Controller.php
@@ -1750,6 +1750,8 @@ class ApiV1Controller extends Controller
]);
$user = $request->user();
+ abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
+
AccountService::setLastActive($user->id);
$media = Media::whereUserId($user->id)
@@ -2983,6 +2985,15 @@ class ApiV1Controller extends Controller
$in_reply_to_id = $request->input('in_reply_to_id');
$user = $request->user();
+
+ if($user->has_roles) {
+ if($in_reply_to_id != null) {
+ abort_if(!UserRoleService::can('can-comment', $user->id), 403, 'Invalid permissions for this action');
+ } else {
+ abort_if(!UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
+ }
+ }
+
$profile = $user->profile;
$limitKey = 'compose:rate-limit:store:' . $user->id;
diff --git a/app/Http/Controllers/ComposeController.php b/app/Http/Controllers/ComposeController.php
index e79625861..e17a37fd7 100644
--- a/app/Http/Controllers/ComposeController.php
+++ b/app/Http/Controllers/ComposeController.php
@@ -6,26 +6,26 @@ use Illuminate\Http\Request;
use Auth, Cache, DB, Storage, URL;
use Carbon\Carbon;
use App\{
- Avatar,
- Collection,
- CollectionItem,
- Hashtag,
- Like,
- Media,
- MediaTag,
- Notification,
- Profile,
- Place,
- Status,
- UserFilter,
- UserSetting
+ Avatar,
+ Collection,
+ CollectionItem,
+ Hashtag,
+ Like,
+ Media,
+ MediaTag,
+ Notification,
+ Profile,
+ Place,
+ Status,
+ UserFilter,
+ UserSetting
};
use App\Models\Poll;
use App\Transformer\Api\{
- MediaTransformer,
- MediaDraftTransformer,
- StatusTransformer,
- StatusStatelessTransformer
+ MediaTransformer,
+ MediaDraftTransformer,
+ StatusTransformer,
+ StatusStatelessTransformer
};
use League\Fractal;
use App\Util\Media\Filter;
@@ -36,9 +36,9 @@ use App\Jobs\ImageOptimizePipeline\ImageOptimize;
use App\Jobs\ImageOptimizePipeline\ImageThumbnail;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Jobs\VideoPipeline\{
- VideoOptimize,
- VideoPostProcess,
- VideoThumbnail
+ VideoOptimize,
+ VideoPostProcess,
+ VideoThumbnail
};
use App\Services\AccountService;
use App\Services\CollectionService;
@@ -58,230 +58,234 @@ use App\Services\UserRoleService;
class ComposeController extends Controller
{
- protected $fractal;
+ protected $fractal;
- public function __construct()
- {
- $this->middleware('auth');
- $this->fractal = new Fractal\Manager();
- $this->fractal->setSerializer(new ArraySerializer());
- }
+ public function __construct()
+ {
+ $this->middleware('auth');
+ $this->fractal = new Fractal\Manager();
+ $this->fractal->setSerializer(new ArraySerializer());
+ }
- public function show(Request $request)
- {
- return view('status.compose');
- }
+ public function show(Request $request)
+ {
+ return view('status.compose');
+ }
- public function mediaUpload(Request $request)
- {
- abort_if(!$request->user(), 403);
+ public function mediaUpload(Request $request)
+ {
+ abort_if(!$request->user(), 403);
- $this->validate($request, [
- 'file.*' => [
- 'required_without:file',
- 'mimetypes:' . config_cache('pixelfed.media_types'),
- 'max:' . config_cache('pixelfed.max_photo_size'),
- ],
- 'file' => [
- 'required_without:file.*',
- 'mimetypes:' . config_cache('pixelfed.media_types'),
- 'max:' . config_cache('pixelfed.max_photo_size'),
- ],
- 'filter_name' => 'nullable|string|max:24',
- 'filter_class' => 'nullable|alpha_dash|max:24'
- ]);
+ $this->validate($request, [
+ 'file.*' => [
+ 'required_without:file',
+ 'mimetypes:' . config_cache('pixelfed.media_types'),
+ 'max:' . config_cache('pixelfed.max_photo_size'),
+ ],
+ 'file' => [
+ 'required_without:file.*',
+ 'mimetypes:' . config_cache('pixelfed.media_types'),
+ 'max:' . config_cache('pixelfed.max_photo_size'),
+ ],
+ 'filter_name' => 'nullable|string|max:24',
+ 'filter_class' => 'nullable|alpha_dash|max:24'
+ ]);
- $user = Auth::user();
- $profile = $user->profile;
- abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
+ $user = Auth::user();
+ $profile = $user->profile;
+ abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
- $limitKey = 'compose:rate-limit:media-upload:' . $user->id;
- $limitTtl = now()->addMinutes(15);
- $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
- $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
+ $limitKey = 'compose:rate-limit:media-upload:' . $user->id;
+ $limitTtl = now()->addMinutes(15);
+ $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
+ $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
- return $dailyLimit >= 1250;
- });
+ return $dailyLimit >= 1250;
+ });
- abort_if($limitReached == true, 429);
+ abort_if($limitReached == true, 429);
- if(config_cache('pixelfed.enforce_account_limit') == true) {
- $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
- return Media::whereUserId($user->id)->sum('size') / 1000;
- });
- $limit = (int) config_cache('pixelfed.max_account_size');
- if ($size >= $limit) {
- abort(403, 'Account size limit reached.');
- }
- }
+ if(config_cache('pixelfed.enforce_account_limit') == true) {
+ $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
+ return Media::whereUserId($user->id)->sum('size') / 1000;
+ });
+ $limit = (int) config_cache('pixelfed.max_account_size');
+ if ($size >= $limit) {
+ abort(403, 'Account size limit reached.');
+ }
+ }
- $filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
- $filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
+ $filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
+ $filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
- $photo = $request->file('file');
+ $photo = $request->file('file');
- $mimes = explode(',', config_cache('pixelfed.media_types'));
+ $mimes = explode(',', config_cache('pixelfed.media_types'));
- abort_if(in_array($photo->getMimeType(), $mimes) == false, 400, 'Invalid media format');
+ abort_if(in_array($photo->getMimeType(), $mimes) == false, 400, 'Invalid media format');
- $storagePath = MediaPathService::get($user, 2);
- $path = $photo->storePublicly($storagePath);
- $hash = \hash_file('sha256', $photo);
- $mime = $photo->getMimeType();
+ $storagePath = MediaPathService::get($user, 2);
+ $path = $photo->storePublicly($storagePath);
+ $hash = \hash_file('sha256', $photo);
+ $mime = $photo->getMimeType();
- abort_if(MediaBlocklistService::exists($hash) == true, 451);
+ abort_if(MediaBlocklistService::exists($hash) == true, 451);
- $media = new Media();
- $media->status_id = null;
- $media->profile_id = $profile->id;
- $media->user_id = $user->id;
- $media->media_path = $path;
- $media->original_sha256 = $hash;
- $media->size = $photo->getSize();
- $media->mime = $mime;
- $media->filter_class = $filterClass;
- $media->filter_name = $filterName;
- $media->version = 3;
- $media->save();
+ $media = new Media();
+ $media->status_id = null;
+ $media->profile_id = $profile->id;
+ $media->user_id = $user->id;
+ $media->media_path = $path;
+ $media->original_sha256 = $hash;
+ $media->size = $photo->getSize();
+ $media->mime = $mime;
+ $media->filter_class = $filterClass;
+ $media->filter_name = $filterName;
+ $media->version = 3;
+ $media->save();
- $preview_url = $media->url() . '?v=' . time();
- $url = $media->url() . '?v=' . time();
+ $preview_url = $media->url() . '?v=' . time();
+ $url = $media->url() . '?v=' . time();
- switch ($media->mime) {
- case 'image/jpeg':
- case 'image/png':
- case 'image/webp':
- ImageOptimize::dispatch($media)->onQueue('mmo');
- break;
+ switch ($media->mime) {
+ case 'image/jpeg':
+ case 'image/png':
+ case 'image/webp':
+ ImageOptimize::dispatch($media)->onQueue('mmo');
+ break;
- case 'video/mp4':
- VideoThumbnail::dispatch($media)->onQueue('mmo');
- $preview_url = '/storage/no-preview.png';
- $url = '/storage/no-preview.png';
- break;
+ case 'video/mp4':
+ VideoThumbnail::dispatch($media)->onQueue('mmo');
+ $preview_url = '/storage/no-preview.png';
+ $url = '/storage/no-preview.png';
+ break;
- default:
- break;
- }
+ default:
+ break;
+ }
- Cache::forget($limitKey);
- $resource = new Fractal\Resource\Item($media, new MediaTransformer());
- $res = $this->fractal->createData($resource)->toArray();
- $res['preview_url'] = $preview_url;
- $res['url'] = $url;
- return response()->json($res);
- }
+ Cache::forget($limitKey);
+ $resource = new Fractal\Resource\Item($media, new MediaTransformer());
+ $res = $this->fractal->createData($resource)->toArray();
+ $res['preview_url'] = $preview_url;
+ $res['url'] = $url;
+ return response()->json($res);
+ }
- public function mediaUpdate(Request $request)
- {
- $this->validate($request, [
- 'id' => 'required',
- 'file' => function() {
- return [
- 'required',
- 'mimetypes:' . config_cache('pixelfed.media_types'),
- 'max:' . config_cache('pixelfed.max_photo_size'),
- ];
- },
- ]);
+ public function mediaUpdate(Request $request)
+ {
+ $this->validate($request, [
+ 'id' => 'required',
+ 'file' => function() {
+ return [
+ 'required',
+ 'mimetypes:' . config_cache('pixelfed.media_types'),
+ 'max:' . config_cache('pixelfed.max_photo_size'),
+ ];
+ },
+ ]);
- $user = Auth::user();
- abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
+ $user = Auth::user();
+ abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
- $limitKey = 'compose:rate-limit:media-updates:' . $user->id;
- $limitTtl = now()->addMinutes(15);
- $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
- $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
+ $limitKey = 'compose:rate-limit:media-updates:' . $user->id;
+ $limitTtl = now()->addMinutes(15);
+ $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
+ $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
- return $dailyLimit >= 1500;
- });
+ return $dailyLimit >= 1500;
+ });
- abort_if($limitReached == true, 429);
+ abort_if($limitReached == true, 429);
- $photo = $request->file('file');
- $id = $request->input('id');
+ $photo = $request->file('file');
+ $id = $request->input('id');
- $media = Media::whereUserId($user->id)
- ->whereProfileId($user->profile_id)
- ->whereNull('status_id')
- ->findOrFail($id);
+ $media = Media::whereUserId($user->id)
+ ->whereProfileId($user->profile_id)
+ ->whereNull('status_id')
+ ->findOrFail($id);
- $media->save();
+ $media->save();
- $fragments = explode('/', $media->media_path);
- $name = last($fragments);
- array_pop($fragments);
- $dir = implode('/', $fragments);
- $path = $photo->storePubliclyAs($dir, $name);
- $res = [
- 'url' => $media->url() . '?v=' . time()
- ];
- ImageOptimize::dispatch($media)->onQueue('mmo');
- Cache::forget($limitKey);
- return $res;
- }
+ $fragments = explode('/', $media->media_path);
+ $name = last($fragments);
+ array_pop($fragments);
+ $dir = implode('/', $fragments);
+ $path = $photo->storePubliclyAs($dir, $name);
+ $res = [
+ 'url' => $media->url() . '?v=' . time()
+ ];
+ ImageOptimize::dispatch($media)->onQueue('mmo');
+ Cache::forget($limitKey);
+ return $res;
+ }
- public function mediaDelete(Request $request)
- {
- abort_if(!$request->user(), 403);
+ public function mediaDelete(Request $request)
+ {
+ abort_if(!$request->user(), 403);
- $this->validate($request, [
- 'id' => 'required|integer|min:1|exists:media,id'
- ]);
+ $this->validate($request, [
+ 'id' => 'required|integer|min:1|exists:media,id'
+ ]);
- $media = Media::whereNull('status_id')
- ->whereUserId(Auth::id())
- ->findOrFail($request->input('id'));
+ abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
- MediaStorageService::delete($media, true);
+ $media = Media::whereNull('status_id')
+ ->whereUserId(Auth::id())
+ ->findOrFail($request->input('id'));
- return response()->json([
- 'msg' => 'Successfully deleted',
- 'code' => 200
- ]);
- }
+ MediaStorageService::delete($media, true);
- public function searchTag(Request $request)
- {
- abort_if(!$request->user(), 403);
+ return response()->json([
+ 'msg' => 'Successfully deleted',
+ 'code' => 200
+ ]);
+ }
- $this->validate($request, [
- 'q' => 'required|string|min:1|max:50'
- ]);
+ public function searchTag(Request $request)
+ {
+ abort_if(!$request->user(), 403);
- $q = $request->input('q');
+ $this->validate($request, [
+ 'q' => 'required|string|min:1|max:50'
+ ]);
- if(Str::of($q)->startsWith('@')) {
- if(strlen($q) < 3) {
- return [];
- }
- $q = mb_substr($q, 1);
- }
+ $q = $request->input('q');
- $blocked = UserFilter::whereFilterableType('App\Profile')
- ->whereFilterType('block')
- ->whereFilterableId($request->user()->profile_id)
- ->pluck('user_id');
+ if(Str::of($q)->startsWith('@')) {
+ if(strlen($q) < 3) {
+ return [];
+ }
+ $q = mb_substr($q, 1);
+ }
- $blocked->push($request->user()->profile_id);
+ abort_if($user->has_roles && !UserRoleService::can('can-post', $user->id), 403, 'Invalid permissions for this action');
- $results = Profile::select('id','domain','username')
- ->whereNotIn('id', $blocked)
- ->whereNull('domain')
- ->where('username','like','%'.$q.'%')
- ->limit(15)
- ->get()
- ->map(function($r) {
- return [
- 'id' => (string) $r->id,
- 'name' => $r->username,
- 'privacy' => true,
- 'avatar' => $r->avatarUrl()
- ];
- });
+ $blocked = UserFilter::whereFilterableType('App\Profile')
+ ->whereFilterType('block')
+ ->whereFilterableId($request->user()->profile_id)
+ ->pluck('user_id');
- return $results;
- }
+ $blocked->push($request->user()->profile_id);
+
+ $results = Profile::select('id','domain','username')
+ ->whereNotIn('id', $blocked)
+ ->whereNull('domain')
+ ->where('username','like','%'.$q.'%')
+ ->limit(15)
+ ->get()
+ ->map(function($r) {
+ return [
+ 'id' => (string) $r->id,
+ 'name' => $r->username,
+ 'privacy' => true,
+ 'avatar' => $r->avatarUrl()
+ ];
+ });
+
+ return $results;
+ }
public function searchUntag(Request $request)
{
@@ -292,6 +296,8 @@ class ComposeController extends Controller
'profile_id' => 'required'
]);
+ abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
+
$user = $request->user();
$status_id = $request->input('status_id');
$profile_id = (int) $request->input('profile_id');
@@ -316,506 +322,520 @@ class ComposeController extends Controller
return [200];
}
- public function searchLocation(Request $request)
- {
- abort_if(!$request->user(), 403);
- $this->validate($request, [
- 'q' => 'required|string|max:100'
- ]);
- $pid = $request->user()->profile_id;
- abort_if(!$pid, 400);
- $q = e($request->input('q'));
+ public function searchLocation(Request $request)
+ {
+ abort_if(!$request->user(), 403);
+ $this->validate($request, [
+ 'q' => 'required|string|max:100'
+ ]);
+ abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
+ $pid = $request->user()->profile_id;
+ abort_if(!$pid, 400);
+ $q = e($request->input('q'));
- $popular = Cache::remember('pf:search:location:v1:popular', 1209600, function() {
- $minId = SnowflakeService::byDate(now()->subDays(290));
- if(config('database.default') == 'pgsql') {
- return Status::selectRaw('id, place_id, count(place_id) as pc')
- ->whereNotNull('place_id')
- ->where('id', '>', $minId)
- ->orderByDesc('pc')
- ->groupBy(['place_id', 'id'])
- ->limit(400)
- ->get()
- ->filter(function($post) {
- return $post;
- })
- ->map(function($place) {
- return [
- 'id' => $place->place_id,
- 'count' => $place->pc
- ];
- })
- ->unique('id')
- ->values();
- }
- return Status::selectRaw('id, place_id, count(place_id) as pc')
- ->whereNotNull('place_id')
- ->where('id', '>', $minId)
- ->groupBy('place_id')
- ->orderByDesc('pc')
- ->limit(400)
- ->get()
- ->filter(function($post) {
- return $post;
- })
- ->map(function($place) {
- return [
- 'id' => $place->place_id,
- 'count' => $place->pc
- ];
- });
- });
- $q = '%' . $q . '%';
- $wildcard = config('database.default') === 'pgsql' ? 'ilike' : 'like';
+ $popular = Cache::remember('pf:search:location:v1:popular', 1209600, function() {
+ $minId = SnowflakeService::byDate(now()->subDays(290));
+ if(config('database.default') == 'pgsql') {
+ return Status::selectRaw('id, place_id, count(place_id) as pc')
+ ->whereNotNull('place_id')
+ ->where('id', '>', $minId)
+ ->orderByDesc('pc')
+ ->groupBy(['place_id', 'id'])
+ ->limit(400)
+ ->get()
+ ->filter(function($post) {
+ return $post;
+ })
+ ->map(function($place) {
+ return [
+ 'id' => $place->place_id,
+ 'count' => $place->pc
+ ];
+ })
+ ->unique('id')
+ ->values();
+ }
+ return Status::selectRaw('id, place_id, count(place_id) as pc')
+ ->whereNotNull('place_id')
+ ->where('id', '>', $minId)
+ ->groupBy('place_id')
+ ->orderByDesc('pc')
+ ->limit(400)
+ ->get()
+ ->filter(function($post) {
+ return $post;
+ })
+ ->map(function($place) {
+ return [
+ 'id' => $place->place_id,
+ 'count' => $place->pc
+ ];
+ });
+ });
+ $q = '%' . $q . '%';
+ $wildcard = config('database.default') === 'pgsql' ? 'ilike' : 'like';
- $places = DB::table('places')
- ->where('name', $wildcard, $q)
- ->limit((strlen($q) > 5 ? 360 : 30))
- ->get()
- ->sortByDesc(function($place, $key) use($popular) {
- return $popular->filter(function($p) use($place) {
- return $p['id'] == $place->id;
- })->map(function($p) use($place) {
- return in_array($place->country, ['Canada', 'USA', 'France', 'Germany', 'United Kingdom']) ? $p['count'] : 1;
- })->values();
- })
- ->map(function($r) {
- return [
- 'id' => $r->id,
- 'name' => $r->name,
- 'country' => $r->country,
- 'url' => url('/discover/places/' . $r->id . '/' . $r->slug)
- ];
- })
- ->values()
- ->all();
- return $places;
- }
+ $places = DB::table('places')
+ ->where('name', $wildcard, $q)
+ ->limit((strlen($q) > 5 ? 360 : 30))
+ ->get()
+ ->sortByDesc(function($place, $key) use($popular) {
+ return $popular->filter(function($p) use($place) {
+ return $p['id'] == $place->id;
+ })->map(function($p) use($place) {
+ return in_array($place->country, ['Canada', 'USA', 'France', 'Germany', 'United Kingdom']) ? $p['count'] : 1;
+ })->values();
+ })
+ ->map(function($r) {
+ return [
+ 'id' => $r->id,
+ 'name' => $r->name,
+ 'country' => $r->country,
+ 'url' => url('/discover/places/' . $r->id . '/' . $r->slug)
+ ];
+ })
+ ->values()
+ ->all();
+ return $places;
+ }
- public function searchMentionAutocomplete(Request $request)
- {
- abort_if(!$request->user(), 403);
+ public function searchMentionAutocomplete(Request $request)
+ {
+ abort_if(!$request->user(), 403);
- $this->validate($request, [
- 'q' => 'required|string|min:2|max:50'
- ]);
+ $this->validate($request, [
+ 'q' => 'required|string|min:2|max:50'
+ ]);
- $q = $request->input('q');
+ abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
- if(Str::of($q)->startsWith('@')) {
- if(strlen($q) < 3) {
- return [];
- }
- }
+ $q = $request->input('q');
- $blocked = UserFilter::whereFilterableType('App\Profile')
- ->whereFilterType('block')
- ->whereFilterableId($request->user()->profile_id)
- ->pluck('user_id');
+ if(Str::of($q)->startsWith('@')) {
+ if(strlen($q) < 3) {
+ return [];
+ }
+ }
- $blocked->push($request->user()->profile_id);
+ $blocked = UserFilter::whereFilterableType('App\Profile')
+ ->whereFilterType('block')
+ ->whereFilterableId($request->user()->profile_id)
+ ->pluck('user_id');
- $results = Profile::select('id','domain','username')
- ->whereNotIn('id', $blocked)
- ->where('username','like','%'.$q.'%')
- ->groupBy('id', 'domain')
- ->limit(15)
- ->get()
- ->map(function($profile) {
- $username = $profile->domain ? substr($profile->username, 1) : $profile->username;
+ $blocked->push($request->user()->profile_id);
+
+ $results = Profile::select('id','domain','username')
+ ->whereNotIn('id', $blocked)
+ ->where('username','like','%'.$q.'%')
+ ->groupBy('id', 'domain')
+ ->limit(15)
+ ->get()
+ ->map(function($profile) {
+ $username = $profile->domain ? substr($profile->username, 1) : $profile->username;
return [
'key' => '@' . str_limit($username, 30),
'value' => $username,
];
- });
+ });
- return $results;
- }
+ return $results;
+ }
- public function searchHashtagAutocomplete(Request $request)
- {
- abort_if(!$request->user(), 403);
+ public function searchHashtagAutocomplete(Request $request)
+ {
+ abort_if(!$request->user(), 403);
- $this->validate($request, [
- 'q' => 'required|string|min:2|max:50'
- ]);
+ $this->validate($request, [
+ 'q' => 'required|string|min:2|max:50'
+ ]);
- $q = $request->input('q');
+ abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
- $results = Hashtag::select('slug')
- ->where('slug', 'like', '%'.$q.'%')
- ->whereIsNsfw(false)
- ->whereIsBanned(false)
- ->limit(5)
- ->get()
- ->map(function($tag) {
- return [
- 'key' => '#' . $tag->slug,
- 'value' => $tag->slug
- ];
- });
+ $q = $request->input('q');
- return $results;
- }
+ $results = Hashtag::select('slug')
+ ->where('slug', 'like', '%'.$q.'%')
+ ->whereIsNsfw(false)
+ ->whereIsBanned(false)
+ ->limit(5)
+ ->get()
+ ->map(function($tag) {
+ return [
+ 'key' => '#' . $tag->slug,
+ 'value' => $tag->slug
+ ];
+ });
- public function store(Request $request)
- {
- $this->validate($request, [
- 'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
- 'media.*' => 'required',
- 'media.*.id' => 'required|integer|min:1',
- 'media.*.filter_class' => 'nullable|alpha_dash|max:30',
- 'media.*.license' => 'nullable|string|max:140',
- 'media.*.alt' => 'nullable|string|max:'.config_cache('pixelfed.max_altext_length'),
- 'cw' => 'nullable|boolean',
- 'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10',
- 'place' => 'nullable',
- 'comments_disabled' => 'nullable',
- 'tagged' => 'nullable',
- 'license' => 'nullable|integer|min:1|max:16',
- 'collections' => 'sometimes|array|min:1|max:5',
- 'spoiler_text' => 'nullable|string|max:140',
- // 'optimize_media' => 'nullable'
- ]);
+ return $results;
+ }
- if(config('costar.enabled') == true) {
- $blockedKeywords = config('costar.keyword.block');
- if($blockedKeywords !== null && $request->caption) {
- $keywords = config('costar.keyword.block');
- foreach($keywords as $kw) {
- if(Str::contains($request->caption, $kw) == true) {
- abort(400, 'Invalid object');
- }
- }
- }
- }
+ public function store(Request $request)
+ {
+ $this->validate($request, [
+ 'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
+ 'media.*' => 'required',
+ 'media.*.id' => 'required|integer|min:1',
+ 'media.*.filter_class' => 'nullable|alpha_dash|max:30',
+ 'media.*.license' => 'nullable|string|max:140',
+ 'media.*.alt' => 'nullable|string|max:'.config_cache('pixelfed.max_altext_length'),
+ 'cw' => 'nullable|boolean',
+ 'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10',
+ 'place' => 'nullable',
+ 'comments_disabled' => 'nullable',
+ 'tagged' => 'nullable',
+ 'license' => 'nullable|integer|min:1|max:16',
+ 'collections' => 'sometimes|array|min:1|max:5',
+ 'spoiler_text' => 'nullable|string|max:140',
+ // 'optimize_media' => 'nullable'
+ ]);
- $user = Auth::user();
- $profile = $user->profile;
+ abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
- $limitKey = 'compose:rate-limit:store:' . $user->id;
- $limitTtl = now()->addMinutes(15);
- $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
- $dailyLimit = Status::whereProfileId($user->profile_id)
- ->whereNull('in_reply_to_id')
- ->whereNull('reblog_of_id')
- ->where('created_at', '>', now()->subDays(1))
- ->count();
+ if(config('costar.enabled') == true) {
+ $blockedKeywords = config('costar.keyword.block');
+ if($blockedKeywords !== null && $request->caption) {
+ $keywords = config('costar.keyword.block');
+ foreach($keywords as $kw) {
+ if(Str::contains($request->caption, $kw) == true) {
+ abort(400, 'Invalid object');
+ }
+ }
+ }
+ }
- return $dailyLimit >= 1000;
- });
+ $user = $request->user();
+ $profile = $user->profile;
- abort_if($limitReached == true, 429);
+ $limitKey = 'compose:rate-limit:store:' . $user->id;
+ $limitTtl = now()->addMinutes(15);
+ $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
+ $dailyLimit = Status::whereProfileId($user->profile_id)
+ ->whereNull('in_reply_to_id')
+ ->whereNull('reblog_of_id')
+ ->where('created_at', '>', now()->subDays(1))
+ ->count();
- $license = in_array($request->input('license'), License::keys()) ? $request->input('license') : null;
+ return $dailyLimit >= 1000;
+ });
- $visibility = $request->input('visibility');
- $medias = $request->input('media');
- $attachments = [];
- $status = new Status;
- $mimes = [];
- $place = $request->input('place');
- $cw = $request->input('cw');
- $tagged = $request->input('tagged');
- $optimize_media = (bool) $request->input('optimize_media');
+ abort_if($limitReached == true, 429);
- foreach($medias as $k => $media) {
- if($k + 1 > config_cache('pixelfed.max_album_length')) {
- continue;
- }
- $m = Media::findOrFail($media['id']);
- if($m->profile_id !== $profile->id || $m->status_id) {
- abort(403, 'Invalid media id');
- }
- $m->filter_class = in_array($media['filter_class'], Filter::classes()) ? $media['filter_class'] : null;
- $m->license = $license;
- $m->caption = isset($media['alt']) ? strip_tags($media['alt']) : null;
- $m->order = isset($media['cursor']) && is_int($media['cursor']) ? (int) $media['cursor'] : $k;
+ $license = in_array($request->input('license'), License::keys()) ? $request->input('license') : null;
- if($cw == true || $profile->cw == true) {
- $m->is_nsfw = $cw;
- $status->is_nsfw = $cw;
- }
- $m->save();
- $attachments[] = $m;
- array_push($mimes, $m->mime);
- }
+ $visibility = $request->input('visibility');
+ $medias = $request->input('media');
+ $attachments = [];
+ $status = new Status;
+ $mimes = [];
+ $place = $request->input('place');
+ $cw = $request->input('cw');
+ $tagged = $request->input('tagged');
+ $optimize_media = (bool) $request->input('optimize_media');
- abort_if(empty($attachments), 422);
+ foreach($medias as $k => $media) {
+ if($k + 1 > config_cache('pixelfed.max_album_length')) {
+ continue;
+ }
+ $m = Media::findOrFail($media['id']);
+ if($m->profile_id !== $profile->id || $m->status_id) {
+ abort(403, 'Invalid media id');
+ }
+ $m->filter_class = in_array($media['filter_class'], Filter::classes()) ? $media['filter_class'] : null;
+ $m->license = $license;
+ $m->caption = isset($media['alt']) ? strip_tags($media['alt']) : null;
+ $m->order = isset($media['cursor']) && is_int($media['cursor']) ? (int) $media['cursor'] : $k;
- $mediaType = StatusController::mimeTypeCheck($mimes);
+ if($cw == true || $profile->cw == true) {
+ $m->is_nsfw = $cw;
+ $status->is_nsfw = $cw;
+ }
+ $m->save();
+ $attachments[] = $m;
+ array_push($mimes, $m->mime);
+ }
- if(in_array($mediaType, ['photo', 'video', 'photo:album']) == false) {
- abort(400, __('exception.compose.invalid.album'));
- }
+ abort_if(empty($attachments), 422);
- if($place && is_array($place)) {
- $status->place_id = $place['id'];
- }
+ $mediaType = StatusController::mimeTypeCheck($mimes);
- if($request->filled('comments_disabled')) {
- $status->comments_disabled = (bool) $request->input('comments_disabled');
- }
+ if(in_array($mediaType, ['photo', 'video', 'photo:album']) == false) {
+ abort(400, __('exception.compose.invalid.album'));
+ }
- if($request->filled('spoiler_text') && $cw) {
- $status->cw_summary = $request->input('spoiler_text');
- }
+ if($place && is_array($place)) {
+ $status->place_id = $place['id'];
+ }
- $status->caption = strip_tags($request->caption);
- $status->rendered = Autolink::create()->autolink($status->caption);
- $status->scope = 'draft';
- $status->visibility = 'draft';
- $status->profile_id = $profile->id;
- $status->save();
+ if($request->filled('comments_disabled')) {
+ $status->comments_disabled = (bool) $request->input('comments_disabled');
+ }
- foreach($attachments as $media) {
- $media->status_id = $status->id;
- $media->save();
- }
+ if($request->filled('spoiler_text') && $cw) {
+ $status->cw_summary = $request->input('spoiler_text');
+ }
- $visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;
- $visibility = $profile->is_private ? 'private' : $visibility;
- $cw = $profile->cw == true ? true : $cw;
- $status->is_nsfw = $cw;
- $status->visibility = $visibility;
- $status->scope = $visibility;
- $status->type = $mediaType;
- $status->save();
+ $status->caption = strip_tags($request->caption);
+ $status->rendered = Autolink::create()->autolink($status->caption);
+ $status->scope = 'draft';
+ $status->visibility = 'draft';
+ $status->profile_id = $profile->id;
+ $status->save();
- foreach($tagged as $tg) {
- $mt = new MediaTag;
- $mt->status_id = $status->id;
- $mt->media_id = $status->media->first()->id;
- $mt->profile_id = $tg['id'];
- $mt->tagged_username = $tg['name'];
- $mt->is_public = true;
- $mt->metadata = json_encode([
- '_v' => 1,
- ]);
- $mt->save();
- MediaTagService::set($mt->status_id, $mt->profile_id);
- MediaTagService::sendNotification($mt);
- }
+ foreach($attachments as $media) {
+ $media->status_id = $status->id;
+ $media->save();
+ }
- if($request->filled('collections')) {
- $collections = Collection::whereProfileId($profile->id)
- ->find($request->input('collections'))
- ->each(function($collection) use($status) {
- $count = $collection->items()->count();
- CollectionItem::firstOrCreate([
- 'collection_id' => $collection->id,
- 'object_type' => 'App\Status',
- 'object_id' => $status->id
- ], [
- 'order' => $count
- ]);
+ $visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;
+ $visibility = $profile->is_private ? 'private' : $visibility;
+ $cw = $profile->cw == true ? true : $cw;
+ $status->is_nsfw = $cw;
+ $status->visibility = $visibility;
+ $status->scope = $visibility;
+ $status->type = $mediaType;
+ $status->save();
- CollectionService::addItem(
- $collection->id,
- $status->id,
- $count
- );
+ foreach($tagged as $tg) {
+ $mt = new MediaTag;
+ $mt->status_id = $status->id;
+ $mt->media_id = $status->media->first()->id;
+ $mt->profile_id = $tg['id'];
+ $mt->tagged_username = $tg['name'];
+ $mt->is_public = true;
+ $mt->metadata = json_encode([
+ '_v' => 1,
+ ]);
+ $mt->save();
+ MediaTagService::set($mt->status_id, $mt->profile_id);
+ MediaTagService::sendNotification($mt);
+ }
- $collection->updated_at = now();
+ if($request->filled('collections')) {
+ $collections = Collection::whereProfileId($profile->id)
+ ->find($request->input('collections'))
+ ->each(function($collection) use($status) {
+ $count = $collection->items()->count();
+ CollectionItem::firstOrCreate([
+ 'collection_id' => $collection->id,
+ 'object_type' => 'App\Status',
+ 'object_id' => $status->id
+ ], [
+ 'order' => $count
+ ]);
+
+ CollectionService::addItem(
+ $collection->id,
+ $status->id,
+ $count
+ );
+
+ $collection->updated_at = now();
$collection->save();
CollectionService::setCollection($collection->id, $collection);
- });
- }
+ });
+ }
- NewStatusPipeline::dispatch($status);
- Cache::forget('user:account:id:'.$profile->user_id);
- Cache::forget('_api:statuses:recent_9:'.$profile->id);
- Cache::forget('profile:status_count:'.$profile->id);
- Cache::forget('status:transformer:media:attachments:'.$status->id);
- Cache::forget($user->storageUsedKey());
- Cache::forget('profile:embed:' . $status->profile_id);
- Cache::forget($limitKey);
+ NewStatusPipeline::dispatch($status);
+ Cache::forget('user:account:id:'.$profile->user_id);
+ Cache::forget('_api:statuses:recent_9:'.$profile->id);
+ Cache::forget('profile:status_count:'.$profile->id);
+ Cache::forget('status:transformer:media:attachments:'.$status->id);
+ Cache::forget($user->storageUsedKey());
+ Cache::forget('profile:embed:' . $status->profile_id);
+ Cache::forget($limitKey);
- return $status->url();
- }
+ return $status->url();
+ }
- public function storeText(Request $request)
- {
- abort_unless(config('exp.top'), 404);
- $this->validate($request, [
- 'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
- 'cw' => 'nullable|boolean',
- 'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10',
- 'place' => 'nullable',
- 'comments_disabled' => 'nullable',
- 'tagged' => 'nullable',
- ]);
+ public function storeText(Request $request)
+ {
+ abort_unless(config('exp.top'), 404);
+ $this->validate($request, [
+ 'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
+ 'cw' => 'nullable|boolean',
+ 'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10',
+ 'place' => 'nullable',
+ 'comments_disabled' => 'nullable',
+ 'tagged' => 'nullable',
+ ]);
- if(config('costar.enabled') == true) {
- $blockedKeywords = config('costar.keyword.block');
- if($blockedKeywords !== null && $request->caption) {
- $keywords = config('costar.keyword.block');
- foreach($keywords as $kw) {
- if(Str::contains($request->caption, $kw) == true) {
- abort(400, 'Invalid object');
- }
- }
- }
- }
+ abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
- $user = Auth::user();
- $profile = $user->profile;
- $visibility = $request->input('visibility');
- $status = new Status;
- $place = $request->input('place');
- $cw = $request->input('cw');
- $tagged = $request->input('tagged');
+ if(config('costar.enabled') == true) {
+ $blockedKeywords = config('costar.keyword.block');
+ if($blockedKeywords !== null && $request->caption) {
+ $keywords = config('costar.keyword.block');
+ foreach($keywords as $kw) {
+ if(Str::contains($request->caption, $kw) == true) {
+ abort(400, 'Invalid object');
+ }
+ }
+ }
+ }
- if($place && is_array($place)) {
- $status->place_id = $place['id'];
- }
+ $user = $request->user();
+ $profile = $user->profile;
+ $visibility = $request->input('visibility');
+ $status = new Status;
+ $place = $request->input('place');
+ $cw = $request->input('cw');
+ $tagged = $request->input('tagged');
- if($request->filled('comments_disabled')) {
- $status->comments_disabled = (bool) $request->input('comments_disabled');
- }
+ if($place && is_array($place)) {
+ $status->place_id = $place['id'];
+ }
- $status->caption = strip_tags($request->caption);
- $status->profile_id = $profile->id;
- $entities = [];
- $visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;
- $cw = $profile->cw == true ? true : $cw;
- $status->is_nsfw = $cw;
- $status->visibility = $visibility;
- $status->scope = $visibility;
- $status->type = 'text';
- $status->rendered = Autolink::create()->autolink($status->caption);
- $status->entities = json_encode(array_merge([
- 'timg' => [
- 'version' => 0,
- 'bg_id' => 1,
- 'font_size' => strlen($status->caption) <= 140 ? 'h1' : 'h3',
- 'length' => strlen($status->caption),
- ]
- ], $entities), JSON_UNESCAPED_SLASHES);
- $status->save();
+ if($request->filled('comments_disabled')) {
+ $status->comments_disabled = (bool) $request->input('comments_disabled');
+ }
- foreach($tagged as $tg) {
- $mt = new MediaTag;
- $mt->status_id = $status->id;
- $mt->media_id = $status->media->first()->id;
- $mt->profile_id = $tg['id'];
- $mt->tagged_username = $tg['name'];
- $mt->is_public = true;
- $mt->metadata = json_encode([
- '_v' => 1,
- ]);
- $mt->save();
- MediaTagService::set($mt->status_id, $mt->profile_id);
- MediaTagService::sendNotification($mt);
- }
+ $status->caption = strip_tags($request->caption);
+ $status->profile_id = $profile->id;
+ $entities = [];
+ $visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;
+ $cw = $profile->cw == true ? true : $cw;
+ $status->is_nsfw = $cw;
+ $status->visibility = $visibility;
+ $status->scope = $visibility;
+ $status->type = 'text';
+ $status->rendered = Autolink::create()->autolink($status->caption);
+ $status->entities = json_encode(array_merge([
+ 'timg' => [
+ 'version' => 0,
+ 'bg_id' => 1,
+ 'font_size' => strlen($status->caption) <= 140 ? 'h1' : 'h3',
+ 'length' => strlen($status->caption),
+ ]
+ ], $entities), JSON_UNESCAPED_SLASHES);
+ $status->save();
+
+ foreach($tagged as $tg) {
+ $mt = new MediaTag;
+ $mt->status_id = $status->id;
+ $mt->media_id = $status->media->first()->id;
+ $mt->profile_id = $tg['id'];
+ $mt->tagged_username = $tg['name'];
+ $mt->is_public = true;
+ $mt->metadata = json_encode([
+ '_v' => 1,
+ ]);
+ $mt->save();
+ MediaTagService::set($mt->status_id, $mt->profile_id);
+ MediaTagService::sendNotification($mt);
+ }
- Cache::forget('user:account:id:'.$profile->user_id);
- Cache::forget('_api:statuses:recent_9:'.$profile->id);
- Cache::forget('profile:status_count:'.$profile->id);
+ Cache::forget('user:account:id:'.$profile->user_id);
+ Cache::forget('_api:statuses:recent_9:'.$profile->id);
+ Cache::forget('profile:status_count:'.$profile->id);
- return $status->url();
- }
+ return $status->url();
+ }
- public function mediaProcessingCheck(Request $request)
- {
- $this->validate($request, [
- 'id' => 'required|integer|min:1'
- ]);
+ public function mediaProcessingCheck(Request $request)
+ {
+ $this->validate($request, [
+ 'id' => 'required|integer|min:1'
+ ]);
- $media = Media::whereUserId($request->user()->id)
- ->whereNull('status_id')
- ->findOrFail($request->input('id'));
+ abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
- if(config('pixelfed.media_fast_process')) {
- return [
- 'finished' => true
- ];
- }
+ $media = Media::whereUserId($request->user()->id)
+ ->whereNull('status_id')
+ ->findOrFail($request->input('id'));
- $finished = false;
+ if(config('pixelfed.media_fast_process')) {
+ return [
+ 'finished' => true
+ ];
+ }
- switch ($media->mime) {
- case 'image/jpeg':
- case 'image/png':
- case 'video/mp4':
- $finished = config_cache('pixelfed.cloud_storage') ? (bool) $media->cdn_url : (bool) $media->processed_at;
- break;
+ $finished = false;
- default:
- # code...
- break;
- }
+ switch ($media->mime) {
+ case 'image/jpeg':
+ case 'image/png':
+ case 'video/mp4':
+ $finished = config_cache('pixelfed.cloud_storage') ? (bool) $media->cdn_url : (bool) $media->processed_at;
+ break;
- return [
- 'finished' => $finished
- ];
- }
+ default:
+ # code...
+ break;
+ }
- public function composeSettings(Request $request)
- {
- $uid = $request->user()->id;
- $default = [
- 'default_license' => 1,
- 'media_descriptions' => false,
- 'max_altext_length' => config_cache('pixelfed.max_altext_length')
- ];
- $settings = AccountService::settings($uid);
- if(isset($settings['other']) && isset($settings['other']['scope'])) {
- $s = $settings['compose_settings'];
- $s['default_scope'] = $settings['other']['scope'];
- $settings['compose_settings'] = $s;
- }
+ return [
+ 'finished' => $finished
+ ];
+ }
- return array_merge($default, $settings['compose_settings']);
- }
+ public function composeSettings(Request $request)
+ {
+ $uid = $request->user()->id;
+ abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
- public function createPoll(Request $request)
- {
- $this->validate($request, [
- 'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
- 'cw' => 'nullable|boolean',
- 'visibility' => 'required|string|in:public,private',
- 'comments_disabled' => 'nullable',
- 'expiry' => 'required|in:60,360,1440,10080',
- 'pollOptions' => 'required|array|min:1|max:4'
- ]);
+ $default = [
+ 'default_license' => 1,
+ 'media_descriptions' => false,
+ 'max_altext_length' => config_cache('pixelfed.max_altext_length')
+ ];
+ $settings = AccountService::settings($uid);
+ if(isset($settings['other']) && isset($settings['other']['scope'])) {
+ $s = $settings['compose_settings'];
+ $s['default_scope'] = $settings['other']['scope'];
+ $settings['compose_settings'] = $s;
+ }
- abort_if(config('instance.polls.enabled') == false, 404, 'Polls not enabled');
+ return array_merge($default, $settings['compose_settings']);
+ }
- abort_if(Status::whereType('poll')
- ->whereProfileId($request->user()->profile_id)
- ->whereCaption($request->input('caption'))
- ->where('created_at', '>', now()->subDays(2))
- ->exists()
- , 422, 'Duplicate detected.');
+ public function createPoll(Request $request)
+ {
+ $this->validate($request, [
+ 'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
+ 'cw' => 'nullable|boolean',
+ 'visibility' => 'required|string|in:public,private',
+ 'comments_disabled' => 'nullable',
+ 'expiry' => 'required|in:60,360,1440,10080',
+ 'pollOptions' => 'required|array|min:1|max:4'
+ ]);
+ abort(404);
+ abort_if(config('instance.polls.enabled') == false, 404, 'Polls not enabled');
+ abort_if($request->user()->has_roles && !UserRoleService::can('can-post', $request->user()->id), 403, 'Invalid permissions for this action');
- $status = new Status;
- $status->profile_id = $request->user()->profile_id;
- $status->caption = $request->input('caption');
- $status->rendered = Autolink::create()->autolink($status->caption);
- $status->visibility = 'draft';
- $status->scope = 'draft';
- $status->type = 'poll';
- $status->local = true;
- $status->save();
+ abort_if(Status::whereType('poll')
+ ->whereProfileId($request->user()->profile_id)
+ ->whereCaption($request->input('caption'))
+ ->where('created_at', '>', now()->subDays(2))
+ ->exists()
+ , 422, 'Duplicate detected.');
- $poll = new Poll;
- $poll->status_id = $status->id;
- $poll->profile_id = $status->profile_id;
- $poll->poll_options = $request->input('pollOptions');
- $poll->expires_at = now()->addMinutes($request->input('expiry'));
- $poll->cached_tallies = collect($poll->poll_options)->map(function($o) {
- return 0;
- })->toArray();
- $poll->save();
+ $status = new Status;
+ $status->profile_id = $request->user()->profile_id;
+ $status->caption = $request->input('caption');
+ $status->rendered = Autolink::create()->autolink($status->caption);
+ $status->visibility = 'draft';
+ $status->scope = 'draft';
+ $status->type = 'poll';
+ $status->local = true;
+ $status->save();
- $status->visibility = $request->input('visibility');
- $status->scope = $request->input('visibility');
- $status->save();
+ $poll = new Poll;
+ $poll->status_id = $status->id;
+ $poll->profile_id = $status->profile_id;
+ $poll->poll_options = $request->input('pollOptions');
+ $poll->expires_at = now()->addMinutes($request->input('expiry'));
+ $poll->cached_tallies = collect($poll->poll_options)->map(function($o) {
+ return 0;
+ })->toArray();
+ $poll->save();
- NewStatusPipeline::dispatch($status);
+ $status->visibility = $request->input('visibility');
+ $status->scope = $request->input('visibility');
+ $status->save();
- return ['url' => $status->url()];
- }
+ NewStatusPipeline::dispatch($status);
+
+ return ['url' => $status->url()];
+ }
}
From 9d365d07f9223cacfa22932982e58a817f78f921 Mon Sep 17 00:00:00 2001
From: Daniel Supernault
Date: Thu, 11 Jan 2024 04:41:38 -0700
Subject: [PATCH 09/16] Update ParentalControls, map updated saved
permissions/roles
---
.../ParentalControlsController.php | 6 ++-
app/Services/UserRoleService.php | 41 ++++++++++++++++++-
2 files changed, 45 insertions(+), 2 deletions(-)
diff --git a/app/Http/Controllers/ParentalControlsController.php b/app/Http/Controllers/ParentalControlsController.php
index 1dc2f578f..373021ab9 100644
--- a/app/Http/Controllers/ParentalControlsController.php
+++ b/app/Http/Controllers/ParentalControlsController.php
@@ -59,9 +59,13 @@ class ParentalControlsController extends Controller
{
$this->authPreflight($request);
$uid = $request->user()->id;
+ $ff = $this->requestFormFields($request);
$pc = ParentalControls::whereParentId($uid)->findOrFail($id);
- $pc->permissions = $this->requestFormFields($request);
+ $pc->permissions = $ff;
$pc->save();
+
+ $roles = UserRoleService::mapActions($pc->child_id, $ff);
+ UserRoles::whereUserId($pc->child_id)->update(['roles' => $roles]);
return redirect($pc->manageUrl() . '?permissions');
}
diff --git a/app/Services/UserRoleService.php b/app/Services/UserRoleService.php
index a18810bf0..ed765a930 100644
--- a/app/Services/UserRoleService.php
+++ b/app/Services/UserRoleService.php
@@ -179,7 +179,7 @@ class UserRoleService
];
foreach ($map as $key => $value) {
- if(!isset($data[$value], $data[substr($value, 1)])) {
+ if(!isset($data[$value]) && !isset($data[substr($value, 1)])) {
$map[$key] = false;
continue;
}
@@ -188,4 +188,43 @@ class UserRoleService
return $map;
}
+
+ public static function mapActions($id, $data = [])
+ {
+ $res = [];
+ $map = [
+ 'account-force-private' => 'private',
+ 'account-ignore-follow-requests' => 'private',
+
+ 'can-view-public-feed' => 'discovery_feeds',
+ 'can-view-network-feed' => 'discovery_feeds',
+ 'can-view-discover' => 'discovery_feeds',
+ 'can-view-hashtag-feed' => 'discovery_feeds',
+
+ 'can-post' => 'post',
+ 'can-comment' => 'comment',
+ 'can-like' => 'like',
+ 'can-share' => 'share',
+
+ 'can-follow' => 'follow',
+ 'can-make-public' => '!private',
+
+ 'can-direct-message' => 'dms',
+ 'can-use-stories' => 'story',
+ 'can-view-sensitive' => '!hide_cw',
+ 'can-bookmark' => 'bookmark',
+ 'can-collections' => 'collection',
+ 'can-federation' => 'federation',
+ ];
+
+ foreach ($map as $key => $value) {
+ if(!isset($data[$value]) && !isset($data[substr($value, 1)])) {
+ $res[$key] = false;
+ continue;
+ }
+ $res[$key] = str_starts_with($value, '!') ? !$data[substr($value, 1)] : $data[$value];
+ }
+
+ return $res;
+ }
}
From fd9b5ad443dbad8056ed2f99297d2618e32c6fbe Mon Sep 17 00:00:00 2001
From: Daniel Supernault
Date: Thu, 11 Jan 2024 04:50:11 -0700
Subject: [PATCH 10/16] Update api controllers, add parental control support
---
app/Http/Controllers/Api/ApiV1Controller.php | 5 +
app/Http/Controllers/Api/ApiV2Controller.php | 515 ++++++++++---------
2 files changed, 267 insertions(+), 253 deletions(-)
diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php
index dde416064..fe1196916 100644
--- a/app/Http/Controllers/Api/ApiV1Controller.php
+++ b/app/Http/Controllers/Api/ApiV1Controller.php
@@ -758,6 +758,8 @@ class ApiV1Controller extends Controller
abort_if(!$request->user(), 403);
$user = $request->user();
+ abort_if($user->has_roles && !UserRoleService::can('can-follow', $user->id), 403, 'Invalid permissions for this action');
+
AccountService::setLastActive($user->id);
$target = Profile::where('id', '!=', $user->profile_id)
@@ -843,6 +845,7 @@ class ApiV1Controller extends Controller
abort_if(!$request->user(), 403);
$user = $request->user();
+
AccountService::setLastActive($user->id);
$target = Profile::where('id', '!=', $user->profile_id)
@@ -947,6 +950,8 @@ class ApiV1Controller extends Controller
]);
$user = $request->user();
+ abort_if($user->has_roles && !UserRoleService::can('can-view-discover', $user->id), 403, 'Invalid permissions for this action');
+
AccountService::setLastActive($user->id);
$query = $request->input('q');
$limit = $request->input('limit') ?? 20;
diff --git a/app/Http/Controllers/Api/ApiV2Controller.php b/app/Http/Controllers/Api/ApiV2Controller.php
index 2ca5b96c5..93f930cd5 100644
--- a/app/Http/Controllers/Api/ApiV2Controller.php
+++ b/app/Http/Controllers/Api/ApiV2Controller.php
@@ -17,304 +17,313 @@ use App\Services\SearchApiV2Service;
use App\Util\Media\Filter;
use App\Jobs\MediaPipeline\MediaDeletePipeline;
use App\Jobs\VideoPipeline\{
- VideoOptimize,
- VideoPostProcess,
- VideoThumbnail
+ VideoOptimize,
+ VideoPostProcess,
+ VideoThumbnail
};
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Transformer\Api\Mastodon\v1\{
- AccountTransformer,
- MediaTransformer,
- NotificationTransformer,
- StatusTransformer,
+ AccountTransformer,
+ MediaTransformer,
+ NotificationTransformer,
+ StatusTransformer,
};
use App\Transformer\Api\{
- RelationshipTransformer,
+ RelationshipTransformer,
};
use App\Util\Site\Nodeinfo;
+use App\Services\UserRoleService;
class ApiV2Controller extends Controller
{
- const PF_API_ENTITY_KEY = "_pe";
+ const PF_API_ENTITY_KEY = "_pe";
- public function json($res, $code = 200, $headers = [])
- {
- return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
- }
+ public function json($res, $code = 200, $headers = [])
+ {
+ return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
+ }
public function instance(Request $request)
{
- $contact = Cache::remember('api:v1:instance-data:contact', 604800, function () {
- if(config_cache('instance.admin.pid')) {
- return AccountService::getMastodon(config_cache('instance.admin.pid'), true);
- }
- $admin = User::whereIsAdmin(true)->first();
- return $admin && isset($admin->profile_id) ?
- AccountService::getMastodon($admin->profile_id, true) :
- null;
- });
+ $contact = Cache::remember('api:v1:instance-data:contact', 604800, function () {
+ if(config_cache('instance.admin.pid')) {
+ return AccountService::getMastodon(config_cache('instance.admin.pid'), true);
+ }
+ $admin = User::whereIsAdmin(true)->first();
+ return $admin && isset($admin->profile_id) ?
+ AccountService::getMastodon($admin->profile_id, true) :
+ null;
+ });
- $rules = Cache::remember('api:v1:instance-data:rules', 604800, function () {
- return config_cache('app.rules') ?
- collect(json_decode(config_cache('app.rules'), true))
- ->map(function($rule, $key) {
- $id = $key + 1;
- return [
- 'id' => "{$id}",
- 'text' => $rule
- ];
- })
- ->toArray() : [];
- });
+ $rules = Cache::remember('api:v1:instance-data:rules', 604800, function () {
+ return config_cache('app.rules') ?
+ collect(json_decode(config_cache('app.rules'), true))
+ ->map(function($rule, $key) {
+ $id = $key + 1;
+ return [
+ 'id' => "{$id}",
+ 'text' => $rule
+ ];
+ })
+ ->toArray() : [];
+ });
- $res = [
- 'domain' => config('pixelfed.domain.app'),
- 'title' => config_cache('app.name'),
- 'version' => config('pixelfed.version'),
- 'source_url' => 'https://github.com/pixelfed/pixelfed',
- 'description' => config_cache('app.short_description'),
- 'usage' => [
- 'users' => [
- 'active_month' => (int) Nodeinfo::activeUsersMonthly()
- ]
- ],
- 'thumbnail' => [
- 'url' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
- 'blurhash' => InstanceService::headerBlurhash(),
- 'versions' => [
- '@1x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
- '@2x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg'))
- ]
- ],
- 'languages' => [config('app.locale')],
- 'configuration' => [
- 'urls' => [
- 'streaming' => 'wss://' . config('pixelfed.domain.app'),
- 'status' => null
- ],
- 'accounts' => [
- 'max_featured_tags' => 0,
- ],
- 'statuses' => [
- 'max_characters' => (int) config('pixelfed.max_caption_length'),
- 'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'),
- 'characters_reserved_per_url' => 23
- ],
- 'media_attachments' => [
- 'supported_mime_types' => explode(',', config_cache('pixelfed.media_types')),
- 'image_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
- 'image_matrix_limit' => 3686400,
- 'video_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
- 'video_frame_rate_limit' => 240,
- 'video_matrix_limit' => 3686400
- ],
- 'polls' => [
- 'max_options' => 4,
- 'max_characters_per_option' => 50,
- 'min_expiration' => 300,
- 'max_expiration' => 2629746,
- ],
- 'translation' => [
- 'enabled' => false,
- ],
- ],
- 'registrations' => [
- 'enabled' => (bool) config_cache('pixelfed.open_registration'),
- 'approval_required' => false,
- 'message' => null
- ],
- 'contact' => [
- 'email' => config('instance.email'),
- 'account' => $contact
- ],
- 'rules' => $rules
- ];
+ $res = [
+ 'domain' => config('pixelfed.domain.app'),
+ 'title' => config_cache('app.name'),
+ 'version' => config('pixelfed.version'),
+ 'source_url' => 'https://github.com/pixelfed/pixelfed',
+ 'description' => config_cache('app.short_description'),
+ 'usage' => [
+ 'users' => [
+ 'active_month' => (int) Nodeinfo::activeUsersMonthly()
+ ]
+ ],
+ 'thumbnail' => [
+ 'url' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
+ 'blurhash' => InstanceService::headerBlurhash(),
+ 'versions' => [
+ '@1x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
+ '@2x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg'))
+ ]
+ ],
+ 'languages' => [config('app.locale')],
+ 'configuration' => [
+ 'urls' => [
+ 'streaming' => 'wss://' . config('pixelfed.domain.app'),
+ 'status' => null
+ ],
+ 'accounts' => [
+ 'max_featured_tags' => 0,
+ ],
+ 'statuses' => [
+ 'max_characters' => (int) config('pixelfed.max_caption_length'),
+ 'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'),
+ 'characters_reserved_per_url' => 23
+ ],
+ 'media_attachments' => [
+ 'supported_mime_types' => explode(',', config_cache('pixelfed.media_types')),
+ 'image_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
+ 'image_matrix_limit' => 3686400,
+ 'video_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
+ 'video_frame_rate_limit' => 240,
+ 'video_matrix_limit' => 3686400
+ ],
+ 'polls' => [
+ 'max_options' => 4,
+ 'max_characters_per_option' => 50,
+ 'min_expiration' => 300,
+ 'max_expiration' => 2629746,
+ ],
+ 'translation' => [
+ 'enabled' => false,
+ ],
+ ],
+ 'registrations' => [
+ 'enabled' => (bool) config_cache('pixelfed.open_registration'),
+ 'approval_required' => false,
+ 'message' => null
+ ],
+ 'contact' => [
+ 'email' => config('instance.email'),
+ 'account' => $contact
+ ],
+ 'rules' => $rules
+ ];
- return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
+ return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
}
- /**
- * GET /api/v2/search
- *
- *
- * @return array
- */
- public function search(Request $request)
- {
- abort_if(!$request->user(), 403);
+ /**
+ * GET /api/v2/search
+ *
+ *
+ * @return array
+ */
+ public function search(Request $request)
+ {
+ abort_if(!$request->user(), 403);
- $this->validate($request, [
- 'q' => 'required|string|min:1|max:100',
- 'account_id' => 'nullable|string',
- 'max_id' => 'nullable|string',
- 'min_id' => 'nullable|string',
- 'type' => 'nullable|in:accounts,hashtags,statuses',
- 'exclude_unreviewed' => 'nullable',
- 'resolve' => 'nullable',
- 'limit' => 'nullable|integer|max:40',
- 'offset' => 'nullable|integer',
- 'following' => 'nullable'
- ]);
+ $this->validate($request, [
+ 'q' => 'required|string|min:1|max:100',
+ 'account_id' => 'nullable|string',
+ 'max_id' => 'nullable|string',
+ 'min_id' => 'nullable|string',
+ 'type' => 'nullable|in:accounts,hashtags,statuses',
+ 'exclude_unreviewed' => 'nullable',
+ 'resolve' => 'nullable',
+ 'limit' => 'nullable|integer|max:40',
+ 'offset' => 'nullable|integer',
+ 'following' => 'nullable'
+ ]);
- $mastodonMode = !$request->has('_pe');
- return $this->json(SearchApiV2Service::query($request, $mastodonMode));
- }
+ if($request->user()->has_roles && !UserRoleService::can('can-view-discover', $request->user()->id)) {
+ return [
+ 'accounts' => [],
+ 'hashtags' => [],
+ 'statuses' => []
+ ];
+ }
- /**
- * GET /api/v2/streaming/config
- *
- *
- * @return object
- */
- public function getWebsocketConfig()
- {
- return config('broadcasting.default') === 'pusher' ? [
- 'host' => config('broadcasting.connections.pusher.options.host'),
- 'port' => config('broadcasting.connections.pusher.options.port'),
- 'key' => config('broadcasting.connections.pusher.key'),
- 'cluster' => config('broadcasting.connections.pusher.options.cluster')
- ] : [];
- }
+ $mastodonMode = !$request->has('_pe');
+ return $this->json(SearchApiV2Service::query($request, $mastodonMode));
+ }
- /**
- * POST /api/v2/media
- *
- *
- * @return MediaTransformer
- */
- public function mediaUploadV2(Request $request)
- {
- abort_if(!$request->user(), 403);
+ /**
+ * GET /api/v2/streaming/config
+ *
+ *
+ * @return object
+ */
+ public function getWebsocketConfig()
+ {
+ return config('broadcasting.default') === 'pusher' ? [
+ 'host' => config('broadcasting.connections.pusher.options.host'),
+ 'port' => config('broadcasting.connections.pusher.options.port'),
+ 'key' => config('broadcasting.connections.pusher.key'),
+ 'cluster' => config('broadcasting.connections.pusher.options.cluster')
+ ] : [];
+ }
- $this->validate($request, [
- 'file.*' => [
- 'required_without:file',
- 'mimetypes:' . config_cache('pixelfed.media_types'),
- 'max:' . config_cache('pixelfed.max_photo_size'),
- ],
- 'file' => [
- 'required_without:file.*',
- 'mimetypes:' . config_cache('pixelfed.media_types'),
- 'max:' . config_cache('pixelfed.max_photo_size'),
- ],
- 'filter_name' => 'nullable|string|max:24',
- 'filter_class' => 'nullable|alpha_dash|max:24',
- 'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length'),
- 'replace_id' => 'sometimes'
- ]);
+ /**
+ * POST /api/v2/media
+ *
+ *
+ * @return MediaTransformer
+ */
+ public function mediaUploadV2(Request $request)
+ {
+ abort_if(!$request->user(), 403);
- $user = $request->user();
+ $this->validate($request, [
+ 'file.*' => [
+ 'required_without:file',
+ 'mimetypes:' . config_cache('pixelfed.media_types'),
+ 'max:' . config_cache('pixelfed.max_photo_size'),
+ ],
+ 'file' => [
+ 'required_without:file.*',
+ 'mimetypes:' . config_cache('pixelfed.media_types'),
+ 'max:' . config_cache('pixelfed.max_photo_size'),
+ ],
+ 'filter_name' => 'nullable|string|max:24',
+ 'filter_class' => 'nullable|alpha_dash|max:24',
+ 'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length'),
+ 'replace_id' => 'sometimes'
+ ]);
- if($user->last_active_at == null) {
- return [];
- }
+ $user = $request->user();
- if(empty($request->file('file'))) {
- return response('', 422);
- }
+ if($user->last_active_at == null) {
+ return [];
+ }
- $limitKey = 'compose:rate-limit:media-upload:' . $user->id;
- $limitTtl = now()->addMinutes(15);
- $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
- $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
+ if(empty($request->file('file'))) {
+ return response('', 422);
+ }
- return $dailyLimit >= 1250;
- });
- abort_if($limitReached == true, 429);
+ $limitKey = 'compose:rate-limit:media-upload:' . $user->id;
+ $limitTtl = now()->addMinutes(15);
+ $limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
+ $dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
- $profile = $user->profile;
+ return $dailyLimit >= 1250;
+ });
+ abort_if($limitReached == true, 429);
- if(config_cache('pixelfed.enforce_account_limit') == true) {
- $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
- return Media::whereUserId($user->id)->sum('size') / 1000;
- });
- $limit = (int) config_cache('pixelfed.max_account_size');
- if ($size >= $limit) {
- abort(403, 'Account size limit reached.');
- }
- }
+ $profile = $user->profile;
- $filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
- $filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
+ if(config_cache('pixelfed.enforce_account_limit') == true) {
+ $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
+ return Media::whereUserId($user->id)->sum('size') / 1000;
+ });
+ $limit = (int) config_cache('pixelfed.max_account_size');
+ if ($size >= $limit) {
+ abort(403, 'Account size limit reached.');
+ }
+ }
- $photo = $request->file('file');
+ $filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
+ $filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
- $mimes = explode(',', config_cache('pixelfed.media_types'));
- if(in_array($photo->getMimeType(), $mimes) == false) {
- abort(403, 'Invalid or unsupported mime type.');
- }
+ $photo = $request->file('file');
- $storagePath = MediaPathService::get($user, 2);
- $path = $photo->storePublicly($storagePath);
- $hash = \hash_file('sha256', $photo);
- $license = null;
- $mime = $photo->getMimeType();
+ $mimes = explode(',', config_cache('pixelfed.media_types'));
+ if(in_array($photo->getMimeType(), $mimes) == false) {
+ abort(403, 'Invalid or unsupported mime type.');
+ }
- $settings = UserSetting::whereUserId($user->id)->first();
+ $storagePath = MediaPathService::get($user, 2);
+ $path = $photo->storePublicly($storagePath);
+ $hash = \hash_file('sha256', $photo);
+ $license = null;
+ $mime = $photo->getMimeType();
- if($settings && !empty($settings->compose_settings)) {
- $compose = $settings->compose_settings;
+ $settings = UserSetting::whereUserId($user->id)->first();
- if(isset($compose['default_license']) && $compose['default_license'] != 1) {
- $license = $compose['default_license'];
- }
- }
+ if($settings && !empty($settings->compose_settings)) {
+ $compose = $settings->compose_settings;
- abort_if(MediaBlocklistService::exists($hash) == true, 451);
+ if(isset($compose['default_license']) && $compose['default_license'] != 1) {
+ $license = $compose['default_license'];
+ }
+ }
- if($request->has('replace_id')) {
- $rpid = $request->input('replace_id');
- $removeMedia = Media::whereNull('status_id')
- ->whereUserId($user->id)
- ->whereProfileId($profile->id)
- ->where('created_at', '>', now()->subHours(2))
- ->find($rpid);
- if($removeMedia) {
- MediaDeletePipeline::dispatch($removeMedia)
- ->onQueue('mmo')
- ->delay(now()->addMinutes(15));
- }
- }
+ abort_if(MediaBlocklistService::exists($hash) == true, 451);
- $media = new Media();
- $media->status_id = null;
- $media->profile_id = $profile->id;
- $media->user_id = $user->id;
- $media->media_path = $path;
- $media->original_sha256 = $hash;
- $media->size = $photo->getSize();
- $media->mime = $mime;
- $media->caption = $request->input('description');
- $media->filter_class = $filterClass;
- $media->filter_name = $filterName;
- if($license) {
- $media->license = $license;
- }
- $media->save();
+ if($request->has('replace_id')) {
+ $rpid = $request->input('replace_id');
+ $removeMedia = Media::whereNull('status_id')
+ ->whereUserId($user->id)
+ ->whereProfileId($profile->id)
+ ->where('created_at', '>', now()->subHours(2))
+ ->find($rpid);
+ if($removeMedia) {
+ MediaDeletePipeline::dispatch($removeMedia)
+ ->onQueue('mmo')
+ ->delay(now()->addMinutes(15));
+ }
+ }
- switch ($media->mime) {
- case 'image/jpeg':
- case 'image/png':
- ImageOptimize::dispatch($media)->onQueue('mmo');
- break;
+ $media = new Media();
+ $media->status_id = null;
+ $media->profile_id = $profile->id;
+ $media->user_id = $user->id;
+ $media->media_path = $path;
+ $media->original_sha256 = $hash;
+ $media->size = $photo->getSize();
+ $media->mime = $mime;
+ $media->caption = $request->input('description');
+ $media->filter_class = $filterClass;
+ $media->filter_name = $filterName;
+ if($license) {
+ $media->license = $license;
+ }
+ $media->save();
- case 'video/mp4':
- VideoThumbnail::dispatch($media)->onQueue('mmo');
- $preview_url = '/storage/no-preview.png';
- $url = '/storage/no-preview.png';
- break;
- }
+ switch ($media->mime) {
+ case 'image/jpeg':
+ case 'image/png':
+ ImageOptimize::dispatch($media)->onQueue('mmo');
+ break;
- Cache::forget($limitKey);
- $fractal = new Fractal\Manager();
- $fractal->setSerializer(new ArraySerializer());
- $resource = new Fractal\Resource\Item($media, new MediaTransformer());
- $res = $fractal->createData($resource)->toArray();
- $res['preview_url'] = $media->url(). '?v=' . time();
- $res['url'] = null;
- return $this->json($res, 202);
- }
+ case 'video/mp4':
+ VideoThumbnail::dispatch($media)->onQueue('mmo');
+ $preview_url = '/storage/no-preview.png';
+ $url = '/storage/no-preview.png';
+ break;
+ }
+
+ Cache::forget($limitKey);
+ $fractal = new Fractal\Manager();
+ $fractal->setSerializer(new ArraySerializer());
+ $resource = new Fractal\Resource\Item($media, new MediaTransformer());
+ $res = $fractal->createData($resource)->toArray();
+ $res['preview_url'] = $media->url(). '?v=' . time();
+ $res['url'] = null;
+ return $this->json($res, 202);
+ }
}
From fe30cd25d1038341666279df075d4bb7194496b0 Mon Sep 17 00:00:00 2001
From: Daniel Supernault
Date: Thu, 11 Jan 2024 05:25:23 -0700
Subject: [PATCH 11/16] Update DirectMessageController, add parental controls
support
---
app/Http/Controllers/Api/ApiV1Controller.php | 6 +-
.../Controllers/DirectMessageController.php | 1698 +++++++++--------
2 files changed, 867 insertions(+), 837 deletions(-)
diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php
index fe1196916..b342d68ce 100644
--- a/app/Http/Controllers/Api/ApiV1Controller.php
+++ b/app/Http/Controllers/Api/ApiV1Controller.php
@@ -2575,7 +2575,11 @@ class ApiV1Controller extends Controller
$limit = $request->input('limit', 20);
$scope = $request->input('scope', 'inbox');
- $pid = $request->user()->profile_id;
+ $user = $request->user();
+ if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id)) {
+ return [];
+ }
+ $pid = $user->profile_id;
if(config('database.default') == 'pgsql') {
$dms = DirectMessage::when($scope === 'inbox', function($q, $scope) use($pid) {
diff --git a/app/Http/Controllers/DirectMessageController.php b/app/Http/Controllers/DirectMessageController.php
index df76d2ab9..0d91d4f17 100644
--- a/app/Http/Controllers/DirectMessageController.php
+++ b/app/Http/Controllers/DirectMessageController.php
@@ -5,14 +5,14 @@ namespace App\Http\Controllers;
use Auth, Cache;
use Illuminate\Http\Request;
use App\{
- DirectMessage,
- Media,
- Notification,
- Profile,
- Status,
- User,
- UserFilter,
- UserSetting
+ DirectMessage,
+ Media,
+ Notification,
+ Profile,
+ Status,
+ User,
+ UserFilter,
+ UserSetting
};
use App\Services\MediaPathService;
use App\Services\MediaBlocklistService;
@@ -26,835 +26,861 @@ use App\Services\WebfingerService;
use App\Models\Conversation;
use App\Jobs\DirectPipeline\DirectDeletePipeline;
use App\Jobs\DirectPipeline\DirectDeliverPipeline;
+use App\Services\UserRoleService;
class DirectMessageController extends Controller
{
- public function __construct()
- {
- $this->middleware('auth');
- }
-
- public function browse(Request $request)
- {
- $this->validate($request, [
- 'a' => 'nullable|string|in:inbox,sent,filtered',
- 'page' => 'nullable|integer|min:1|max:99'
- ]);
-
- $profile = $request->user()->profile_id;
- $action = $request->input('a', 'inbox');
- $page = $request->input('page');
-
- if(config('database.default') == 'pgsql') {
- if($action == 'inbox') {
- $dms = DirectMessage::select('id', 'type', 'to_id', 'from_id', 'id', 'status_id', 'is_hidden', 'meta', 'created_at', 'read_at')
- ->whereToId($profile)
- ->with(['author','status'])
- ->whereIsHidden(false)
- ->when($page, function($q, $page) {
- if($page > 1) {
- return $q->offset($page * 8 - 8);
- }
- })
- ->latest()
- ->get()
- ->unique('from_id')
- ->take(8)
- ->map(function($r) use($profile) {
- return $r->from_id !== $profile ? [
- 'id' => (string) $r->from_id,
- 'name' => $r->author->name,
- 'username' => $r->author->username,
- 'avatar' => $r->author->avatarUrl(),
- 'url' => $r->author->url(),
- 'isLocal' => (bool) !$r->author->domain,
- 'domain' => $r->author->domain,
- 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
- 'lastMessage' => $r->status->caption,
- 'messages' => []
- ] : [
- 'id' => (string) $r->to_id,
- 'name' => $r->recipient->name,
- 'username' => $r->recipient->username,
- 'avatar' => $r->recipient->avatarUrl(),
- 'url' => $r->recipient->url(),
- 'isLocal' => (bool) !$r->recipient->domain,
- 'domain' => $r->recipient->domain,
- 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
- 'lastMessage' => $r->status->caption,
- 'messages' => []
- ];
- })->values();
- }
-
- if($action == 'sent') {
- $dms = DirectMessage::select('id', 'type', 'to_id', 'from_id', 'id', 'status_id', 'is_hidden', 'meta', 'created_at', 'read_at')
- ->whereFromId($profile)
- ->with(['author','status'])
- ->orderBy('id', 'desc')
- ->when($page, function($q, $page) {
- if($page > 1) {
- return $q->offset($page * 8 - 8);
- }
- })
- ->get()
- ->unique('to_id')
- ->take(8)
- ->map(function($r) use($profile) {
- return $r->from_id !== $profile ? [
- 'id' => (string) $r->from_id,
- 'name' => $r->author->name,
- 'username' => $r->author->username,
- 'avatar' => $r->author->avatarUrl(),
- 'url' => $r->author->url(),
- 'isLocal' => (bool) !$r->author->domain,
- 'domain' => $r->author->domain,
- 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
- 'lastMessage' => $r->status->caption,
- 'messages' => []
- ] : [
- 'id' => (string) $r->to_id,
- 'name' => $r->recipient->name,
- 'username' => $r->recipient->username,
- 'avatar' => $r->recipient->avatarUrl(),
- 'url' => $r->recipient->url(),
- 'isLocal' => (bool) !$r->recipient->domain,
- 'domain' => $r->recipient->domain,
- 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
- 'lastMessage' => $r->status->caption,
- 'messages' => []
- ];
- });
- }
-
- if($action == 'filtered') {
- $dms = DirectMessage::select('id', 'type', 'to_id', 'from_id', 'id', 'status_id', 'is_hidden', 'meta', 'created_at', 'read_at')
- ->whereToId($profile)
- ->with(['author','status'])
- ->whereIsHidden(true)
- ->orderBy('id', 'desc')
- ->when($page, function($q, $page) {
- if($page > 1) {
- return $q->offset($page * 8 - 8);
- }
- })
- ->get()
- ->unique('from_id')
- ->take(8)
- ->map(function($r) use($profile) {
- return $r->from_id !== $profile ? [
- 'id' => (string) $r->from_id,
- 'name' => $r->author->name,
- 'username' => $r->author->username,
- 'avatar' => $r->author->avatarUrl(),
- 'url' => $r->author->url(),
- 'isLocal' => (bool) !$r->author->domain,
- 'domain' => $r->author->domain,
- 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
- 'lastMessage' => $r->status->caption,
- 'messages' => []
- ] : [
- 'id' => (string) $r->to_id,
- 'name' => $r->recipient->name,
- 'username' => $r->recipient->username,
- 'avatar' => $r->recipient->avatarUrl(),
- 'url' => $r->recipient->url(),
- 'isLocal' => (bool) !$r->recipient->domain,
- 'domain' => $r->recipient->domain,
- 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
- 'lastMessage' => $r->status->caption,
- 'messages' => []
- ];
- });
- }
- } elseif(config('database.default') == 'mysql') {
- if($action == 'inbox') {
- $dms = DirectMessage::selectRaw('*, max(created_at) as createdAt')
- ->whereToId($profile)
- ->with(['author','status'])
- ->whereIsHidden(false)
- ->groupBy('from_id')
- ->latest()
- ->when($page, function($q, $page) {
- if($page > 1) {
- return $q->offset($page * 8 - 8);
- }
- })
- ->limit(8)
- ->get()
- ->map(function($r) use($profile) {
- return $r->from_id !== $profile ? [
- 'id' => (string) $r->from_id,
- 'name' => $r->author->name,
- 'username' => $r->author->username,
- 'avatar' => $r->author->avatarUrl(),
- 'url' => $r->author->url(),
- 'isLocal' => (bool) !$r->author->domain,
- 'domain' => $r->author->domain,
- 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
- 'lastMessage' => $r->status->caption,
- 'messages' => []
- ] : [
- 'id' => (string) $r->to_id,
- 'name' => $r->recipient->name,
- 'username' => $r->recipient->username,
- 'avatar' => $r->recipient->avatarUrl(),
- 'url' => $r->recipient->url(),
- 'isLocal' => (bool) !$r->recipient->domain,
- 'domain' => $r->recipient->domain,
- 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
- 'lastMessage' => $r->status->caption,
- 'messages' => []
- ];
- });
- }
-
- if($action == 'sent') {
- $dms = DirectMessage::selectRaw('*, max(created_at) as createdAt')
- ->whereFromId($profile)
- ->with(['author','status'])
- ->groupBy('to_id')
- ->orderBy('createdAt', 'desc')
- ->when($page, function($q, $page) {
- if($page > 1) {
- return $q->offset($page * 8 - 8);
- }
- })
- ->limit(8)
- ->get()
- ->map(function($r) use($profile) {
- return $r->from_id !== $profile ? [
- 'id' => (string) $r->from_id,
- 'name' => $r->author->name,
- 'username' => $r->author->username,
- 'avatar' => $r->author->avatarUrl(),
- 'url' => $r->author->url(),
- 'isLocal' => (bool) !$r->author->domain,
- 'domain' => $r->author->domain,
- 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
- 'lastMessage' => $r->status->caption,
- 'messages' => []
- ] : [
- 'id' => (string) $r->to_id,
- 'name' => $r->recipient->name,
- 'username' => $r->recipient->username,
- 'avatar' => $r->recipient->avatarUrl(),
- 'url' => $r->recipient->url(),
- 'isLocal' => (bool) !$r->recipient->domain,
- 'domain' => $r->recipient->domain,
- 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
- 'lastMessage' => $r->status->caption,
- 'messages' => []
- ];
- });
- }
-
- if($action == 'filtered') {
- $dms = DirectMessage::selectRaw('*, max(created_at) as createdAt')
- ->whereToId($profile)
- ->with(['author','status'])
- ->whereIsHidden(true)
- ->groupBy('from_id')
- ->orderBy('createdAt', 'desc')
- ->when($page, function($q, $page) {
- if($page > 1) {
- return $q->offset($page * 8 - 8);
- }
- })
- ->limit(8)
- ->get()
- ->map(function($r) use($profile) {
- return $r->from_id !== $profile ? [
- 'id' => (string) $r->from_id,
- 'name' => $r->author->name,
- 'username' => $r->author->username,
- 'avatar' => $r->author->avatarUrl(),
- 'url' => $r->author->url(),
- 'isLocal' => (bool) !$r->author->domain,
- 'domain' => $r->author->domain,
- 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
- 'lastMessage' => $r->status->caption,
- 'messages' => []
- ] : [
- 'id' => (string) $r->to_id,
- 'name' => $r->recipient->name,
- 'username' => $r->recipient->username,
- 'avatar' => $r->recipient->avatarUrl(),
- 'url' => $r->recipient->url(),
- 'isLocal' => (bool) !$r->recipient->domain,
- 'domain' => $r->recipient->domain,
- 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
- 'lastMessage' => $r->status->caption,
- 'messages' => []
- ];
- });
- }
- }
-
- return response()->json($dms->all());
- }
-
- public function create(Request $request)
- {
- $this->validate($request, [
- 'to_id' => 'required',
- 'message' => 'required|string|min:1|max:500',
- 'type' => 'required|in:text,emoji'
- ]);
-
- $profile = $request->user()->profile;
- $recipient = Profile::where('id', '!=', $profile->id)->findOrFail($request->input('to_id'));
-
- abort_if(in_array($profile->id, $recipient->blockedIds()->toArray()), 403);
- $msg = $request->input('message');
-
- if((!$recipient->domain && $recipient->user->settings->public_dm == false) || $recipient->is_private) {
- if($recipient->follows($profile) == true) {
- $hidden = false;
- } else {
- $hidden = true;
- }
- } else {
- $hidden = false;
- }
-
- $status = new Status;
- $status->profile_id = $profile->id;
- $status->caption = $msg;
- $status->rendered = $msg;
- $status->visibility = 'direct';
- $status->scope = 'direct';
- $status->in_reply_to_profile_id = $recipient->id;
- $status->save();
-
- $dm = new DirectMessage;
- $dm->to_id = $recipient->id;
- $dm->from_id = $profile->id;
- $dm->status_id = $status->id;
- $dm->is_hidden = $hidden;
- $dm->type = $request->input('type');
- $dm->save();
-
- Conversation::updateOrInsert(
- [
- 'to_id' => $recipient->id,
- 'from_id' => $profile->id
- ],
- [
- 'type' => $dm->type,
- 'status_id' => $status->id,
- 'dm_id' => $dm->id,
- 'is_hidden' => $hidden
- ]
- );
-
- if(filter_var($msg, FILTER_VALIDATE_URL)) {
- if(Helpers::validateUrl($msg)) {
- $dm->type = 'link';
- $dm->meta = [
- 'domain' => parse_url($msg, PHP_URL_HOST),
- 'local' => parse_url($msg, PHP_URL_HOST) ==
- parse_url(config('app.url'), PHP_URL_HOST)
- ];
- $dm->save();
- }
- }
-
- $nf = UserFilter::whereUserId($recipient->id)
- ->whereFilterableId($profile->id)
- ->whereFilterableType('App\Profile')
- ->whereFilterType('dm.mute')
- ->exists();
-
- if($recipient->domain == null && $hidden == false && !$nf) {
- $notification = new Notification();
- $notification->profile_id = $recipient->id;
- $notification->actor_id = $profile->id;
- $notification->action = 'dm';
- $notification->item_id = $dm->id;
- $notification->item_type = "App\DirectMessage";
- $notification->save();
- }
-
- if($recipient->domain) {
- $this->remoteDeliver($dm);
- }
-
- $res = [
- 'id' => (string) $dm->id,
- 'isAuthor' => $profile->id == $dm->from_id,
- 'reportId' => (string) $dm->status_id,
- 'hidden' => (bool) $dm->is_hidden,
- 'type' => $dm->type,
- 'text' => $dm->status->caption,
- 'media' => null,
- 'timeAgo' => $dm->created_at->diffForHumans(null,null,true),
- 'seen' => $dm->read_at != null,
- 'meta' => $dm->meta
- ];
-
- return response()->json($res);
- }
-
- public function thread(Request $request)
- {
- $this->validate($request, [
- 'pid' => 'required'
- ]);
- $uid = $request->user()->profile_id;
- $pid = $request->input('pid');
- $max_id = $request->input('max_id');
- $min_id = $request->input('min_id');
-
- $r = Profile::findOrFail($pid);
-
- if($min_id) {
- $res = DirectMessage::select('*')
- ->where('id', '>', $min_id)
- ->where(function($q) use($pid,$uid) {
- return $q->where([['from_id',$pid],['to_id',$uid]
- ])->orWhere([['from_id',$uid],['to_id',$pid]]);
- })
- ->latest()
- ->take(8)
- ->get();
- } else if ($max_id) {
- $res = DirectMessage::select('*')
- ->where('id', '<', $max_id)
- ->where(function($q) use($pid,$uid) {
- return $q->where([['from_id',$pid],['to_id',$uid]
- ])->orWhere([['from_id',$uid],['to_id',$pid]]);
- })
- ->latest()
- ->take(8)
- ->get();
- } else {
- $res = DirectMessage::where(function($q) use($pid,$uid) {
- return $q->where([['from_id',$pid],['to_id',$uid]
- ])->orWhere([['from_id',$uid],['to_id',$pid]]);
- })
- ->latest()
- ->take(8)
- ->get();
- }
-
- $res = $res->filter(function($s) {
- return $s && $s->status;
- })
- ->map(function($s) use ($uid) {
- return [
- 'id' => (string) $s->id,
- 'hidden' => (bool) $s->is_hidden,
- 'isAuthor' => $uid == $s->from_id,
- 'type' => $s->type,
- 'text' => $s->status->caption,
- 'media' => $s->status->firstMedia() ? $s->status->firstMedia()->url() : null,
- 'timeAgo' => $s->created_at->diffForHumans(null,null,true),
- 'seen' => $s->read_at != null,
- 'reportId' => (string) $s->status_id,
- 'meta' => json_decode($s->meta,true)
- ];
- })
- ->values();
-
- $w = [
- 'id' => (string) $r->id,
- 'name' => $r->name,
- 'username' => $r->username,
- 'avatar' => $r->avatarUrl(),
- 'url' => $r->url(),
- 'muted' => UserFilter::whereUserId($uid)
- ->whereFilterableId($r->id)
- ->whereFilterableType('App\Profile')
- ->whereFilterType('dm.mute')
- ->first() ? true : false,
- 'isLocal' => (bool) !$r->domain,
- 'domain' => $r->domain,
- 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
- 'lastMessage' => '',
- 'messages' => $res
- ];
-
- return response()->json($w, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
- }
-
- public function delete(Request $request)
- {
- $this->validate($request, [
- 'id' => 'required'
- ]);
-
- $sid = $request->input('id');
- $pid = $request->user()->profile_id;
-
- $dm = DirectMessage::whereFromId($pid)
- ->whereStatusId($sid)
- ->firstOrFail();
-
- $status = Status::whereProfileId($pid)
- ->findOrFail($dm->status_id);
-
- $recipient = AccountService::get($dm->to_id);
-
- if(!$recipient) {
- return response('', 422);
- }
-
- if($recipient['local'] == false) {
- $dmc = $dm;
- $this->remoteDelete($dmc);
- } else {
- StatusDelete::dispatch($status)->onQueue('high');
- }
-
- if(Conversation::whereStatusId($sid)->count()) {
- $latest = DirectMessage::where(['from_id' => $dm->from_id, 'to_id' => $dm->to_id])
- ->orWhere(['to_id' => $dm->from_id, 'from_id' => $dm->to_id])
- ->latest()
- ->first();
-
- if($latest->status_id == $sid) {
- Conversation::where(['to_id' => $dm->from_id, 'from_id' => $dm->to_id])
- ->update([
- 'updated_at' => $latest->updated_at,
- 'status_id' => $latest->status_id,
- 'type' => $latest->type,
- 'is_hidden' => false
- ]);
-
- Conversation::where(['to_id' => $dm->to_id, 'from_id' => $dm->from_id])
- ->update([
- 'updated_at' => $latest->updated_at,
- 'status_id' => $latest->status_id,
- 'type' => $latest->type,
- 'is_hidden' => false
- ]);
- } else {
- Conversation::where([
- 'status_id' => $sid,
- 'to_id' => $dm->from_id,
- 'from_id' => $dm->to_id
- ])->delete();
-
- Conversation::where([
- 'status_id' => $sid,
- 'from_id' => $dm->from_id,
- 'to_id' => $dm->to_id
- ])->delete();
- }
- }
-
- StatusService::del($status->id, true);
-
- $status->forceDeleteQuietly();
- return [200];
- }
-
- public function get(Request $request, $id)
- {
- $pid = $request->user()->profile_id;
- $dm = DirectMessage::whereStatusId($id)->firstOrFail();
- abort_if($pid !== $dm->to_id && $pid !== $dm->from_id, 404);
- return response()->json($dm, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
- }
-
- public function mediaUpload(Request $request)
- {
- $this->validate($request, [
- 'file' => function() {
- return [
- 'required',
- 'mimetypes:' . config_cache('pixelfed.media_types'),
- 'max:' . config_cache('pixelfed.max_photo_size'),
- ];
- },
- 'to_id' => 'required'
- ]);
-
- $user = $request->user();
- $profile = $user->profile;
- $recipient = Profile::where('id', '!=', $profile->id)->findOrFail($request->input('to_id'));
- abort_if(in_array($profile->id, $recipient->blockedIds()->toArray()), 403);
-
- if((!$recipient->domain && $recipient->user->settings->public_dm == false) || $recipient->is_private) {
- if($recipient->follows($profile) == true) {
- $hidden = false;
- } else {
- $hidden = true;
- }
- } else {
- $hidden = false;
- }
-
- if(config_cache('pixelfed.enforce_account_limit') == true) {
- $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
- return Media::whereUserId($user->id)->sum('size') / 1000;
- });
- $limit = (int) config_cache('pixelfed.max_account_size');
- if ($size >= $limit) {
- abort(403, 'Account size limit reached.');
- }
- }
- $photo = $request->file('file');
-
- $mimes = explode(',', config_cache('pixelfed.media_types'));
- if(in_array($photo->getMimeType(), $mimes) == false) {
- abort(403, 'Invalid or unsupported mime type.');
- }
-
- $storagePath = MediaPathService::get($user, 2) . Str::random(8);
- $path = $photo->storePublicly($storagePath);
- $hash = \hash_file('sha256', $photo);
-
- abort_if(MediaBlocklistService::exists($hash) == true, 451);
-
- $status = new Status;
- $status->profile_id = $profile->id;
- $status->caption = null;
- $status->rendered = null;
- $status->visibility = 'direct';
- $status->scope = 'direct';
- $status->in_reply_to_profile_id = $recipient->id;
- $status->save();
-
- $media = new Media();
- $media->status_id = $status->id;
- $media->profile_id = $profile->id;
- $media->user_id = $user->id;
- $media->media_path = $path;
- $media->original_sha256 = $hash;
- $media->size = $photo->getSize();
- $media->mime = $photo->getMimeType();
- $media->caption = null;
- $media->filter_class = null;
- $media->filter_name = null;
- $media->save();
-
- $dm = new DirectMessage;
- $dm->to_id = $recipient->id;
- $dm->from_id = $profile->id;
- $dm->status_id = $status->id;
- $dm->type = array_first(explode('/', $media->mime)) == 'video' ? 'video' : 'photo';
- $dm->is_hidden = $hidden;
- $dm->save();
-
- Conversation::updateOrInsert(
- [
- 'to_id' => $recipient->id,
- 'from_id' => $profile->id
- ],
- [
- 'type' => $dm->type,
- 'status_id' => $status->id,
- 'dm_id' => $dm->id,
- 'is_hidden' => $hidden
- ]
- );
-
- if($recipient->domain) {
- $this->remoteDeliver($dm);
- }
-
- return [
- 'id' => $dm->id,
- 'reportId' => (string) $dm->status_id,
- 'type' => $dm->type,
- 'url' => $media->url()
- ];
- }
-
- public function composeLookup(Request $request)
- {
- $this->validate($request, [
- 'q' => 'required|string|min:2|max:50',
- 'remote' => 'nullable',
- ]);
-
- $q = $request->input('q');
- $r = $request->input('remote', false);
-
- if($r && !Str::of($q)->contains('.')) {
- return [];
- }
-
- if($r && Helpers::validateUrl($q)) {
- Helpers::profileFetch($q);
- }
-
- if(Str::of($q)->startsWith('@')) {
- if(strlen($q) < 3) {
- return [];
- }
- if(substr_count($q, '@') == 2) {
- WebfingerService::lookup($q);
- }
- $q = mb_substr($q, 1);
- }
-
- $blocked = UserFilter::whereFilterableType('App\Profile')
- ->whereFilterType('block')
- ->whereFilterableId($request->user()->profile_id)
- ->pluck('user_id');
-
- $blocked->push($request->user()->profile_id);
-
- $results = Profile::select('id','domain','username')
- ->whereNotIn('id', $blocked)
- ->where('username','like','%'.$q.'%')
- ->orderBy('domain')
- ->limit(8)
- ->get()
- ->map(function($r) {
- $acct = AccountService::get($r->id);
- return [
- 'local' => (bool) !$r->domain,
- 'id' => (string) $r->id,
- 'name' => $r->username,
- 'privacy' => true,
- 'avatar' => $r->avatarUrl(),
- 'account' => $acct
- ];
- });
-
- return $results;
- }
-
- public function read(Request $request)
- {
- $this->validate($request, [
- 'pid' => 'required',
- 'sid' => 'required'
- ]);
-
- $pid = $request->input('pid');
- $sid = $request->input('sid');
-
- $dms = DirectMessage::whereToId($request->user()->profile_id)
- ->whereFromId($pid)
- ->where('status_id', '>=', $sid)
- ->get();
-
- $now = now();
- foreach($dms as $dm) {
- $dm->read_at = $now;
- $dm->save();
- }
-
- return response()->json($dms->pluck('id'));
- }
-
- public function mute(Request $request)
- {
- $this->validate($request, [
- 'id' => 'required'
- ]);
-
- $fid = $request->input('id');
- $pid = $request->user()->profile_id;
-
- UserFilter::firstOrCreate(
- [
- 'user_id' => $pid,
- 'filterable_id' => $fid,
- 'filterable_type' => 'App\Profile',
- 'filter_type' => 'dm.mute'
- ]
- );
-
- return [200];
- }
-
- public function unmute(Request $request)
- {
- $this->validate($request, [
- 'id' => 'required'
- ]);
-
- $fid = $request->input('id');
- $pid = $request->user()->profile_id;
-
- $f = UserFilter::whereUserId($pid)
- ->whereFilterableId($fid)
- ->whereFilterableType('App\Profile')
- ->whereFilterType('dm.mute')
- ->firstOrFail();
-
- $f->delete();
-
- return [200];
- }
-
- public function remoteDeliver($dm)
- {
- $profile = $dm->author;
- $url = $dm->recipient->sharedInbox ?? $dm->recipient->inbox_url;
-
- $tags = [
- [
- 'type' => 'Mention',
- 'href' => $dm->recipient->permalink(),
- 'name' => $dm->recipient->emailUrl(),
- ]
- ];
-
- $body = [
- '@context' => [
- 'https://w3id.org/security/v1',
- 'https://www.w3.org/ns/activitystreams',
- ],
- 'id' => $dm->status->permalink(),
- 'type' => 'Create',
- 'actor' => $dm->status->profile->permalink(),
- 'published' => $dm->status->created_at->toAtomString(),
- 'to' => [$dm->recipient->permalink()],
- 'cc' => [],
- 'object' => [
- 'id' => $dm->status->url(),
- 'type' => 'Note',
- 'summary' => null,
- 'content' => $dm->status->rendered ?? $dm->status->caption,
- 'inReplyTo' => null,
- 'published' => $dm->status->created_at->toAtomString(),
- 'url' => $dm->status->url(),
- 'attributedTo' => $dm->status->profile->permalink(),
- 'to' => [$dm->recipient->permalink()],
- 'cc' => [],
- 'sensitive' => (bool) $dm->status->is_nsfw,
- 'attachment' => $dm->status->media()->orderBy('order')->get()->map(function ($media) {
- return [
- 'type' => $media->activityVerb(),
- 'mediaType' => $media->mime,
- 'url' => $media->url(),
- 'name' => $media->caption,
- ];
- })->toArray(),
- 'tag' => $tags,
- ]
- ];
-
- DirectDeliverPipeline::dispatch($profile, $url, $body)->onQueue('high');
- }
-
- public function remoteDelete($dm)
- {
- $profile = $dm->author;
- $url = $dm->recipient->sharedInbox ?? $dm->recipient->inbox_url;
-
- $body = [
- '@context' => [
- 'https://www.w3.org/ns/activitystreams',
- ],
- 'id' => $dm->status->permalink('#delete'),
- 'to' => [
- 'https://www.w3.org/ns/activitystreams#Public'
- ],
- 'type' => 'Delete',
- 'actor' => $dm->status->profile->permalink(),
- 'object' => [
- 'id' => $dm->status->url(),
- 'type' => 'Tombstone'
- ]
- ];
- DirectDeletePipeline::dispatch($profile, $url, $body)->onQueue('high');
- }
+ public function __construct()
+ {
+ $this->middleware('auth');
+ }
+
+ public function browse(Request $request)
+ {
+ $this->validate($request, [
+ 'a' => 'nullable|string|in:inbox,sent,filtered',
+ 'page' => 'nullable|integer|min:1|max:99'
+ ]);
+
+ $user = $request->user();
+ if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id)) {
+ return [];
+ }
+ $profile = $user->profile_id;
+ $action = $request->input('a', 'inbox');
+ $page = $request->input('page');
+
+ if(config('database.default') == 'pgsql') {
+ if($action == 'inbox') {
+ $dms = DirectMessage::select('id', 'type', 'to_id', 'from_id', 'id', 'status_id', 'is_hidden', 'meta', 'created_at', 'read_at')
+ ->whereToId($profile)
+ ->with(['author','status'])
+ ->whereIsHidden(false)
+ ->when($page, function($q, $page) {
+ if($page > 1) {
+ return $q->offset($page * 8 - 8);
+ }
+ })
+ ->latest()
+ ->get()
+ ->unique('from_id')
+ ->take(8)
+ ->map(function($r) use($profile) {
+ return $r->from_id !== $profile ? [
+ 'id' => (string) $r->from_id,
+ 'name' => $r->author->name,
+ 'username' => $r->author->username,
+ 'avatar' => $r->author->avatarUrl(),
+ 'url' => $r->author->url(),
+ 'isLocal' => (bool) !$r->author->domain,
+ 'domain' => $r->author->domain,
+ 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
+ 'lastMessage' => $r->status->caption,
+ 'messages' => []
+ ] : [
+ 'id' => (string) $r->to_id,
+ 'name' => $r->recipient->name,
+ 'username' => $r->recipient->username,
+ 'avatar' => $r->recipient->avatarUrl(),
+ 'url' => $r->recipient->url(),
+ 'isLocal' => (bool) !$r->recipient->domain,
+ 'domain' => $r->recipient->domain,
+ 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
+ 'lastMessage' => $r->status->caption,
+ 'messages' => []
+ ];
+ })->values();
+ }
+
+ if($action == 'sent') {
+ $dms = DirectMessage::select('id', 'type', 'to_id', 'from_id', 'id', 'status_id', 'is_hidden', 'meta', 'created_at', 'read_at')
+ ->whereFromId($profile)
+ ->with(['author','status'])
+ ->orderBy('id', 'desc')
+ ->when($page, function($q, $page) {
+ if($page > 1) {
+ return $q->offset($page * 8 - 8);
+ }
+ })
+ ->get()
+ ->unique('to_id')
+ ->take(8)
+ ->map(function($r) use($profile) {
+ return $r->from_id !== $profile ? [
+ 'id' => (string) $r->from_id,
+ 'name' => $r->author->name,
+ 'username' => $r->author->username,
+ 'avatar' => $r->author->avatarUrl(),
+ 'url' => $r->author->url(),
+ 'isLocal' => (bool) !$r->author->domain,
+ 'domain' => $r->author->domain,
+ 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
+ 'lastMessage' => $r->status->caption,
+ 'messages' => []
+ ] : [
+ 'id' => (string) $r->to_id,
+ 'name' => $r->recipient->name,
+ 'username' => $r->recipient->username,
+ 'avatar' => $r->recipient->avatarUrl(),
+ 'url' => $r->recipient->url(),
+ 'isLocal' => (bool) !$r->recipient->domain,
+ 'domain' => $r->recipient->domain,
+ 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
+ 'lastMessage' => $r->status->caption,
+ 'messages' => []
+ ];
+ });
+ }
+
+ if($action == 'filtered') {
+ $dms = DirectMessage::select('id', 'type', 'to_id', 'from_id', 'id', 'status_id', 'is_hidden', 'meta', 'created_at', 'read_at')
+ ->whereToId($profile)
+ ->with(['author','status'])
+ ->whereIsHidden(true)
+ ->orderBy('id', 'desc')
+ ->when($page, function($q, $page) {
+ if($page > 1) {
+ return $q->offset($page * 8 - 8);
+ }
+ })
+ ->get()
+ ->unique('from_id')
+ ->take(8)
+ ->map(function($r) use($profile) {
+ return $r->from_id !== $profile ? [
+ 'id' => (string) $r->from_id,
+ 'name' => $r->author->name,
+ 'username' => $r->author->username,
+ 'avatar' => $r->author->avatarUrl(),
+ 'url' => $r->author->url(),
+ 'isLocal' => (bool) !$r->author->domain,
+ 'domain' => $r->author->domain,
+ 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
+ 'lastMessage' => $r->status->caption,
+ 'messages' => []
+ ] : [
+ 'id' => (string) $r->to_id,
+ 'name' => $r->recipient->name,
+ 'username' => $r->recipient->username,
+ 'avatar' => $r->recipient->avatarUrl(),
+ 'url' => $r->recipient->url(),
+ 'isLocal' => (bool) !$r->recipient->domain,
+ 'domain' => $r->recipient->domain,
+ 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
+ 'lastMessage' => $r->status->caption,
+ 'messages' => []
+ ];
+ });
+ }
+ } elseif(config('database.default') == 'mysql') {
+ if($action == 'inbox') {
+ $dms = DirectMessage::selectRaw('*, max(created_at) as createdAt')
+ ->whereToId($profile)
+ ->with(['author','status'])
+ ->whereIsHidden(false)
+ ->groupBy('from_id')
+ ->latest()
+ ->when($page, function($q, $page) {
+ if($page > 1) {
+ return $q->offset($page * 8 - 8);
+ }
+ })
+ ->limit(8)
+ ->get()
+ ->map(function($r) use($profile) {
+ return $r->from_id !== $profile ? [
+ 'id' => (string) $r->from_id,
+ 'name' => $r->author->name,
+ 'username' => $r->author->username,
+ 'avatar' => $r->author->avatarUrl(),
+ 'url' => $r->author->url(),
+ 'isLocal' => (bool) !$r->author->domain,
+ 'domain' => $r->author->domain,
+ 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
+ 'lastMessage' => $r->status->caption,
+ 'messages' => []
+ ] : [
+ 'id' => (string) $r->to_id,
+ 'name' => $r->recipient->name,
+ 'username' => $r->recipient->username,
+ 'avatar' => $r->recipient->avatarUrl(),
+ 'url' => $r->recipient->url(),
+ 'isLocal' => (bool) !$r->recipient->domain,
+ 'domain' => $r->recipient->domain,
+ 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
+ 'lastMessage' => $r->status->caption,
+ 'messages' => []
+ ];
+ });
+ }
+
+ if($action == 'sent') {
+ $dms = DirectMessage::selectRaw('*, max(created_at) as createdAt')
+ ->whereFromId($profile)
+ ->with(['author','status'])
+ ->groupBy('to_id')
+ ->orderBy('createdAt', 'desc')
+ ->when($page, function($q, $page) {
+ if($page > 1) {
+ return $q->offset($page * 8 - 8);
+ }
+ })
+ ->limit(8)
+ ->get()
+ ->map(function($r) use($profile) {
+ return $r->from_id !== $profile ? [
+ 'id' => (string) $r->from_id,
+ 'name' => $r->author->name,
+ 'username' => $r->author->username,
+ 'avatar' => $r->author->avatarUrl(),
+ 'url' => $r->author->url(),
+ 'isLocal' => (bool) !$r->author->domain,
+ 'domain' => $r->author->domain,
+ 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
+ 'lastMessage' => $r->status->caption,
+ 'messages' => []
+ ] : [
+ 'id' => (string) $r->to_id,
+ 'name' => $r->recipient->name,
+ 'username' => $r->recipient->username,
+ 'avatar' => $r->recipient->avatarUrl(),
+ 'url' => $r->recipient->url(),
+ 'isLocal' => (bool) !$r->recipient->domain,
+ 'domain' => $r->recipient->domain,
+ 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
+ 'lastMessage' => $r->status->caption,
+ 'messages' => []
+ ];
+ });
+ }
+
+ if($action == 'filtered') {
+ $dms = DirectMessage::selectRaw('*, max(created_at) as createdAt')
+ ->whereToId($profile)
+ ->with(['author','status'])
+ ->whereIsHidden(true)
+ ->groupBy('from_id')
+ ->orderBy('createdAt', 'desc')
+ ->when($page, function($q, $page) {
+ if($page > 1) {
+ return $q->offset($page * 8 - 8);
+ }
+ })
+ ->limit(8)
+ ->get()
+ ->map(function($r) use($profile) {
+ return $r->from_id !== $profile ? [
+ 'id' => (string) $r->from_id,
+ 'name' => $r->author->name,
+ 'username' => $r->author->username,
+ 'avatar' => $r->author->avatarUrl(),
+ 'url' => $r->author->url(),
+ 'isLocal' => (bool) !$r->author->domain,
+ 'domain' => $r->author->domain,
+ 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
+ 'lastMessage' => $r->status->caption,
+ 'messages' => []
+ ] : [
+ 'id' => (string) $r->to_id,
+ 'name' => $r->recipient->name,
+ 'username' => $r->recipient->username,
+ 'avatar' => $r->recipient->avatarUrl(),
+ 'url' => $r->recipient->url(),
+ 'isLocal' => (bool) !$r->recipient->domain,
+ 'domain' => $r->recipient->domain,
+ 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
+ 'lastMessage' => $r->status->caption,
+ 'messages' => []
+ ];
+ });
+ }
+ }
+
+ return response()->json($dms->all());
+ }
+
+ public function create(Request $request)
+ {
+ $this->validate($request, [
+ 'to_id' => 'required',
+ 'message' => 'required|string|min:1|max:500',
+ 'type' => 'required|in:text,emoji'
+ ]);
+
+ $user = $request->user();
+ abort_if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action');
+ $profile = $user->profile;
+ $recipient = Profile::where('id', '!=', $profile->id)->findOrFail($request->input('to_id'));
+
+ abort_if(in_array($profile->id, $recipient->blockedIds()->toArray()), 403);
+ $msg = $request->input('message');
+
+ if((!$recipient->domain && $recipient->user->settings->public_dm == false) || $recipient->is_private) {
+ if($recipient->follows($profile) == true) {
+ $hidden = false;
+ } else {
+ $hidden = true;
+ }
+ } else {
+ $hidden = false;
+ }
+
+ $status = new Status;
+ $status->profile_id = $profile->id;
+ $status->caption = $msg;
+ $status->rendered = $msg;
+ $status->visibility = 'direct';
+ $status->scope = 'direct';
+ $status->in_reply_to_profile_id = $recipient->id;
+ $status->save();
+
+ $dm = new DirectMessage;
+ $dm->to_id = $recipient->id;
+ $dm->from_id = $profile->id;
+ $dm->status_id = $status->id;
+ $dm->is_hidden = $hidden;
+ $dm->type = $request->input('type');
+ $dm->save();
+
+ Conversation::updateOrInsert(
+ [
+ 'to_id' => $recipient->id,
+ 'from_id' => $profile->id
+ ],
+ [
+ 'type' => $dm->type,
+ 'status_id' => $status->id,
+ 'dm_id' => $dm->id,
+ 'is_hidden' => $hidden
+ ]
+ );
+
+ if(filter_var($msg, FILTER_VALIDATE_URL)) {
+ if(Helpers::validateUrl($msg)) {
+ $dm->type = 'link';
+ $dm->meta = [
+ 'domain' => parse_url($msg, PHP_URL_HOST),
+ 'local' => parse_url($msg, PHP_URL_HOST) ==
+ parse_url(config('app.url'), PHP_URL_HOST)
+ ];
+ $dm->save();
+ }
+ }
+
+ $nf = UserFilter::whereUserId($recipient->id)
+ ->whereFilterableId($profile->id)
+ ->whereFilterableType('App\Profile')
+ ->whereFilterType('dm.mute')
+ ->exists();
+
+ if($recipient->domain == null && $hidden == false && !$nf) {
+ $notification = new Notification();
+ $notification->profile_id = $recipient->id;
+ $notification->actor_id = $profile->id;
+ $notification->action = 'dm';
+ $notification->item_id = $dm->id;
+ $notification->item_type = "App\DirectMessage";
+ $notification->save();
+ }
+
+ if($recipient->domain) {
+ $this->remoteDeliver($dm);
+ }
+
+ $res = [
+ 'id' => (string) $dm->id,
+ 'isAuthor' => $profile->id == $dm->from_id,
+ 'reportId' => (string) $dm->status_id,
+ 'hidden' => (bool) $dm->is_hidden,
+ 'type' => $dm->type,
+ 'text' => $dm->status->caption,
+ 'media' => null,
+ 'timeAgo' => $dm->created_at->diffForHumans(null,null,true),
+ 'seen' => $dm->read_at != null,
+ 'meta' => $dm->meta
+ ];
+
+ return response()->json($res);
+ }
+
+ public function thread(Request $request)
+ {
+ $this->validate($request, [
+ 'pid' => 'required'
+ ]);
+ $user = $request->user();
+ abort_if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action');
+
+ $uid = $user->profile_id;
+ $pid = $request->input('pid');
+ $max_id = $request->input('max_id');
+ $min_id = $request->input('min_id');
+
+ $r = Profile::findOrFail($pid);
+
+ if($min_id) {
+ $res = DirectMessage::select('*')
+ ->where('id', '>', $min_id)
+ ->where(function($q) use($pid,$uid) {
+ return $q->where([['from_id',$pid],['to_id',$uid]
+ ])->orWhere([['from_id',$uid],['to_id',$pid]]);
+ })
+ ->latest()
+ ->take(8)
+ ->get();
+ } else if ($max_id) {
+ $res = DirectMessage::select('*')
+ ->where('id', '<', $max_id)
+ ->where(function($q) use($pid,$uid) {
+ return $q->where([['from_id',$pid],['to_id',$uid]
+ ])->orWhere([['from_id',$uid],['to_id',$pid]]);
+ })
+ ->latest()
+ ->take(8)
+ ->get();
+ } else {
+ $res = DirectMessage::where(function($q) use($pid,$uid) {
+ return $q->where([['from_id',$pid],['to_id',$uid]
+ ])->orWhere([['from_id',$uid],['to_id',$pid]]);
+ })
+ ->latest()
+ ->take(8)
+ ->get();
+ }
+
+ $res = $res->filter(function($s) {
+ return $s && $s->status;
+ })
+ ->map(function($s) use ($uid) {
+ return [
+ 'id' => (string) $s->id,
+ 'hidden' => (bool) $s->is_hidden,
+ 'isAuthor' => $uid == $s->from_id,
+ 'type' => $s->type,
+ 'text' => $s->status->caption,
+ 'media' => $s->status->firstMedia() ? $s->status->firstMedia()->url() : null,
+ 'timeAgo' => $s->created_at->diffForHumans(null,null,true),
+ 'seen' => $s->read_at != null,
+ 'reportId' => (string) $s->status_id,
+ 'meta' => json_decode($s->meta,true)
+ ];
+ })
+ ->values();
+
+ $w = [
+ 'id' => (string) $r->id,
+ 'name' => $r->name,
+ 'username' => $r->username,
+ 'avatar' => $r->avatarUrl(),
+ 'url' => $r->url(),
+ 'muted' => UserFilter::whereUserId($uid)
+ ->whereFilterableId($r->id)
+ ->whereFilterableType('App\Profile')
+ ->whereFilterType('dm.mute')
+ ->first() ? true : false,
+ 'isLocal' => (bool) !$r->domain,
+ 'domain' => $r->domain,
+ 'timeAgo' => $r->created_at->diffForHumans(null, true, true),
+ 'lastMessage' => '',
+ 'messages' => $res
+ ];
+
+ return response()->json($w, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
+ }
+
+ public function delete(Request $request)
+ {
+ $this->validate($request, [
+ 'id' => 'required'
+ ]);
+
+ $sid = $request->input('id');
+ $pid = $request->user()->profile_id;
+
+ $dm = DirectMessage::whereFromId($pid)
+ ->whereStatusId($sid)
+ ->firstOrFail();
+
+ $status = Status::whereProfileId($pid)
+ ->findOrFail($dm->status_id);
+
+ $recipient = AccountService::get($dm->to_id);
+
+ if(!$recipient) {
+ return response('', 422);
+ }
+
+ if($recipient['local'] == false) {
+ $dmc = $dm;
+ $this->remoteDelete($dmc);
+ } else {
+ StatusDelete::dispatch($status)->onQueue('high');
+ }
+
+ if(Conversation::whereStatusId($sid)->count()) {
+ $latest = DirectMessage::where(['from_id' => $dm->from_id, 'to_id' => $dm->to_id])
+ ->orWhere(['to_id' => $dm->from_id, 'from_id' => $dm->to_id])
+ ->latest()
+ ->first();
+
+ if($latest->status_id == $sid) {
+ Conversation::where(['to_id' => $dm->from_id, 'from_id' => $dm->to_id])
+ ->update([
+ 'updated_at' => $latest->updated_at,
+ 'status_id' => $latest->status_id,
+ 'type' => $latest->type,
+ 'is_hidden' => false
+ ]);
+
+ Conversation::where(['to_id' => $dm->to_id, 'from_id' => $dm->from_id])
+ ->update([
+ 'updated_at' => $latest->updated_at,
+ 'status_id' => $latest->status_id,
+ 'type' => $latest->type,
+ 'is_hidden' => false
+ ]);
+ } else {
+ Conversation::where([
+ 'status_id' => $sid,
+ 'to_id' => $dm->from_id,
+ 'from_id' => $dm->to_id
+ ])->delete();
+
+ Conversation::where([
+ 'status_id' => $sid,
+ 'from_id' => $dm->from_id,
+ 'to_id' => $dm->to_id
+ ])->delete();
+ }
+ }
+
+ StatusService::del($status->id, true);
+
+ $status->forceDeleteQuietly();
+ return [200];
+ }
+
+ public function get(Request $request, $id)
+ {
+ $user = $request->user();
+ abort_if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action');
+
+ $pid = $request->user()->profile_id;
+ $dm = DirectMessage::whereStatusId($id)->firstOrFail();
+ abort_if($pid !== $dm->to_id && $pid !== $dm->from_id, 404);
+ return response()->json($dm, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
+ }
+
+ public function mediaUpload(Request $request)
+ {
+ $this->validate($request, [
+ 'file' => function() {
+ return [
+ 'required',
+ 'mimetypes:' . config_cache('pixelfed.media_types'),
+ 'max:' . config_cache('pixelfed.max_photo_size'),
+ ];
+ },
+ 'to_id' => 'required'
+ ]);
+
+ $user = $request->user();
+ abort_if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action');
+ $profile = $user->profile;
+ $recipient = Profile::where('id', '!=', $profile->id)->findOrFail($request->input('to_id'));
+ abort_if(in_array($profile->id, $recipient->blockedIds()->toArray()), 403);
+
+ if((!$recipient->domain && $recipient->user->settings->public_dm == false) || $recipient->is_private) {
+ if($recipient->follows($profile) == true) {
+ $hidden = false;
+ } else {
+ $hidden = true;
+ }
+ } else {
+ $hidden = false;
+ }
+
+ if(config_cache('pixelfed.enforce_account_limit') == true) {
+ $size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
+ return Media::whereUserId($user->id)->sum('size') / 1000;
+ });
+ $limit = (int) config_cache('pixelfed.max_account_size');
+ if ($size >= $limit) {
+ abort(403, 'Account size limit reached.');
+ }
+ }
+ $photo = $request->file('file');
+
+ $mimes = explode(',', config_cache('pixelfed.media_types'));
+ if(in_array($photo->getMimeType(), $mimes) == false) {
+ abort(403, 'Invalid or unsupported mime type.');
+ }
+
+ $storagePath = MediaPathService::get($user, 2) . Str::random(8);
+ $path = $photo->storePublicly($storagePath);
+ $hash = \hash_file('sha256', $photo);
+
+ abort_if(MediaBlocklistService::exists($hash) == true, 451);
+
+ $status = new Status;
+ $status->profile_id = $profile->id;
+ $status->caption = null;
+ $status->rendered = null;
+ $status->visibility = 'direct';
+ $status->scope = 'direct';
+ $status->in_reply_to_profile_id = $recipient->id;
+ $status->save();
+
+ $media = new Media();
+ $media->status_id = $status->id;
+ $media->profile_id = $profile->id;
+ $media->user_id = $user->id;
+ $media->media_path = $path;
+ $media->original_sha256 = $hash;
+ $media->size = $photo->getSize();
+ $media->mime = $photo->getMimeType();
+ $media->caption = null;
+ $media->filter_class = null;
+ $media->filter_name = null;
+ $media->save();
+
+ $dm = new DirectMessage;
+ $dm->to_id = $recipient->id;
+ $dm->from_id = $profile->id;
+ $dm->status_id = $status->id;
+ $dm->type = array_first(explode('/', $media->mime)) == 'video' ? 'video' : 'photo';
+ $dm->is_hidden = $hidden;
+ $dm->save();
+
+ Conversation::updateOrInsert(
+ [
+ 'to_id' => $recipient->id,
+ 'from_id' => $profile->id
+ ],
+ [
+ 'type' => $dm->type,
+ 'status_id' => $status->id,
+ 'dm_id' => $dm->id,
+ 'is_hidden' => $hidden
+ ]
+ );
+
+ if($recipient->domain) {
+ $this->remoteDeliver($dm);
+ }
+
+ return [
+ 'id' => $dm->id,
+ 'reportId' => (string) $dm->status_id,
+ 'type' => $dm->type,
+ 'url' => $media->url()
+ ];
+ }
+
+ public function composeLookup(Request $request)
+ {
+ $this->validate($request, [
+ 'q' => 'required|string|min:2|max:50',
+ 'remote' => 'nullable',
+ ]);
+
+ $user = $request->user();
+ if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id)) {
+ return [];
+ }
+
+ $q = $request->input('q');
+ $r = $request->input('remote', false);
+
+ if($r && !Str::of($q)->contains('.')) {
+ return [];
+ }
+
+ if($r && Helpers::validateUrl($q)) {
+ Helpers::profileFetch($q);
+ }
+
+ if(Str::of($q)->startsWith('@')) {
+ if(strlen($q) < 3) {
+ return [];
+ }
+ if(substr_count($q, '@') == 2) {
+ WebfingerService::lookup($q);
+ }
+ $q = mb_substr($q, 1);
+ }
+
+ $blocked = UserFilter::whereFilterableType('App\Profile')
+ ->whereFilterType('block')
+ ->whereFilterableId($request->user()->profile_id)
+ ->pluck('user_id');
+
+ $blocked->push($request->user()->profile_id);
+
+ $results = Profile::select('id','domain','username')
+ ->whereNotIn('id', $blocked)
+ ->where('username','like','%'.$q.'%')
+ ->orderBy('domain')
+ ->limit(8)
+ ->get()
+ ->map(function($r) {
+ $acct = AccountService::get($r->id);
+ return [
+ 'local' => (bool) !$r->domain,
+ 'id' => (string) $r->id,
+ 'name' => $r->username,
+ 'privacy' => true,
+ 'avatar' => $r->avatarUrl(),
+ 'account' => $acct
+ ];
+ });
+
+ return $results;
+ }
+
+ public function read(Request $request)
+ {
+ $this->validate($request, [
+ 'pid' => 'required',
+ 'sid' => 'required'
+ ]);
+
+ $pid = $request->input('pid');
+ $sid = $request->input('sid');
+ $user = $request->user();
+ abort_if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action');
+
+ $dms = DirectMessage::whereToId($request->user()->profile_id)
+ ->whereFromId($pid)
+ ->where('status_id', '>=', $sid)
+ ->get();
+
+ $now = now();
+ foreach($dms as $dm) {
+ $dm->read_at = $now;
+ $dm->save();
+ }
+
+ return response()->json($dms->pluck('id'));
+ }
+
+ public function mute(Request $request)
+ {
+ $this->validate($request, [
+ 'id' => 'required'
+ ]);
+
+ $user = $request->user();
+ abort_if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action');
+ $fid = $request->input('id');
+ $pid = $request->user()->profile_id;
+
+ UserFilter::firstOrCreate(
+ [
+ 'user_id' => $pid,
+ 'filterable_id' => $fid,
+ 'filterable_type' => 'App\Profile',
+ 'filter_type' => 'dm.mute'
+ ]
+ );
+
+ return [200];
+ }
+
+ public function unmute(Request $request)
+ {
+ $this->validate($request, [
+ 'id' => 'required'
+ ]);
+
+ $user = $request->user();
+ abort_if($user->has_roles && !UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action');
+
+ $fid = $request->input('id');
+ $pid = $request->user()->profile_id;
+
+ $f = UserFilter::whereUserId($pid)
+ ->whereFilterableId($fid)
+ ->whereFilterableType('App\Profile')
+ ->whereFilterType('dm.mute')
+ ->firstOrFail();
+
+ $f->delete();
+
+ return [200];
+ }
+
+ public function remoteDeliver($dm)
+ {
+ $profile = $dm->author;
+ $url = $dm->recipient->sharedInbox ?? $dm->recipient->inbox_url;
+
+ $tags = [
+ [
+ 'type' => 'Mention',
+ 'href' => $dm->recipient->permalink(),
+ 'name' => $dm->recipient->emailUrl(),
+ ]
+ ];
+
+ $body = [
+ '@context' => [
+ 'https://w3id.org/security/v1',
+ 'https://www.w3.org/ns/activitystreams',
+ ],
+ 'id' => $dm->status->permalink(),
+ 'type' => 'Create',
+ 'actor' => $dm->status->profile->permalink(),
+ 'published' => $dm->status->created_at->toAtomString(),
+ 'to' => [$dm->recipient->permalink()],
+ 'cc' => [],
+ 'object' => [
+ 'id' => $dm->status->url(),
+ 'type' => 'Note',
+ 'summary' => null,
+ 'content' => $dm->status->rendered ?? $dm->status->caption,
+ 'inReplyTo' => null,
+ 'published' => $dm->status->created_at->toAtomString(),
+ 'url' => $dm->status->url(),
+ 'attributedTo' => $dm->status->profile->permalink(),
+ 'to' => [$dm->recipient->permalink()],
+ 'cc' => [],
+ 'sensitive' => (bool) $dm->status->is_nsfw,
+ 'attachment' => $dm->status->media()->orderBy('order')->get()->map(function ($media) {
+ return [
+ 'type' => $media->activityVerb(),
+ 'mediaType' => $media->mime,
+ 'url' => $media->url(),
+ 'name' => $media->caption,
+ ];
+ })->toArray(),
+ 'tag' => $tags,
+ ]
+ ];
+
+ DirectDeliverPipeline::dispatch($profile, $url, $body)->onQueue('high');
+ }
+
+ public function remoteDelete($dm)
+ {
+ $profile = $dm->author;
+ $url = $dm->recipient->sharedInbox ?? $dm->recipient->inbox_url;
+
+ $body = [
+ '@context' => [
+ 'https://www.w3.org/ns/activitystreams',
+ ],
+ 'id' => $dm->status->permalink('#delete'),
+ 'to' => [
+ 'https://www.w3.org/ns/activitystreams#Public'
+ ],
+ 'type' => 'Delete',
+ 'actor' => $dm->status->profile->permalink(),
+ 'object' => [
+ 'id' => $dm->status->url(),
+ 'type' => 'Tombstone'
+ ]
+ ];
+ DirectDeletePipeline::dispatch($profile, $url, $body)->onQueue('high');
+ }
}
From 71c148c61ea399993e4738ad30b600aa04da4544 Mon Sep 17 00:00:00 2001
From: Daniel Supernault
Date: Thu, 11 Jan 2024 05:46:02 -0700
Subject: [PATCH 12/16] 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');
+ }
}
From c7ed684a5c0488d3501a23e9c5101a35baa17af0 Mon Sep 17 00:00:00 2001
From: Daniel Supernault
Date: Thu, 11 Jan 2024 06:31:19 -0700
Subject: [PATCH 13/16] Update ParentalControlsController
---
app/Http/Controllers/ParentalControlsController.php | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/app/Http/Controllers/ParentalControlsController.php b/app/Http/Controllers/ParentalControlsController.php
index 373021ab9..24f7747ac 100644
--- a/app/Http/Controllers/ParentalControlsController.php
+++ b/app/Http/Controllers/ParentalControlsController.php
@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\ParentalControls;
use App\Models\UserRoles;
+use App\Profile;
use App\User;
use App\Http\Controllers\Auth\RegisterController;
use Illuminate\Auth\Events\Registered;
@@ -65,6 +66,11 @@ class ParentalControlsController extends Controller
$pc->save();
$roles = UserRoleService::mapActions($pc->child_id, $ff);
+ if(isset($roles['account-force-private'])) {
+ $c = Profile::whereUserId($pc->child_id)->first();
+ $c->is_private = $roles['account-force-private'];
+ $c->save();
+ }
UserRoles::whereUserId($pc->child_id)->update(['roles' => $roles]);
return redirect($pc->manageUrl() . '?permissions');
}
From db1b466792e2b6229b2632bf7ff8edba0989d8c9 Mon Sep 17 00:00:00 2001
From: Daniel Supernault
Date: Thu, 11 Jan 2024 06:45:32 -0700
Subject: [PATCH 14/16] Update instance config
---
config/instance.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/config/instance.php b/config/instance.php
index 5e173684c..03f666a79 100644
--- a/config/instance.php
+++ b/config/instance.php
@@ -132,11 +132,11 @@ return [
],
'parental_controls' => [
- 'enabled' => env('INSTANCE_PARENTAL_CONTROLS', true),
+ 'enabled' => env('INSTANCE_PARENTAL_CONTROLS', false),
'limits' => [
'respect_open_registration' => env('INSTANCE_PARENTAL_CONTROLS_RESPECT_OPENREG', true),
- 'max_children' => env('INSTANCE_PARENTAL_CONTROLS_MAX_CHILDREN', 10),
+ 'max_children' => env('INSTANCE_PARENTAL_CONTROLS_MAX_CHILDREN', 1),
'auto_verify_email' => true,
],
]
From c91f1c595aff9cccba6c58485f6f531fdec3d18b Mon Sep 17 00:00:00 2001
From: Daniel Supernault
Date: Thu, 11 Jan 2024 06:52:12 -0700
Subject: [PATCH 15/16] Update ParentalControlsController, prevent children
from adding accounts
---
app/Http/Controllers/ParentalControlsController.php | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/Http/Controllers/ParentalControlsController.php b/app/Http/Controllers/ParentalControlsController.php
index 24f7747ac..7d8625863 100644
--- a/app/Http/Controllers/ParentalControlsController.php
+++ b/app/Http/Controllers/ParentalControlsController.php
@@ -19,6 +19,7 @@ class ParentalControlsController extends Controller
{
if($authCheck) {
abort_unless($request->user(), 404);
+ abort_unless($request->user()->has_roles === 0, 404);
}
abort_unless(config('instance.parental_controls.enabled'), 404);
if(config_cache('pixelfed.open_registration') == false) {
From 85a612742d3c9f6994b9740adf545f4f2a28104c Mon Sep 17 00:00:00 2001
From: Daniel Supernault
Date: Thu, 11 Jan 2024 06:54:20 -0700
Subject: [PATCH 16/16] Update changelog
---
CHANGELOG.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 13baca035..06c1a9720 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@
- Added `app:hashtag-related-generate` command to generate related hashtags ([176b4ed7](https://github.com/pixelfed/pixelfed/commit/176b4ed7))
- Added Mutual Followers API endpoint ([33dbbe46](https://github.com/pixelfed/pixelfed/commit/33dbbe46))
- Added User Domain Blocks ([#4834](https://github.com/pixelfed/pixelfed/pull/4834)) ([fa0380ac](https://github.com/pixelfed/pixelfed/commit/fa0380ac))
+- Added Parental Controls ([#4862](https://github.com/pixelfed/pixelfed/pull/4862)) ([c91f1c59](https://github.com/pixelfed/pixelfed/commit/c91f1c59))
### Federation
- Update Privacy Settings, add support for Mastodon `indexable` search flag ([fc24630e](https://github.com/pixelfed/pixelfed/commit/fc24630e))