Merge branch 'staging' of github.com:pixelfed/pixelfed into jippi-fork

This commit is contained in:
Christian Winther 2024-02-19 13:31:29 +00:00
commit 5a9cfe1f2a
136 changed files with 5575 additions and 1668 deletions

7
.gitattributes vendored
View file

@ -3,3 +3,10 @@
*.scss linguist-vendored *.scss linguist-vendored
*.js linguist-vendored *.js linguist-vendored
CHANGELOG.md export-ignore CHANGELOG.md export-ignore
# Collapse diffs for generated files:
public/**/*.js text -diff
public/**/*.json text -diff
public/**/*.css text -diff
public/img/* binary -diff
public/fonts/* binary -diff

View file

@ -1,6 +1,19 @@
# Release Notes # Release Notes
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.12...dev) ## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.12...dev)
### Features
- Curated Onboarding ([8dac2caf](https://github.com/pixelfed/pixelfed/commit/8dac2caf))
### Updates
- Update Inbox, cast live filters to lowercase ([d835e0ad](https://github.com/pixelfed/pixelfed/commit/d835e0ad))
- Update federation config, increase default timeline days falloff to 90 days from 2 days. Fixes #4905 ([011834f4](https://github.com/pixelfed/pixelfed/commit/011834f4))
- Update cache config, use predis as default redis driver client ([ea6b1623](https://github.com/pixelfed/pixelfed/commit/ea6b1623))
- Update .gitattributes to collapse diffs on generated files ([ThisIsMissEm](https://github.com/pixelfed/pixelfed/commit/9978b2b9))
- Update api v1/v2 instance endpoints, bump mastoapi version from 2.7.2 to 3.5.3 ([545f7d5e](https://github.com/pixelfed/pixelfed/commit/545f7d5e))
- Update ApiV1Controller, implement better limit logic to gracefully handle requests with limits that exceed the max ([1f74a95d](https://github.com/pixelfed/pixelfed/commit/1f74a95d))
- ([](https://github.com/pixelfed/pixelfed/commit/)) - ([](https://github.com/pixelfed/pixelfed/commit/))
## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12) ## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12)
@ -9,7 +22,7 @@
- Autospam Live Filters - block remote activities based on comma separated keywords ([40b45b2a](https://github.com/pixelfed/pixelfed/commit/40b45b2a)) - Autospam Live Filters - block remote activities based on comma separated keywords ([40b45b2a](https://github.com/pixelfed/pixelfed/commit/40b45b2a))
- Added Software Update banner to admin home feeds ([b0fb1988](https://github.com/pixelfed/pixelfed/commit/b0fb1988)) - Added Software Update banner to admin home feeds ([b0fb1988](https://github.com/pixelfed/pixelfed/commit/b0fb1988))
### Updated ### Updates
- Update ApiV1Controller, fix network timeline ([0faf59e3](https://github.com/pixelfed/pixelfed/commit/0faf59e3)) - Update ApiV1Controller, fix network timeline ([0faf59e3](https://github.com/pixelfed/pixelfed/commit/0faf59e3))
- Update public/network timelines, fix non-redis response and fix reblogs in home feed ([8b4ac5cc](https://github.com/pixelfed/pixelfed/commit/8b4ac5cc)) - Update public/network timelines, fix non-redis response and fix reblogs in home feed ([8b4ac5cc](https://github.com/pixelfed/pixelfed/commit/8b4ac5cc))

View file

@ -17,269 +17,288 @@ use Illuminate\Support\Str;
trait AdminSettingsController trait AdminSettingsController
{ {
public function settings(Request $request) public function settings(Request $request)
{ {
$cloud_storage = ConfigCacheService::get('pixelfed.cloud_storage'); $cloud_storage = ConfigCacheService::get('pixelfed.cloud_storage');
$cloud_disk = config('filesystems.cloud'); $cloud_disk = config('filesystems.cloud');
$cloud_ready = !empty(config('filesystems.disks.' . $cloud_disk . '.key')) && !empty(config('filesystems.disks.' . $cloud_disk . '.secret')); $cloud_ready = !empty(config('filesystems.disks.' . $cloud_disk . '.key')) && !empty(config('filesystems.disks.' . $cloud_disk . '.secret'));
$types = explode(',', ConfigCacheService::get('pixelfed.media_types')); $types = explode(',', ConfigCacheService::get('pixelfed.media_types'));
$rules = ConfigCacheService::get('app.rules') ? json_decode(ConfigCacheService::get('app.rules'), true) : null; $rules = ConfigCacheService::get('app.rules') ? json_decode(ConfigCacheService::get('app.rules'), true) : null;
$jpeg = in_array('image/jpg', $types) || in_array('image/jpeg', $types); $jpeg = in_array('image/jpg', $types) || in_array('image/jpeg', $types);
$png = in_array('image/png', $types); $png = in_array('image/png', $types);
$gif = in_array('image/gif', $types); $gif = in_array('image/gif', $types);
$mp4 = in_array('video/mp4', $types); $mp4 = in_array('video/mp4', $types);
$webp = in_array('image/webp', $types); $webp = in_array('image/webp', $types);
$availableAdmins = User::whereIsAdmin(true)->get(); $availableAdmins = User::whereIsAdmin(true)->get();
$currentAdmin = config_cache('instance.admin.pid') ? AccountService::get(config_cache('instance.admin.pid'), true) : null; $currentAdmin = config_cache('instance.admin.pid') ? AccountService::get(config_cache('instance.admin.pid'), true) : null;
$openReg = (bool) config_cache('pixelfed.open_registration');
$curOnboarding = (bool) config_cache('instance.curated_registration.enabled');
$regState = $openReg ? 'open' : ($curOnboarding ? 'filtered' : 'closed');
// $system = [ return view('admin.settings.home', compact(
// 'permissions' => is_writable(base_path('storage')) && is_writable(base_path('bootstrap')), 'jpeg',
// 'max_upload_size' => ini_get('post_max_size'), 'png',
// 'image_driver' => config('image.driver'), 'gif',
// 'image_driver_loaded' => extension_loaded(config('image.driver')) 'mp4',
// ]; 'webp',
'rules',
'cloud_storage',
'cloud_disk',
'cloud_ready',
'availableAdmins',
'currentAdmin',
'regState'
));
}
return view('admin.settings.home', compact( public function settingsHomeStore(Request $request)
'jpeg', {
'png', $this->validate($request, [
'gif', 'name' => 'nullable|string',
'mp4', 'short_description' => 'nullable',
'webp', 'long_description' => 'nullable',
'rules', 'max_photo_size' => 'nullable|integer|min:1',
'cloud_storage', 'max_album_length' => 'nullable|integer|min:1|max:100',
'cloud_disk', 'image_quality' => 'nullable|integer|min:1|max:100',
'cloud_ready', 'type_jpeg' => 'nullable',
'availableAdmins', 'type_png' => 'nullable',
'currentAdmin' 'type_gif' => 'nullable',
// 'system' 'type_mp4' => 'nullable',
)); 'type_webp' => 'nullable',
} 'admin_account_id' => 'nullable',
'regs' => 'required|in:open,filtered,closed'
]);
public function settingsHomeStore(Request $request) $orb = false;
{ $cob = false;
$this->validate($request, [ switch($request->input('regs')) {
'name' => 'nullable|string', case 'open':
'short_description' => 'nullable', $orb = true;
'long_description' => 'nullable', $cob = false;
'max_photo_size' => 'nullable|integer|min:1', break;
'max_album_length' => 'nullable|integer|min:1|max:100',
'image_quality' => 'nullable|integer|min:1|max:100',
'type_jpeg' => 'nullable',
'type_png' => 'nullable',
'type_gif' => 'nullable',
'type_mp4' => 'nullable',
'type_webp' => 'nullable',
'admin_account_id' => 'nullable',
]);
if($request->filled('admin_account_id')) { case 'filtered':
ConfigCacheService::put('instance.admin.pid', $request->admin_account_id); $orb = false;
Cache::forget('api:v1:instance-data:contact'); $cob = true;
Cache::forget('api:v1:instance-data-response-v1'); break;
}
if($request->filled('rule_delete')) {
$index = (int) $request->input('rule_delete');
$rules = ConfigCacheService::get('app.rules');
$json = json_decode($rules, true);
if(!$rules || empty($json)) {
return;
}
unset($json[$index]);
$json = json_encode(array_values($json));
ConfigCacheService::put('app.rules', $json);
Cache::forget('api:v1:instance-data:rules');
Cache::forget('api:v1:instance-data-response-v1');
return 200;
}
$media_types = explode(',', config_cache('pixelfed.media_types')); case 'closed':
$media_types_original = $media_types; $orb = false;
$cob = false;
break;
}
$mimes = [ ConfigCacheService::put('pixelfed.open_registration', (bool) $orb);
'type_jpeg' => 'image/jpeg', ConfigCacheService::put('instance.curated_registration.enabled', (bool) $cob);
'type_png' => 'image/png',
'type_gif' => 'image/gif',
'type_mp4' => 'video/mp4',
'type_webp' => 'image/webp',
];
foreach ($mimes as $key => $value) { if($request->filled('admin_account_id')) {
if($request->input($key) == 'on') { ConfigCacheService::put('instance.admin.pid', $request->admin_account_id);
if(!in_array($value, $media_types)) { Cache::forget('api:v1:instance-data:contact');
array_push($media_types, $value); Cache::forget('api:v1:instance-data-response-v1');
} }
} else { if($request->filled('rule_delete')) {
$media_types = array_diff($media_types, [$value]); $index = (int) $request->input('rule_delete');
} $rules = ConfigCacheService::get('app.rules');
} $json = json_decode($rules, true);
if(!$rules || empty($json)) {
return;
}
unset($json[$index]);
$json = json_encode(array_values($json));
ConfigCacheService::put('app.rules', $json);
Cache::forget('api:v1:instance-data:rules');
Cache::forget('api:v1:instance-data-response-v1');
return 200;
}
if($media_types !== $media_types_original) { $media_types = explode(',', config_cache('pixelfed.media_types'));
ConfigCacheService::put('pixelfed.media_types', implode(',', array_unique($media_types))); $media_types_original = $media_types;
}
$keys = [ $mimes = [
'name' => 'app.name', 'type_jpeg' => 'image/jpeg',
'short_description' => 'app.short_description', 'type_png' => 'image/png',
'long_description' => 'app.description', 'type_gif' => 'image/gif',
'max_photo_size' => 'pixelfed.max_photo_size', 'type_mp4' => 'video/mp4',
'max_album_length' => 'pixelfed.max_album_length', 'type_webp' => 'image/webp',
'image_quality' => 'pixelfed.image_quality', ];
'account_limit' => 'pixelfed.max_account_size',
'custom_css' => 'uikit.custom.css',
'custom_js' => 'uikit.custom.js',
'about_title' => 'about.title'
];
foreach ($keys as $key => $value) { foreach ($mimes as $key => $value) {
$cc = ConfigCache::whereK($value)->first(); if($request->input($key) == 'on') {
$val = $request->input($key); if(!in_array($value, $media_types)) {
if($cc && $cc->v != $val) { array_push($media_types, $value);
ConfigCacheService::put($value, $val); }
} else if(!empty($val)) { } else {
ConfigCacheService::put($value, $val); $media_types = array_diff($media_types, [$value]);
} }
} }
$bools = [ if($media_types !== $media_types_original) {
'activitypub' => 'federation.activitypub.enabled', ConfigCacheService::put('pixelfed.media_types', implode(',', array_unique($media_types)));
'open_registration' => 'pixelfed.open_registration', }
'mobile_apis' => 'pixelfed.oauth_enabled',
'stories' => 'instance.stories.enabled',
'ig_import' => 'pixelfed.import.instagram.enabled',
'spam_detection' => 'pixelfed.bouncer.enabled',
'require_email_verification' => 'pixelfed.enforce_email_verification',
'enforce_account_limit' => 'pixelfed.enforce_account_limit',
'show_custom_css' => 'uikit.show_custom.css',
'show_custom_js' => 'uikit.show_custom.js',
'cloud_storage' => 'pixelfed.cloud_storage',
'account_autofollow' => 'account.autofollow',
'show_directory' => 'instance.landing.show_directory',
'show_explore_feed' => 'instance.landing.show_explore',
];
foreach ($bools as $key => $value) { $keys = [
$active = $request->input($key) == 'on'; 'name' => 'app.name',
'short_description' => 'app.short_description',
'long_description' => 'app.description',
'max_photo_size' => 'pixelfed.max_photo_size',
'max_album_length' => 'pixelfed.max_album_length',
'image_quality' => 'pixelfed.image_quality',
'account_limit' => 'pixelfed.max_account_size',
'custom_css' => 'uikit.custom.css',
'custom_js' => 'uikit.custom.js',
'about_title' => 'about.title'
];
if($key == 'activitypub' && $active && !InstanceActor::exists()) { foreach ($keys as $key => $value) {
Artisan::call('instance:actor'); $cc = ConfigCache::whereK($value)->first();
} $val = $request->input($key);
if($cc && $cc->v != $val) {
ConfigCacheService::put($value, $val);
} else if(!empty($val)) {
ConfigCacheService::put($value, $val);
}
}
if( $key == 'mobile_apis' && $bools = [
$active && 'activitypub' => 'federation.activitypub.enabled',
!file_exists(storage_path('oauth-public.key')) && // 'open_registration' => 'pixelfed.open_registration',
!file_exists(storage_path('oauth-private.key')) 'mobile_apis' => 'pixelfed.oauth_enabled',
) { 'stories' => 'instance.stories.enabled',
Artisan::call('passport:keys'); 'ig_import' => 'pixelfed.import.instagram.enabled',
Artisan::call('route:cache'); 'spam_detection' => 'pixelfed.bouncer.enabled',
} 'require_email_verification' => 'pixelfed.enforce_email_verification',
'enforce_account_limit' => 'pixelfed.enforce_account_limit',
'show_custom_css' => 'uikit.show_custom.css',
'show_custom_js' => 'uikit.show_custom.js',
'cloud_storage' => 'pixelfed.cloud_storage',
'account_autofollow' => 'account.autofollow',
'show_directory' => 'instance.landing.show_directory',
'show_explore_feed' => 'instance.landing.show_explore',
];
if(config_cache($value) !== $active) { foreach ($bools as $key => $value) {
ConfigCacheService::put($value, (bool) $active); $active = $request->input($key) == 'on';
}
}
if($request->filled('new_rule')) { if($key == 'activitypub' && $active && !InstanceActor::exists()) {
$rules = ConfigCacheService::get('app.rules'); Artisan::call('instance:actor');
$val = $request->input('new_rule'); }
if(!$rules) {
ConfigCacheService::put('app.rules', json_encode([$val]));
} else {
$json = json_decode($rules, true);
$json[] = $val;
ConfigCacheService::put('app.rules', json_encode(array_values($json)));
}
Cache::forget('api:v1:instance-data:rules');
Cache::forget('api:v1:instance-data-response-v1');
}
if($request->filled('account_autofollow_usernames')) { if( $key == 'mobile_apis' &&
$usernames = explode(',', $request->input('account_autofollow_usernames')); $active &&
$names = []; !file_exists(storage_path('oauth-public.key')) &&
!file_exists(storage_path('oauth-private.key'))
) {
Artisan::call('passport:keys');
Artisan::call('route:cache');
}
foreach($usernames as $n) { if(config_cache($value) !== $active) {
$p = Profile::whereUsername($n)->first(); ConfigCacheService::put($value, (bool) $active);
if(!$p) { }
continue; }
}
array_push($names, $p->username);
}
ConfigCacheService::put('account.autofollow_usernames', implode(',', $names)); if($request->filled('new_rule')) {
} $rules = ConfigCacheService::get('app.rules');
$val = $request->input('new_rule');
if(!$rules) {
ConfigCacheService::put('app.rules', json_encode([$val]));
} else {
$json = json_decode($rules, true);
$json[] = $val;
ConfigCacheService::put('app.rules', json_encode(array_values($json)));
}
Cache::forget('api:v1:instance-data:rules');
Cache::forget('api:v1:instance-data-response-v1');
}
Cache::forget(Config::CACHE_KEY); if($request->filled('account_autofollow_usernames')) {
$usernames = explode(',', $request->input('account_autofollow_usernames'));
$names = [];
return redirect('/i/admin/settings')->with('status', 'Successfully updated settings!'); foreach($usernames as $n) {
} $p = Profile::whereUsername($n)->first();
if(!$p) {
continue;
}
array_push($names, $p->username);
}
public function settingsBackups(Request $request) ConfigCacheService::put('account.autofollow_usernames', implode(',', $names));
{ }
$path = storage_path('app/'.config('app.name'));
$files = is_dir($path) ? new \DirectoryIterator($path) : [];
return view('admin.settings.backups', compact('files'));
}
public function settingsMaintenance(Request $request) Cache::forget(Config::CACHE_KEY);
{
return view('admin.settings.maintenance');
}
public function settingsStorage(Request $request) return redirect('/i/admin/settings')->with('status', 'Successfully updated settings!');
{ }
$storage = [];
return view('admin.settings.storage', compact('storage'));
}
public function settingsFeatures(Request $request) public function settingsBackups(Request $request)
{ {
return view('admin.settings.features'); $path = storage_path('app/'.config('app.name'));
} $files = is_dir($path) ? new \DirectoryIterator($path) : [];
return view('admin.settings.backups', compact('files'));
}
public function settingsPages(Request $request) public function settingsMaintenance(Request $request)
{ {
$pages = Page::orderByDesc('updated_at')->paginate(10); return view('admin.settings.maintenance');
return view('admin.pages.home', compact('pages')); }
}
public function settingsPageEdit(Request $request) public function settingsStorage(Request $request)
{ {
return view('admin.pages.edit'); $storage = [];
} return view('admin.settings.storage', compact('storage'));
}
public function settingsSystem(Request $request) public function settingsFeatures(Request $request)
{ {
$sys = [ return view('admin.settings.features');
'pixelfed' => config('pixelfed.version'), }
'php' => phpversion(),
'laravel' => app()->version(),
];
switch (config('database.default')) {
case 'pgsql':
$exp = DB::raw('select version();');
$expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
$sys['database'] = [
'name' => 'Postgres',
'version' => explode(' ', DB::select($expQuery)[0]->version)[1]
];
break;
case 'mysql': public function settingsPages(Request $request)
$exp = DB::raw('select version()'); {
$expQuery = $exp->getValue(DB::connection()->getQueryGrammar()); $pages = Page::orderByDesc('updated_at')->paginate(10);
$sys['database'] = [ return view('admin.pages.home', compact('pages'));
'name' => 'MySQL', }
'version' => DB::select($expQuery)[0]->{'version()'}
];
break;
default: public function settingsPageEdit(Request $request)
$sys['database'] = [ {
'name' => 'Unknown', return view('admin.pages.edit');
'version' => '?' }
];
break; public function settingsSystem(Request $request)
} {
return view('admin.settings.system', compact('sys')); $sys = [
} 'pixelfed' => config('pixelfed.version'),
'php' => phpversion(),
'laravel' => app()->version(),
];
switch (config('database.default')) {
case 'pgsql':
$exp = DB::raw('select version();');
$expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
$sys['database'] = [
'name' => 'Postgres',
'version' => explode(' ', DB::select($expQuery)[0]->version)[1]
];
break;
case 'mysql':
$exp = DB::raw('select version()');
$expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
$sys['database'] = [
'name' => 'MySQL',
'version' => DB::select($expQuery)[0]->{'version()'}
];
break;
default:
$sys['database'] = [
'name' => 'Unknown',
'version' => '?'
];
break;
}
return view('admin.settings.system', compact('sys'));
}
} }

View file

@ -0,0 +1,226 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\CuratedRegister;
use App\Models\CuratedRegisterActivity;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Mail;
use App\Mail\CuratedRegisterRequestDetailsFromUser;
use App\Mail\CuratedRegisterAcceptUser;
use App\Mail\CuratedRegisterRejectUser;
use App\User;
class AdminCuratedRegisterController extends Controller
{
public function __construct()
{
$this->middleware(['auth','admin']);
}
public function index(Request $request)
{
$this->validate($request, [
'filter' => 'sometimes|in:open,all,awaiting,approved,rejected'
]);
$filter = $request->input('filter', 'open');
$records = CuratedRegister::when($filter, function($q, $filter) {
if($filter === 'open') {
return $q->where('is_rejected', false)
->whereNotNull('email_verified_at')
->whereIsClosed(false);
} else if($filter === 'all') {
return $q;
} elseif ($filter === 'awaiting') {
return $q->whereIsClosed(false)
->whereNull('is_rejected')
->whereNull('is_approved');
} elseif ($filter === 'approved') {
return $q->whereIsClosed(true)->whereIsApproved(true);
} elseif ($filter === 'rejected') {
return $q->whereIsClosed(true)->whereIsRejected(true);
}
})
->latest()
->paginate(10);
return view('admin.curated-register.index', compact('records', 'filter'));
}
public function show(Request $request, $id)
{
$record = CuratedRegister::findOrFail($id);
return view('admin.curated-register.show', compact('record'));
}
public function apiActivityLog(Request $request, $id)
{
$record = CuratedRegister::findOrFail($id);
$res = collect([
[
'id' => 1,
'action' => 'created',
'title' => 'Onboarding application created',
'message' => null,
'link' => null,
'timestamp' => $record->created_at,
]
]);
if($record->email_verified_at) {
$res->push([
'id' => 3,
'action' => 'email_verified_at',
'title' => 'Applicant successfully verified email address',
'message' => null,
'link' => null,
'timestamp' => $record->email_verified_at,
]);
}
$activities = CuratedRegisterActivity::whereRegisterId($record->id)->get();
$idx = 4;
$userResponses = collect([]);
foreach($activities as $activity) {
$idx++;
if($activity->from_user) {
$userResponses->push($activity);
continue;
}
$res->push([
'id' => $idx,
'aid' => $activity->id,
'action' => $activity->type,
'title' => $activity->from_admin ? 'Admin requested info' : 'User responded',
'message' => $activity->message,
'link' => $activity->adminReviewUrl(),
'timestamp' => $activity->created_at,
]);
}
foreach($userResponses as $ur) {
$res = $res->map(function($r) use($ur) {
if(!isset($r['aid'])) {
return $r;
}
if($ur->reply_to_id === $r['aid']) {
$r['user_response'] = $ur;
return $r;
}
return $r;
});
}
if($record->is_approved) {
$idx++;
$res->push([
'id' => $idx,
'action' => 'approved',
'title' => 'Application Approved',
'message' => null,
'link' => null,
'timestamp' => $record->action_taken_at,
]);
} else if ($record->is_rejected) {
$idx++;
$res->push([
'id' => $idx,
'action' => 'rejected',
'title' => 'Application Rejected',
'message' => null,
'link' => null,
'timestamp' => $record->action_taken_at,
]);
}
return $res->reverse()->values();
}
public function apiMessagePreviewStore(Request $request, $id)
{
$record = CuratedRegister::findOrFail($id);
return $request->all();
}
public function apiMessageSendStore(Request $request, $id)
{
$this->validate($request, [
'message' => 'required|string|min:5|max:1000'
]);
$record = CuratedRegister::findOrFail($id);
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
$activity = new CuratedRegisterActivity;
$activity->register_id = $record->id;
$activity->admin_id = $request->user()->id;
$activity->secret_code = Str::random(32);
$activity->type = 'request_details';
$activity->from_admin = true;
$activity->message = $request->input('message');
$activity->save();
$record->is_awaiting_more_info = true;
$record->save();
Mail::to($record->email)->send(new CuratedRegisterRequestDetailsFromUser($record, $activity));
return $request->all();
}
public function previewDetailsMessageShow(Request $request, $id)
{
$record = CuratedRegister::findOrFail($id);
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
$activity = new CuratedRegisterActivity;
$activity->message = $request->input('message');
return new \App\Mail\CuratedRegisterRequestDetailsFromUser($record, $activity);
}
public function previewMessageShow(Request $request, $id)
{
$record = CuratedRegister::findOrFail($id);
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
$record->message = $request->input('message');
return new \App\Mail\CuratedRegisterSendMessage($record);
}
public function apiHandleReject(Request $request, $id)
{
$this->validate($request, [
'action' => 'required|in:reject-email,reject-silent'
]);
$action = $request->input('action');
$record = CuratedRegister::findOrFail($id);
abort_if($record->email_verified_at === null, 400, 'Cannot reject an unverified email');
$record->is_rejected = true;
$record->is_closed = true;
$record->action_taken_at = now();
$record->save();
if($action === 'reject-email') {
Mail::to($record->email)->send(new CuratedRegisterRejectUser($record));
}
return [200];
}
public function apiHandleApprove(Request $request, $id)
{
$record = CuratedRegister::findOrFail($id);
abort_if($record->email_verified_at === null, 400, 'Cannot reject an unverified email');
$record->is_approved = true;
$record->is_closed = true;
$record->action_taken_at = now();
$record->save();
$user = User::create([
'name' => $record->username,
'username' => $record->username,
'email' => $record->email,
'password' => $record->password,
'app_register_ip' => $record->ip_address,
'email_verified_at' => now(),
'register_source' => 'cur_onboarding'
]);
Mail::to($record->email)->send(new CuratedRegisterAcceptUser($record));
return [200];
}
}

View file

@ -496,9 +496,12 @@ class ApiV1Controller extends Controller
abort_if(!$account, 404); abort_if(!$account, 404);
$pid = $request->user()->profile_id; $pid = $request->user()->profile_id;
$this->validate($request, [ $this->validate($request, [
'limit' => 'sometimes|integer|min:1|max:80' 'limit' => 'sometimes|integer|min:1'
]); ]);
$limit = $request->input('limit', 10); $limit = $request->input('limit', 10);
if($limit > 80) {
$limit = 80;
}
$napi = $request->has(self::PF_API_ENTITY_KEY); $napi = $request->has(self::PF_API_ENTITY_KEY);
if($account && strpos($account['acct'], '@') != -1) { if($account && strpos($account['acct'], '@') != -1) {
@ -594,9 +597,12 @@ class ApiV1Controller extends Controller
abort_if(!$account, 404); abort_if(!$account, 404);
$pid = $request->user()->profile_id; $pid = $request->user()->profile_id;
$this->validate($request, [ $this->validate($request, [
'limit' => 'sometimes|integer|min:1|max:80' 'limit' => 'sometimes|integer|min:1'
]); ]);
$limit = $request->input('limit', 10); $limit = $request->input('limit', 10);
if($limit > 80) {
$limit = 80;
}
$napi = $request->has(self::PF_API_ENTITY_KEY); $napi = $request->has(self::PF_API_ENTITY_KEY);
if($account && strpos($account['acct'], '@') != -1) { if($account && strpos($account['acct'], '@') != -1) {
@ -698,7 +704,7 @@ class ApiV1Controller extends Controller
'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
'since_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, 'since_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
'limit' => 'nullable|integer|min:1|max:100' 'limit' => 'nullable|integer|min:1'
]); ]);
$napi = $request->has(self::PF_API_ENTITY_KEY); $napi = $request->has(self::PF_API_ENTITY_KEY);
@ -713,7 +719,10 @@ class ApiV1Controller extends Controller
abort_if(in_array($domain, InstanceService::getBannedDomains()), 404); abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
} }
$limit = $request->limit ?? 20; $limit = $request->input('limit') ?? 20;
if($limit > 40) {
$limit = 40;
}
$max_id = $request->max_id; $max_id = $request->max_id;
$min_id = $request->min_id; $min_id = $request->min_id;
@ -959,12 +968,16 @@ class ApiV1Controller extends Controller
abort_if(!$request->user(), 403); abort_if(!$request->user(), 403);
$this->validate($request, [ $this->validate($request, [
'id' => 'required|array|min:1|max:20', 'id' => 'required|array|min:1',
'id.*' => 'required|integer|min:1|max:' . PHP_INT_MAX 'id.*' => 'required|integer|min:1|max:' . PHP_INT_MAX
]); ]);
$ids = $request->input('id');
if(count($ids) > 20) {
$ids = collect($ids)->take(20)->toArray();
}
$napi = $request->has(self::PF_API_ENTITY_KEY); $napi = $request->has(self::PF_API_ENTITY_KEY);
$pid = $request->user()->profile_id ?? $request->user()->profile->id; $pid = $request->user()->profile_id ?? $request->user()->profile->id;
$res = collect($request->input('id')) $res = collect($ids)
->filter(function($id) use($pid) { ->filter(function($id) use($pid) {
return intval($id) !== intval($pid); return intval($id) !== intval($pid);
}) })
@ -989,8 +1002,8 @@ class ApiV1Controller extends Controller
abort_unless($request->user()->tokenCan('read'), 403); abort_unless($request->user()->tokenCan('read'), 403);
$this->validate($request, [ $this->validate($request, [
'q' => 'required|string|min:1|max:255', 'q' => 'required|string|min:1|max:30',
'limit' => 'nullable|integer|min:1|max:40', 'limit' => 'nullable|integer|min:1',
'resolve' => 'nullable' 'resolve' => 'nullable'
]); ]);
@ -1000,22 +1013,23 @@ class ApiV1Controller extends Controller
AccountService::setLastActive($user->id); AccountService::setLastActive($user->id);
$query = $request->input('q'); $query = $request->input('q');
$limit = $request->input('limit') ?? 20; $limit = $request->input('limit') ?? 20;
$resolve = (bool) $request->input('resolve', false); if($limit > 20) {
$q = '%' . $query . '%'; $limit = 20;
}
$resolve = $request->boolean('resolve', false);
$q = $query . '%';
$profiles = Cache::remember('api:v1:accounts:search:' . sha1($query) . ':limit:' . $limit, 86400, function() use($q, $limit) { $profiles = Profile::where('username', 'like', $q)
return Profile::whereNull('status') ->orderByDesc('followers_count')
->where('username', 'like', $q) ->limit($limit)
->orWhere('name', 'like', $q) ->pluck('id')
->limit($limit) ->map(function($id) {
->pluck('id') return AccountService::getMastodon($id);
->map(function($id) { })
return AccountService::getMastodon($id); ->filter(function($account) {
}) return $account && isset($account['id']);
->filter(function($account) { })
return $account && isset($account['id']); ->values();
});
});
return $this->json($profiles); return $this->json($profiles);
} }
@ -1033,20 +1047,25 @@ class ApiV1Controller extends Controller
abort_unless($request->user()->tokenCan('read'), 403); abort_unless($request->user()->tokenCan('read'), 403);
$this->validate($request, [ $this->validate($request, [
'limit' => 'nullable|integer|min:1|max:40', 'limit' => 'sometimes|integer|min:1',
'page' => 'nullable|integer|min:1|max:10' 'page' => 'sometimes|integer|min:1'
]); ]);
$user = $request->user(); $user = $request->user();
$limit = $request->input('limit') ?? 40; $limit = $request->input('limit') ?? 40;
if($limit > 80) {
$limit = 80;
}
$blocked = UserFilter::select('filterable_id','filterable_type','filter_type','user_id') $blocks = UserFilter::select('filterable_id','filterable_type','filter_type','user_id')
->whereUserId($user->profile_id) ->whereUserId($user->profile_id)
->whereFilterableType('App\Profile') ->whereFilterableType('App\Profile')
->whereFilterType('block') ->whereFilterType('block')
->orderByDesc('id') ->orderByDesc('id')
->simplePaginate($limit) ->simplePaginate($limit)
->pluck('filterable_id') ->withQueryString();
$res = $blocks->pluck('filterable_id')
->map(function($id) { ->map(function($id) {
return AccountService::get($id, true); return AccountService::get($id, true);
}) })
@ -1055,7 +1074,23 @@ class ApiV1Controller extends Controller
}) })
->values(); ->values();
return $this->json($blocked); $baseUrl = config('app.url') . '/api/v1/blocks?limit=' . $limit . '&';
$next = $blocks->nextPageUrl();
$prev = $blocks->previousPageUrl();
if($next && !$prev) {
$link = '<'.$next.'>; rel="next"';
}
if(!$next && $prev) {
$link = '<'.$prev.'>; rel="prev"';
}
if($next && $prev) {
$link = '<'.$next.'>; rel="next",<'.$prev.'>; rel="prev"';
}
$headers = isset($link) ? ['Link' => $link] : [];
return $this->json($res, 200, $headers);
} }
/** /**
@ -1247,13 +1282,16 @@ class ApiV1Controller extends Controller
abort_unless($request->user()->tokenCan('read'), 403); abort_unless($request->user()->tokenCan('read'), 403);
$this->validate($request, [ $this->validate($request, [
'limit' => 'sometimes|integer|min:1|max:40' 'limit' => 'sometimes|integer|min:1'
]); ]);
$user = $request->user(); $user = $request->user();
$maxId = $request->input('max_id'); $maxId = $request->input('max_id');
$minId = $request->input('min_id'); $minId = $request->input('min_id');
$limit = $request->input('limit') ?? 10; $limit = $request->input('limit') ?? 10;
if($limit > 40) {
$limit = 40;
}
$res = Like::whereProfileId($user->profile_id) $res = Like::whereProfileId($user->profile_id)
->when($maxId, function($q, $maxId) { ->when($maxId, function($q, $maxId) {
@ -1612,15 +1650,15 @@ class ApiV1Controller extends Controller
'short_description' => config_cache('app.short_description'), 'short_description' => config_cache('app.short_description'),
'description' => config_cache('app.description'), 'description' => config_cache('app.description'),
'email' => config('instance.email'), 'email' => config('instance.email'),
'version' => '2.7.2 (compatible; Pixelfed ' . config('pixelfed.version') .')', 'version' => '3.5.3 (compatible; Pixelfed ' . config('pixelfed.version') .')',
'urls' => [ 'urls' => [
'streaming_api' => 'wss://' . config('pixelfed.domain.app') 'streaming_api' => null,
], ],
'stats' => $stats, 'stats' => $stats,
'thumbnail' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')), 'thumbnail' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
'languages' => [config('app.locale')], 'languages' => [config('app.locale')],
'registrations' => (bool) config_cache('pixelfed.open_registration'), 'registrations' => (bool) config_cache('pixelfed.open_registration'),
'approval_required' => false, 'approval_required' => (bool) config_cache('instance.curated_registration.enabled'),
'contact_account' => $contact, 'contact_account' => $contact,
'rules' => $rules, 'rules' => $rules,
'configuration' => [ 'configuration' => [
@ -2049,18 +2087,23 @@ class ApiV1Controller extends Controller
abort_unless($request->user()->tokenCan('read'), 403); abort_unless($request->user()->tokenCan('read'), 403);
$this->validate($request, [ $this->validate($request, [
'limit' => 'nullable|integer|min:1|max:40' 'limit' => 'sometimes|integer|min:1'
]); ]);
$user = $request->user(); $user = $request->user();
$limit = $request->input('limit', 40); $limit = $request->input('limit', 40);
if($limit > 80) {
$limit = 80;
}
$mutes = UserFilter::whereUserId($user->profile_id) $mutes = UserFilter::whereUserId($user->profile_id)
->whereFilterableType('App\Profile') ->whereFilterableType('App\Profile')
->whereFilterType('mute') ->whereFilterType('mute')
->orderByDesc('id') ->orderByDesc('id')
->simplePaginate($limit) ->simplePaginate($limit)
->pluck('filterable_id') ->withQueryString();
$res = $mutes->pluck('filterable_id')
->map(function($id) { ->map(function($id) {
return AccountService::get($id, true); return AccountService::get($id, true);
}) })
@ -2069,7 +2112,23 @@ class ApiV1Controller extends Controller
}) })
->values(); ->values();
return $this->json($mutes); $baseUrl = config('app.url') . '/api/v1/mutes?limit=' . $limit . '&';
$next = $mutes->nextPageUrl();
$prev = $mutes->previousPageUrl();
if($next && !$prev) {
$link = '<'.$next.'>; rel="next"';
}
if(!$next && $prev) {
$link = '<'.$prev.'>; rel="prev"';
}
if($next && $prev) {
$link = '<'.$next.'>; rel="next",<'.$prev.'>; rel="prev"';
}
$headers = isset($link) ? ['Link' => $link] : [];
return $this->json($res, 200, $headers);
} }
/** /**
@ -2181,7 +2240,7 @@ class ApiV1Controller extends Controller
abort_unless($request->user()->tokenCan('read'), 403); abort_unless($request->user()->tokenCan('read'), 403);
$this->validate($request, [ $this->validate($request, [
'limit' => 'nullable|integer|min:1|max:100', 'limit' => 'sometimes|integer|min:1',
'min_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX, 'min_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX,
'max_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX, 'max_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX,
'since_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX, 'since_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX,
@ -2191,6 +2250,9 @@ class ApiV1Controller extends Controller
$pid = $request->user()->profile_id; $pid = $request->user()->profile_id;
$limit = $request->input('limit', 20); $limit = $request->input('limit', 20);
if($limit > 40) {
$limit = 40;
}
$since = $request->input('since_id'); $since = $request->input('since_id');
$min = $request->input('min_id'); $min = $request->input('min_id');
@ -2200,6 +2262,10 @@ class ApiV1Controller extends Controller
$min = 1; $min = 1;
} }
if($since) {
$min = $since + 1;
}
$types = $request->input('types'); $types = $request->input('types');
$maxId = null; $maxId = null;
@ -2261,7 +2327,7 @@ class ApiV1Controller extends Controller
'page' => 'sometimes|integer|max:40', 'page' => 'sometimes|integer|max:40',
'min_id' => 'sometimes|integer|min:0|max:' . PHP_INT_MAX, 'min_id' => 'sometimes|integer|min:0|max:' . PHP_INT_MAX,
'max_id' => 'sometimes|integer|min:0|max:' . PHP_INT_MAX, 'max_id' => 'sometimes|integer|min:0|max:' . PHP_INT_MAX,
'limit' => 'sometimes|integer|min:1|max:40', 'limit' => 'sometimes|integer|min:1',
'include_reblogs' => 'sometimes', 'include_reblogs' => 'sometimes',
]); ]);
@ -2270,6 +2336,9 @@ class ApiV1Controller extends Controller
$min = $request->input('min_id'); $min = $request->input('min_id');
$max = $request->input('max_id'); $max = $request->input('max_id');
$limit = $request->input('limit') ?? 20; $limit = $request->input('limit') ?? 20;
if($limit > 40) {
$limit = 40;
}
$pid = $request->user()->profile_id; $pid = $request->user()->profile_id;
$includeReblogs = $request->filled('include_reblogs') ? $request->boolean('include_reblogs') : false; $includeReblogs = $request->filled('include_reblogs') ? $request->boolean('include_reblogs') : false;
$nullFields = $includeReblogs ? $nullFields = $includeReblogs ?
@ -2515,7 +2584,7 @@ class ApiV1Controller extends Controller
$this->validate($request,[ $this->validate($request,[
'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
'limit' => 'nullable|integer|max:100', 'limit' => 'sometimes|integer|min:1',
'remote' => 'sometimes', 'remote' => 'sometimes',
'local' => 'sometimes' 'local' => 'sometimes'
]); ]);
@ -2525,6 +2594,9 @@ class ApiV1Controller extends Controller
$max = $request->input('max_id'); $max = $request->input('max_id');
$minOrMax = $request->anyFilled(['max_id', 'min_id']); $minOrMax = $request->anyFilled(['max_id', 'min_id']);
$limit = $request->input('limit') ?? 20; $limit = $request->input('limit') ?? 20;
if($limit > 40) {
$limit = 40;
}
$user = $request->user(); $user = $request->user();
$remote = $request->has('remote'); $remote = $request->has('remote');
@ -3043,10 +3115,13 @@ class ApiV1Controller extends Controller
abort_unless($request->user()->tokenCan('read'), 403); abort_unless($request->user()->tokenCan('read'), 403);
$this->validate($request, [ $this->validate($request, [
'limit' => 'nullable|integer|min:1|max:80' 'limit' => 'sometimes|integer|min:1'
]); ]);
$limit = $request->input('limit', 10); $limit = $request->input('limit', 40);
if($limit > 80) {
$limit = 80;
}
$user = $request->user(); $user = $request->user();
$pid = $user->profile_id; $pid = $user->profile_id;
$status = Status::findOrFail($id); $status = Status::findOrFail($id);
@ -3485,7 +3560,7 @@ class ApiV1Controller extends Controller
'page' => 'nullable|integer|max:40', 'page' => 'nullable|integer|max:40',
'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
'limit' => 'nullable|integer|max:100', 'limit' => 'sometimes|integer|min:1',
'only_media' => 'sometimes|boolean', 'only_media' => 'sometimes|boolean',
'_pe' => 'sometimes' '_pe' => 'sometimes'
]); ]);
@ -3518,6 +3593,9 @@ class ApiV1Controller extends Controller
$min = $request->input('min_id'); $min = $request->input('min_id');
$max = $request->input('max_id'); $max = $request->input('max_id');
$limit = $request->input('limit', 20); $limit = $request->input('limit', 20);
if($limit > 40) {
$limit = 40;
}
$onlyMedia = $request->input('only_media', true); $onlyMedia = $request->input('only_media', true);
$pe = $request->has(self::PF_API_ENTITY_KEY); $pe = $request->has(self::PF_API_ENTITY_KEY);
$pid = $request->user()->profile_id; $pid = $request->user()->profile_id;
@ -3547,7 +3625,7 @@ class ApiV1Controller extends Controller
->whereStatusVisibility('public') ->whereStatusVisibility('public')
->where('status_id', $dir, $id) ->where('status_id', $dir, $id)
->orderBy('status_id', 'desc') ->orderBy('status_id', 'desc')
->limit($limit) ->limit(100)
->pluck('status_id') ->pluck('status_id')
->map(function ($i) use($pe) { ->map(function ($i) use($pe) {
return $pe ? StatusService::get($i) : StatusService::getMastodon($i); return $pe ? StatusService::get($i) : StatusService::getMastodon($i);
@ -3565,6 +3643,7 @@ class ApiV1Controller extends Controller
$domain = strtolower(parse_url($i['url'], PHP_URL_HOST)); $domain = strtolower(parse_url($i['url'], PHP_URL_HOST));
return !in_array($i['account']['id'], $filters) && !in_array($domain, $domainBlocks); return !in_array($i['account']['id'], $filters) && !in_array($domain, $domainBlocks);
}) })
->take($limit)
->values() ->values()
->toArray(); ->toArray();
@ -3584,7 +3663,7 @@ class ApiV1Controller extends Controller
abort_unless($request->user()->tokenCan('read'), 403); abort_unless($request->user()->tokenCan('read'), 403);
$this->validate($request, [ $this->validate($request, [
'limit' => 'nullable|integer|min:1|max:40', 'limit' => 'sometimes|integer|min:1',
'max_id' => 'nullable|integer|min:0', 'max_id' => 'nullable|integer|min:0',
'since_id' => 'nullable|integer|min:0', 'since_id' => 'nullable|integer|min:0',
'min_id' => 'nullable|integer|min:0' 'min_id' => 'nullable|integer|min:0'
@ -3593,6 +3672,9 @@ class ApiV1Controller extends Controller
$pe = $request->has('_pe'); $pe = $request->has('_pe');
$pid = $request->user()->profile_id; $pid = $request->user()->profile_id;
$limit = $request->input('limit') ?? 20; $limit = $request->input('limit') ?? 20;
if($limit > 40) {
$limit = 40;
}
$max_id = $request->input('max_id'); $max_id = $request->input('max_id');
$since_id = $request->input('since_id'); $since_id = $request->input('since_id');
$min_id = $request->input('min_id'); $min_id = $request->input('min_id');
@ -3758,11 +3840,14 @@ class ApiV1Controller extends Controller
abort_if(!$request->user(), 403); abort_if(!$request->user(), 403);
$this->validate($request, [ $this->validate($request, [
'limit' => 'int|min:1|max:10', 'limit' => 'sometimes|integer|min:1',
'sort' => 'in:all,newest,popular' 'sort' => 'in:all,newest,popular'
]); ]);
$limit = $request->input('limit', 3); $limit = $request->input('limit', 3);
if($limit > 10) {
$limit = 10;
}
$pid = $request->user()->profile_id; $pid = $request->user()->profile_id;
$status = StatusService::getMastodon($id, false); $status = StatusService::getMastodon($id, false);

View file

@ -71,72 +71,77 @@ class ApiV2Controller extends Controller
->toArray() : []; ->toArray() : [];
}); });
$res = [ $res = Cache::remember('api:v2:instance-data-response-v2', 1800, function () use($contact, $rules) {
'domain' => config('pixelfed.domain.app'), return [
'title' => config_cache('app.name'), 'domain' => config('pixelfed.domain.app'),
'version' => config('pixelfed.version'), 'title' => config_cache('app.name'),
'source_url' => 'https://github.com/pixelfed/pixelfed', 'version' => '3.5.3 (compatible; Pixelfed ' . config('pixelfed.version') .')',
'description' => config_cache('app.short_description'), 'source_url' => 'https://github.com/pixelfed/pixelfed',
'usage' => [ 'description' => config_cache('app.short_description'),
'users' => [ 'usage' => [
'active_month' => (int) Nodeinfo::activeUsersMonthly() '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
], ],
'vapid' => [ 'thumbnail' => [
'public_key' => config('webpush.vapid.public_key'), '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'))
]
], ],
'accounts' => [ 'languages' => [config('app.locale')],
'max_featured_tags' => 0, 'configuration' => [
'urls' => [
'streaming' => null,
'status' => null
],
'vapid' => [
'public_key' => config('webpush.vapid.public_key'),
],
'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' => 0,
'max_characters_per_option' => 0,
'min_expiration' => 0,
'max_expiration' => 0,
],
'translation' => [
'enabled' => false,
],
], ],
'statuses' => [ 'registrations' => [
'max_characters' => (int) config('pixelfed.max_caption_length'), 'enabled' => null,
'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'), 'approval_required' => false,
'characters_reserved_per_url' => 23 'message' => null,
'url' => null,
], ],
'media_attachments' => [ 'contact' => [
'supported_mime_types' => explode(',', config_cache('pixelfed.media_types')), 'email' => config('instance.email'),
'image_size_limit' => config_cache('pixelfed.max_photo_size') * 1024, 'account' => $contact
'image_matrix_limit' => 3686400,
'video_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
'video_frame_rate_limit' => 240,
'video_matrix_limit' => 3686400
], ],
'polls' => [ 'rules' => $rules
'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['registrations']['enabled'] = (bool) config_cache('pixelfed.open_registration');
$res['registrations']['approval_required'] = (bool) config_cache('instance.curated_registration.enabled');
return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES); return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
} }

View file

@ -174,7 +174,7 @@ class RegisterController extends Controller
*/ */
public function showRegistrationForm() public function showRegistrationForm()
{ {
if(config_cache('pixelfed.open_registration')) { if((bool) config_cache('pixelfed.open_registration')) {
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) { if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
abort_if(BouncerService::checkIp(request()->ip()), 404); abort_if(BouncerService::checkIp(request()->ip()), 404);
} }
@ -191,7 +191,11 @@ class RegisterController extends Controller
return view('auth.register'); return view('auth.register');
} }
} else { } else {
abort(404); if((bool) config_cache('instance.curated_registration.enabled') && config('instance.curated_registration.state.fallback_on_closed_reg')) {
return redirect('/auth/sign_up');
} else {
abort(404);
}
} }
} }

View file

@ -0,0 +1,398 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use App\User;
use App\Models\CuratedRegister;
use App\Models\CuratedRegisterActivity;
use App\Services\EmailService;
use App\Services\BouncerService;
use App\Util\Lexer\RestrictedNames;
use App\Mail\CuratedRegisterConfirmEmail;
use App\Mail\CuratedRegisterNotifyAdmin;
use Illuminate\Support\Facades\Mail;
use App\Jobs\CuratedOnboarding\CuratedOnboardingNotifyAdminNewApplicationPipeline;
class CuratedRegisterController extends Controller
{
public function __construct()
{
abort_unless((bool) config_cache('instance.curated_registration.enabled'), 404);
if((bool) config_cache('pixelfed.open_registration')) {
abort_if(config('instance.curated_registration.state.only_enabled_on_closed_reg'), 404);
} else {
abort_unless(config('instance.curated_registration.state.fallback_on_closed_reg'), 404);
}
}
public function index(Request $request)
{
abort_if($request->user(), 404);
return view('auth.curated-register.index', ['step' => 1]);
}
public function concierge(Request $request)
{
abort_if($request->user(), 404);
$emailConfirmed = $request->session()->has('cur-reg-con.email-confirmed') &&
$request->has('next') &&
$request->session()->has('cur-reg-con.cr-id');
return view('auth.curated-register.concierge', compact('emailConfirmed'));
}
public function conciergeResponseSent(Request $request)
{
return view('auth.curated-register.user_response_sent');
}
public function conciergeFormShow(Request $request)
{
abort_if($request->user(), 404);
abort_unless(
$request->session()->has('cur-reg-con.email-confirmed') &&
$request->session()->has('cur-reg-con.cr-id') &&
$request->session()->has('cur-reg-con.ac-id'), 404);
$crid = $request->session()->get('cur-reg-con.cr-id');
$arid = $request->session()->get('cur-reg-con.ac-id');
$showCaptcha = config('instance.curated_registration.captcha_enabled');
if($attempts = $request->session()->get('cur-reg-con-attempt')) {
$showCaptcha = $attempts && $attempts >= 2;
} else {
$showCaptcha = false;
}
$activity = CuratedRegisterActivity::whereRegisterId($crid)->whereFromAdmin(true)->findOrFail($arid);
return view('auth.curated-register.concierge_form', compact('activity', 'showCaptcha'));
}
public function conciergeFormStore(Request $request)
{
abort_if($request->user(), 404);
$request->session()->increment('cur-reg-con-attempt');
abort_unless(
$request->session()->has('cur-reg-con.email-confirmed') &&
$request->session()->has('cur-reg-con.cr-id') &&
$request->session()->has('cur-reg-con.ac-id'), 404);
$attempts = $request->session()->get('cur-reg-con-attempt');
$messages = [];
$rules = [
'response' => 'required|string|min:5|max:1000',
'crid' => 'required|integer|min:1',
'acid' => 'required|integer|min:1'
];
if(config('instance.curated_registration.captcha_enabled') && $attempts >= 3) {
$rules['h-captcha-response'] = 'required|captcha';
$messages['h-captcha-response.required'] = 'The captcha must be filled';
}
$this->validate($request, $rules, $messages);
$crid = $request->session()->get('cur-reg-con.cr-id');
$acid = $request->session()->get('cur-reg-con.ac-id');
abort_if((string) $crid !== $request->input('crid'), 404);
abort_if((string) $acid !== $request->input('acid'), 404);
if(CuratedRegisterActivity::whereRegisterId($crid)->whereReplyToId($acid)->exists()) {
return redirect()->back()->withErrors(['code' => 'You already replied to this request.']);
}
$act = CuratedRegisterActivity::create([
'register_id' => $crid,
'reply_to_id' => $acid,
'type' => 'user_response',
'message' => $request->input('response'),
'from_user' => true,
'action_required' => true,
]);
$request->session()->pull('cur-reg-con');
$request->session()->pull('cur-reg-con-attempt');
return view('auth.curated-register.user_response_sent');
}
public function conciergeStore(Request $request)
{
abort_if($request->user(), 404);
$rules = [
'sid' => 'required_if:action,email|integer|min:1|max:20000000',
'id' => 'required_if:action,email|integer|min:1|max:20000000',
'code' => 'required_if:action,email',
'action' => 'required|string|in:email,message',
'email' => 'required_if:action,email|email',
'response' => 'required_if:action,message|string|min:20|max:1000',
];
$messages = [];
if(config('instance.curated_registration.captcha_enabled')) {
$rules['h-captcha-response'] = 'required|captcha';
$messages['h-captcha-response.required'] = 'The captcha must be filled';
}
$this->validate($request, $rules, $messages);
$action = $request->input('action');
$sid = $request->input('sid');
$id = $request->input('id');
$code = $request->input('code');
$email = $request->input('email');
$cr = CuratedRegister::whereIsClosed(false)->findOrFail($sid);
$ac = CuratedRegisterActivity::whereRegisterId($cr->id)->whereFromAdmin(true)->findOrFail($id);
if(!hash_equals($ac->secret_code, $code)) {
return redirect()->back()->withErrors(['code' => 'Invalid code']);
}
if(!hash_equals($cr->email, $email)) {
return redirect()->back()->withErrors(['email' => 'Invalid email']);
}
$request->session()->put('cur-reg-con.email-confirmed', true);
$request->session()->put('cur-reg-con.cr-id', $cr->id);
$request->session()->put('cur-reg-con.ac-id', $ac->id);
$emailConfirmed = true;
return redirect('/auth/sign_up/concierge/form');
}
public function confirmEmail(Request $request)
{
if($request->user()) {
return redirect(route('help.email-confirmation-issues'));
}
return view('auth.curated-register.confirm_email');
}
public function emailConfirmed(Request $request)
{
if($request->user()) {
return redirect(route('help.email-confirmation-issues'));
}
return view('auth.curated-register.email_confirmed');
}
public function resendConfirmation(Request $request)
{
return view('auth.curated-register.resend-confirmation');
}
public function resendConfirmationProcess(Request $request)
{
$rules = [
'email' => [
'required',
'string',
app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email',
'exists:curated_registers',
]
];
$messages = [];
if(config('instance.curated_registration.captcha_enabled')) {
$rules['h-captcha-response'] = 'required|captcha';
$messages['h-captcha-response.required'] = 'The captcha must be filled';
}
$this->validate($request, $rules, $messages);
$cur = CuratedRegister::whereEmail($request->input('email'))->whereIsClosed(false)->first();
if(!$cur) {
return redirect()->back()->withErrors(['email' => 'The selected email is invalid.']);
}
$totalCount = CuratedRegisterActivity::whereRegisterId($cur->id)
->whereType('user_resend_email_confirmation')
->count();
if($totalCount && $totalCount >= config('instance.curated_registration.resend_confirmation_limit')) {
return redirect()->back()->withErrors(['email' => 'You have re-attempted too many times. To proceed with your application, please <a href="/site/contact" class="text-white" style="text-decoration: underline;">contact the admin team</a>.']);
}
$count = CuratedRegisterActivity::whereRegisterId($cur->id)
->whereType('user_resend_email_confirmation')
->where('created_at', '>', now()->subHours(12))
->count();
if($count) {
return redirect()->back()->withErrors(['email' => 'You can only re-send the confirmation email once per 12 hours. Try again later.']);
}
CuratedRegisterActivity::create([
'register_id' => $cur->id,
'type' => 'user_resend_email_confirmation',
'admin_only_view' => true,
'from_admin' => false,
'from_user' => false,
'action_required' => false,
]);
Mail::to($cur->email)->send(new CuratedRegisterConfirmEmail($cur));
return view('auth.curated-register.resent-confirmation');
return $request->all();
}
public function confirmEmailHandle(Request $request)
{
$rules = [
'sid' => 'required',
'code' => 'required'
];
$messages = [];
if(config('instance.curated_registration.captcha_enabled')) {
$rules['h-captcha-response'] = 'required|captcha';
$messages['h-captcha-response.required'] = 'The captcha must be filled';
}
$this->validate($request, $rules, $messages);
$cr = CuratedRegister::whereNull('email_verified_at')
->where('created_at', '>', now()->subHours(24))
->find($request->input('sid'));
if(!$cr) {
return redirect(route('help.email-confirmation-issues'));
}
if(!hash_equals($cr->verify_code, $request->input('code'))) {
return redirect(route('help.email-confirmation-issues'));
}
$cr->email_verified_at = now();
$cr->save();
if(config('instance.curated_registration.notify.admin.on_verify_email.enabled')) {
CuratedOnboardingNotifyAdminNewApplicationPipeline::dispatch($cr);
}
return view('auth.curated-register.email_confirmed');
}
public function proceed(Request $request)
{
$this->validate($request, [
'step' => 'required|integer|in:1,2,3,4'
]);
$step = $request->input('step');
switch($step) {
case 1:
$step = 2;
$request->session()->put('cur-step', 1);
return view('auth.curated-register.index', compact('step'));
break;
case 2:
$this->stepTwo($request);
$step = 3;
$request->session()->put('cur-step', 2);
return view('auth.curated-register.index', compact('step'));
break;
case 3:
$this->stepThree($request);
$step = 3;
$request->session()->put('cur-step', 3);
$verifiedEmail = true;
$request->session()->pull('cur-reg');
return view('auth.curated-register.index', compact('step', 'verifiedEmail'));
break;
}
}
protected function stepTwo($request)
{
if($request->filled('reason')) {
$request->session()->put('cur-reg.form-reason', $request->input('reason'));
}
if($request->filled('username')) {
$request->session()->put('cur-reg.form-username', $request->input('username'));
}
if($request->filled('email')) {
$request->session()->put('cur-reg.form-email', $request->input('email'));
}
$this->validate($request, [
'username' => [
'required',
'min:2',
'max:15',
'unique:curated_registers',
'unique:users',
function ($attribute, $value, $fail) {
$dash = substr_count($value, '-');
$underscore = substr_count($value, '_');
$period = substr_count($value, '.');
if(ends_with($value, ['.php', '.js', '.css'])) {
return $fail('Username is invalid.');
}
if(($dash + $underscore + $period) > 1) {
return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
}
if (!ctype_alnum($value[0])) {
return $fail('Username is invalid. Must start with a letter or number.');
}
if (!ctype_alnum($value[strlen($value) - 1])) {
return $fail('Username is invalid. Must end with a letter or number.');
}
$val = str_replace(['_', '.', '-'], '', $value);
if(!ctype_alnum($val)) {
return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
}
$restricted = RestrictedNames::get();
if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
return $fail('Username cannot be used.');
}
},
],
'email' => [
'required',
'string',
app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email',
'max:255',
'unique:users',
'unique:curated_registers',
function ($attribute, $value, $fail) {
$banned = EmailService::isBanned($value);
if($banned) {
return $fail('Email is invalid.');
}
},
],
'password' => 'required|min:8',
'password_confirmation' => 'required|same:password',
'reason' => 'required|min:20|max:1000',
'agree' => 'required|accepted'
]);
$request->session()->put('cur-reg.form-email', $request->input('email'));
$request->session()->put('cur-reg.form-password', $request->input('password'));
}
protected function stepThree($request)
{
$this->validate($request, [
'email' => [
'required',
'string',
app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email',
'max:255',
'unique:users',
'unique:curated_registers',
function ($attribute, $value, $fail) {
$banned = EmailService::isBanned($value);
if($banned) {
return $fail('Email is invalid.');
}
},
]
]);
$cr = new CuratedRegister;
$cr->email = $request->email;
$cr->username = $request->session()->get('cur-reg.form-username');
$cr->password = bcrypt($request->session()->get('cur-reg.form-password'));
$cr->ip_address = $request->ip();
$cr->reason_to_join = $request->session()->get('cur-reg.form-reason');
$cr->verify_code = Str::random(40);
$cr->save();
Mail::to($cr->email)->send(new CuratedRegisterConfirmEmail($cr));
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace App\Jobs\CuratedOnboarding;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\CuratedRegister;
use App\User;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use App\Mail\CuratedRegisterNotifyAdmin;
class CuratedOnboardingNotifyAdminNewApplicationPipeline implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $cr;
/**
* Create a new job instance.
*/
public function __construct(CuratedRegister $cr)
{
$this->cr = $cr;
}
/**
* Execute the job.
*/
public function handle(): void
{
if(!config('instance.curated_registration.notify.admin.on_verify_email.enabled')) {
return;
}
config('instance.curated_registration.notify.admin.on_verify_email.bundle') ?
$this->handleBundled() :
$this->handleUnbundled();
}
protected function handleBundled()
{
$cr = $this->cr;
Storage::append('conanap.json', json_encode([
'id' => $cr->id,
'email' => $cr->email,
'created_at' => $cr->created_at,
'updated_at' => $cr->updated_at,
]));
}
protected function handleUnbundled()
{
$cr = $this->cr;
if($aid = config_cache('instance.admin.pid')) {
$admin = User::whereProfileId($aid)->first();
if($admin && $admin->email) {
Mail::to($admin->email)->send(new CuratedRegisterNotifyAdmin($cr));
}
}
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class CuratedRegisterAcceptUser extends Mailable
{
use Queueable, SerializesModels;
public $verify;
/**
* Create a new message instance.
*/
public function __construct($verify)
{
$this->verify = $verify;
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: 'Your ' . config('pixelfed.domain.app') . ' Registration Update',
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
markdown: 'emails.curated-register.request-accepted',
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
use App\Models\CuratedRegister;
class CuratedRegisterConfirmEmail extends Mailable
{
use Queueable, SerializesModels;
public $verify;
/**
* Create a new message instance.
*/
public function __construct(CuratedRegister $verify)
{
$this->verify = $verify;
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: 'Welcome to Pixelfed! Please Confirm Your Email',
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
markdown: 'emails.curated-register.confirm_email',
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
use App\Models\CuratedRegister;
class CuratedRegisterNotifyAdmin extends Mailable
{
use Queueable, SerializesModels;
public $verify;
/**
* Create a new message instance.
*/
public function __construct(CuratedRegister $verify)
{
$this->verify = $verify;
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: '[Requires Action]: New Curated Onboarding Application',
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
markdown: 'emails.curated-register.admin_notify',
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class CuratedRegisterNotifyAdminUserResponse extends Mailable
{
use Queueable, SerializesModels;
public $activity;
/**
* Create a new message instance.
*/
public function __construct($activity)
{
$this->activity = $activity;
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: 'Curated Register Notify Admin User Response',
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
markdown: 'emails.curated-register.admin_notify_user_response',
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class CuratedRegisterRejectUser extends Mailable
{
use Queueable, SerializesModels;
public $verify;
/**
* Create a new message instance.
*/
public function __construct($verify)
{
$this->verify = $verify;
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: 'Your ' . config('pixelfed.domain.app') . ' Registration Update',
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
markdown: 'emails.curated-register.request-rejected',
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
use App\Models\CuratedRegister;
use App\Models\CuratedRegisterActivity;
class CuratedRegisterRequestDetailsFromUser extends Mailable
{
use Queueable, SerializesModels;
public $verify;
public $activity;
/**
* Create a new message instance.
*/
public function __construct(CuratedRegister $verify, CuratedRegisterActivity $activity)
{
$this->verify = $verify;
$this->activity = $activity;
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: '[Action Needed]: Additional information requested',
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
markdown: 'emails.curated-register.request-details-from-user',
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class CuratedRegisterSendMessage extends Mailable
{
use Queueable, SerializesModels;
public $verify;
/**
* Create a new message instance.
*/
public function __construct($verify)
{
$this->verify = $verify;
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: 'Your ' . config('pixelfed.domain.app') . ' Registration Update',
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
markdown: 'emails.curated-register.message-from-admin',
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CuratedRegister extends Model
{
use HasFactory;
protected $casts = [
'autofollow_account_ids' => 'array',
'admin_notes' => 'array',
'email_verified_at' => 'datetime',
'admin_notified_at' => 'datetime',
'action_taken_at' => 'datetime',
];
public function adminStatusLabel()
{
if(!$this->email_verified_at) {
return '<span class="border border-danger px-3 py-1 rounded text-white font-weight-bold">Unverified email</span>';
}
if($this->is_accepted) { return 'Approved'; }
if($this->is_rejected) { return 'Rejected'; }
if($this->is_awaiting_more_info ) {
return '<span class="border border-info px-3 py-1 rounded text-white font-weight-bold">Awaiting Details</span>';
}
if($this->is_closed ) { return 'Closed'; }
return '<span class="border border-success px-3 py-1 rounded text-white font-weight-bold">Open</span>';
}
public function emailConfirmUrl()
{
return url('/auth/sign_up/confirm?sid=' . $this->id . '&code=' . $this->verify_code);
}
public function emailReplyUrl()
{
return url('/auth/sign_up/concierge?sid=' . $this->id . '&code=' . $this->verify_code . '&sc=' . str_random(8));
}
public function adminReviewUrl()
{
return url('/i/admin/curated-onboarding/show/' . $this->id);
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CuratedRegisterActivity extends Model
{
use HasFactory;
protected $guarded = [];
protected $casts = [
'metadata' => 'array',
'admin_notified_at' => 'datetime',
'action_taken_at' => 'datetime',
];
public function application()
{
return $this->belongsTo(CuratedRegister::class, 'register_id');
}
public function emailReplyUrl()
{
return url('/auth/sign_up/concierge?sid='.$this->register_id . '&id=' . $this->id . '&code=' . $this->secret_code);
}
public function adminReviewUrl()
{
$url = '/i/admin/curated-onboarding/show/' . $this->register_id . '/?ah=' . $this->id;
if($this->reply_to_id) {
$url .= '&rtid=' . $this->reply_to_id;
}
return url($url);
}
}

View file

@ -72,6 +72,8 @@ class ConfigCacheService
'instance.banner.blurhash', 'instance.banner.blurhash',
'autospam.nlp.enabled', 'autospam.nlp.enabled',
'instance.curated_registration.enabled',
// 'system.user_mode' // 'system.user_mode'
]; ];

View file

@ -48,13 +48,16 @@ class LandingService
->toArray() : []; ->toArray() : [];
}); });
$openReg = (bool) config_cache('pixelfed.open_registration');
$res = [ $res = [
'name' => config_cache('app.name'), 'name' => config_cache('app.name'),
'url' => config_cache('app.url'), 'url' => config_cache('app.url'),
'domain' => config('pixelfed.domain.app'), 'domain' => config('pixelfed.domain.app'),
'show_directory' => config_cache('instance.landing.show_directory'), 'show_directory' => config_cache('instance.landing.show_directory'),
'show_explore_feed' => config_cache('instance.landing.show_explore'), 'show_explore_feed' => config_cache('instance.landing.show_explore'),
'open_registration' => config_cache('pixelfed.open_registration') == 1, 'open_registration' => (bool) $openReg,
'curated_onboarding' => (bool) config_cache('instance.curated_registration.enabled'),
'version' => config('pixelfed.version'), 'version' => config('pixelfed.version'),
'about' => [ 'about' => [
'banner_image' => config_cache('app.banner_image') ?? url('/storage/headers/default.jpg'), 'banner_image' => config_cache('app.banner_image') ?? url('/storage/headers/default.jpg'),

View file

@ -5,7 +5,7 @@
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"type": "project", "type": "project",
"require": { "require": {
"php": "^8.1|^8.2", "php": "^8.1|^8.2|^8.3",
"ext-bcmath": "*", "ext-bcmath": "*",
"ext-ctype": "*", "ext-ctype": "*",
"ext-curl": "*", "ext-curl": "*",

1078
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -74,7 +74,7 @@ return [
'redis' => [ 'redis' => [
'driver' => 'redis', 'driver' => 'redis',
'lock_connection' => 'default', 'lock_connection' => 'default',
'client' => env('REDIS_CLIENT', 'phpredis'), 'client' => env('REDIS_CLIENT', 'predis'),
'default' => [ 'default' => [
'scheme' => env('REDIS_SCHEME', 'tcp'), 'scheme' => env('REDIS_SCHEME', 'tcp'),

View file

@ -49,7 +49,7 @@ return [
], ],
'network_timeline' => env('PF_NETWORK_TIMELINE', true), 'network_timeline' => env('PF_NETWORK_TIMELINE', true),
'network_timeline_days_falloff' => env('PF_NETWORK_TIMELINE_DAYS_FALLOFF', 2), 'network_timeline_days_falloff' => env('PF_NETWORK_TIMELINE_DAYS_FALLOFF', 90),
'custom_emoji' => [ 'custom_emoji' => [
'enabled' => env('CUSTOM_EMOJI', false), 'enabled' => env('CUSTOM_EMOJI', false),

View file

@ -145,4 +145,35 @@ return [
'software-update' => [ 'software-update' => [
'disable_failed_warning' => env('INSTANCE_SOFTWARE_UPDATE_DISABLE_FAILED_WARNING', false) 'disable_failed_warning' => env('INSTANCE_SOFTWARE_UPDATE_DISABLE_FAILED_WARNING', false)
], ],
'notifications' => [
'gc' => [
'enabled' => env('INSTANCE_NOTIFY_AUTO_GC', false),
'delete_after_days' => env('INSTANCE_NOTIFY_AUTO_GC_DEL_AFTER_DAYS', 365)
]
],
'curated_registration' => [
'enabled' => env('INSTANCE_CUR_REG', false),
'resend_confirmation_limit' => env('INSTANCE_CUR_REG_RESEND_LIMIT', 5),
'captcha_enabled' => env('INSTANCE_CUR_REG_CAPTCHA', env('CAPTCHA_ENABLED', false)),
'state' => [
'fallback_on_closed_reg' => true,
'only_enabled_on_closed_reg' => env('INSTANCE_CUR_REG_STATE_ONLY_ON_CLOSED', true),
],
'notify' => [
'admin' => [
'on_verify_email' => [
'enabled' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_VERIFY', false),
'bundle' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_VERIFY_BUNDLE', false),
'max_per_day' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_VERIFY_MPD', 10),
],
'on_user_response' => env('INSTANCE_CUR_REG_NOTIFY_ADMIN_ON_USER_RESPONSE', false),
]
],
],
]; ];

View file

@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('curated_registers', function (Blueprint $table) {
$table->id();
$table->string('email')->unique()->nullable()->index();
$table->string('username')->unique()->nullable()->index();
$table->string('password')->nullable();
$table->string('ip_address')->nullable();
$table->string('verify_code')->nullable();
$table->text('reason_to_join')->nullable();
$table->unsignedBigInteger('invited_by')->nullable()->index();
$table->boolean('is_approved')->default(0)->index();
$table->boolean('is_rejected')->default(0)->index();
$table->boolean('is_awaiting_more_info')->default(0)->index();
$table->boolean('is_closed')->default(0)->index();
$table->json('autofollow_account_ids')->nullable();
$table->json('admin_notes')->nullable();
$table->unsignedInteger('approved_by_admin_id')->nullable();
$table->timestamp('email_verified_at')->nullable();
$table->timestamp('admin_notified_at')->nullable();
$table->timestamp('action_taken_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('curated_registers');
}
};

View file

@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('curated_register_activities', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('register_id')->nullable()->index();
$table->unsignedInteger('admin_id')->nullable();
$table->unsignedInteger('reply_to_id')->nullable()->index();
$table->string('secret_code')->nullable();
$table->string('type')->nullable()->index();
$table->string('title')->nullable();
$table->string('link')->nullable();
$table->text('message')->nullable();
$table->json('metadata')->nullable();
$table->boolean('from_admin')->default(false)->index();
$table->boolean('from_user')->default(false)->index();
$table->boolean('admin_only_view')->default(true);
$table->boolean('action_required')->default(false);
$table->timestamp('admin_notified_at')->nullable();
$table->timestamp('action_taken_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('curated_register_activities');
}
};

1543
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,7 @@
}, },
"devDependencies": { "devDependencies": {
"acorn": "^8.7.1", "acorn": "^8.7.1",
"axios": "^0.21.1", "axios": ">=1.6.0",
"bootstrap": "^4.5.2", "bootstrap": "^4.5.2",
"cross-env": "^5.2.1", "cross-env": "^5.2.1",
"jquery": "^3.6.0", "jquery": "^3.6.0",

Binary file not shown.

BIN
public/js/activity.js vendored

Binary file not shown.

BIN
public/js/admin.js vendored

Binary file not shown.

Binary file not shown.

BIN
public/js/app.js vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/compose.js vendored

Binary file not shown.

BIN
public/js/daci.chunk.34dc7bad3a0792cc.js vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/direct.js vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/discover.js vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/dms.chunk.2b55effc0e8ba89f.js vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/hashtag.js vendored

Binary file not shown.

BIN
public/js/home.chunk.264eeb47bfac56c1.js vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/landing.js vendored

Binary file not shown.

BIN
public/js/manifest.js vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/portfolio.js vendored

Binary file not shown.

BIN
public/js/post.chunk.5ff16664f9adb901.js vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/profile.js vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/search.js vendored

Binary file not shown.

BIN
public/js/spa.js vendored

Binary file not shown.

BIN
public/js/status.js vendored

Binary file not shown.

BIN
public/js/stories.js vendored

Binary file not shown.

Binary file not shown.

BIN
public/js/timeline.js vendored

Binary file not shown.

BIN
public/js/vendor.js vendored

Binary file not shown.

View file

@ -71,8 +71,8 @@
*/ */
/*! /*!
* Vue.js v2.7.14 * Vue.js v2.7.16
* (c) 2014-2022 Evan You * (c) 2014-2023 Evan You
* Released under the MIT License. * Released under the MIT License.
*/ */

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more