Merge pull request #5005 from pixelfed/staging

Update Admin Settings
This commit is contained in:
daniel 2024-04-05 22:05:09 -06:00 committed by GitHub
commit 0b162dc15e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
113 changed files with 4975 additions and 2672 deletions

View file

@ -21,7 +21,12 @@ jobs:
steps: steps:
- checkout - checkout
- run: sudo apt update && sudo apt install zlib1g-dev libsqlite3-dev - run:
name: "Create Environment file and generate app key"
command: |
mv .env.testing .env
- run: sudo apt install zlib1g-dev libsqlite3-dev
# Download and cache dependencies # Download and cache dependencies
@ -36,18 +41,17 @@ jobs:
- run: composer install -n --prefer-dist - run: composer install -n --prefer-dist
- save_cache: - save_cache:
key: composer-v2-{{ checksum "composer.lock" }} key: v2-dependencies-{{ checksum "composer.json" }}
paths: paths:
- vendor - vendor
- run: cp .env.testing .env
- run: php artisan config:cache - run: php artisan config:cache
- run: php artisan route:clear - run: php artisan route:clear
- run: php artisan storage:link - run: php artisan storage:link
- run: php artisan key:generate - run: php artisan key:generate
# run tests with phpunit or codecept # run tests with phpunit or codecept
- run: ./vendor/bin/phpunit - run: php artisan test
- store_test_results: - store_test_results:
path: tests/_output path: tests/_output
- store_artifacts: - store_artifacts:

View file

@ -82,7 +82,7 @@ class AvatarStorage extends Command
$this->line(' '); $this->line(' ');
if(config_cache('pixelfed.cloud_storage')) { if((bool) config_cache('pixelfed.cloud_storage')) {
$this->info('✅ - Cloud storage configured'); $this->info('✅ - Cloud storage configured');
$this->line(' '); $this->line(' ');
} }
@ -92,7 +92,7 @@ class AvatarStorage extends Command
$this->line(' '); $this->line(' ');
} }
if(config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud')) { if((bool) config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud')) {
$disk = Storage::disk(config_cache('filesystems.cloud')); $disk = Storage::disk(config_cache('filesystems.cloud'));
$exists = $disk->exists('cache/avatars/default.jpg'); $exists = $disk->exists('cache/avatars/default.jpg');
$state = $exists ? '✅' : '❌'; $state = $exists ? '✅' : '❌';
@ -100,7 +100,7 @@ class AvatarStorage extends Command
$this->info($msg); $this->info($msg);
} }
$options = config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud') ? $options = (bool) config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud') ?
[ [
'Cancel', 'Cancel',
'Upload default avatar to cloud', 'Upload default avatar to cloud',
@ -164,7 +164,7 @@ class AvatarStorage extends Command
protected function uploadAvatarsToCloud() protected function uploadAvatarsToCloud()
{ {
if(!config_cache('pixelfed.cloud_storage') || !config('instance.avatar.local_to_cloud')) { if(!(bool) config_cache('pixelfed.cloud_storage') || !config('instance.avatar.local_to_cloud')) {
$this->error('Enable cloud storage and avatar cloud storage to perform this action'); $this->error('Enable cloud storage and avatar cloud storage to perform this action');
return; return;
} }
@ -213,7 +213,7 @@ class AvatarStorage extends Command
return; return;
} }
if(config_cache('pixelfed.cloud_storage') == false && config_cache('federation.avatars.store_local') == false) { if((bool) config_cache('pixelfed.cloud_storage') == false && config_cache('federation.avatars.store_local') == false) {
$this->error('You have cloud storage disabled and local avatar storage disabled, we cannot refetch avatars.'); $this->error('You have cloud storage disabled and local avatar storage disabled, we cannot refetch avatars.');
return; return;
} }

View file

@ -44,7 +44,7 @@ class AvatarStorageDeepClean extends Command
$this->line(' '); $this->line(' ');
$storage = [ $storage = [
'cloud' => boolval(config_cache('pixelfed.cloud_storage')), 'cloud' => (bool) config_cache('pixelfed.cloud_storage'),
'local' => boolval(config_cache('federation.avatars.store_local')) 'local' => boolval(config_cache('federation.avatars.store_local'))
]; ];

View file

@ -35,12 +35,16 @@ class CloudMediaMigrate extends Command
*/ */
public function handle() public function handle()
{ {
$enabled = config('pixelfed.cloud_storage'); $enabled = (bool) config_cache('pixelfed.cloud_storage');
if(!$enabled) { if(!$enabled) {
$this->error('Cloud storage not enabled. Exiting...'); $this->error('Cloud storage not enabled. Exiting...');
return; return;
} }
if(!$this->confirm('Are you sure you want to proceed?')) {
return;
}
$limit = $this->option('limit'); $limit = $this->option('limit');
$hugeMode = $this->option('huge'); $hugeMode = $this->option('huge');

View file

@ -2,11 +2,11 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Media; use App\Media;
use Illuminate\Support\Facades\Http;
use App\Services\MediaService; use App\Services\MediaService;
use App\Services\StatusService; use App\Services\StatusService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
class FetchMissingMediaMimeType extends Command class FetchMissingMediaMimeType extends Command
{ {
@ -29,20 +29,20 @@ class FetchMissingMediaMimeType extends Command
*/ */
public function handle() public function handle()
{ {
foreach(Media::whereNotNull(['remote_url', 'status_id'])->whereNull('mime')->lazyByIdDesc(50, 'id') as $media) { foreach (Media::whereNotNull(['remote_url', 'status_id'])->whereNull('mime')->lazyByIdDesc(50, 'id') as $media) {
$res = Http::retry(2, 100, throw: false)->head($media->remote_url); $res = Http::retry(2, 100, throw: false)->head($media->remote_url);
if(!$res->successful()) { if (! $res->successful()) {
continue; continue;
} }
if(!in_array($res->header('content-type'), explode(',',config('pixelfed.media_types')))) { if (! in_array($res->header('content-type'), explode(',', config_cache('pixelfed.media_types')))) {
continue; continue;
} }
$media->mime = $res->header('content-type'); $media->mime = $res->header('content-type');
if($res->hasHeader('content-length')) { if ($res->hasHeader('content-length')) {
$media->size = $res->header('content-length'); $media->size = $res->header('content-length');
} }
@ -50,7 +50,7 @@ class FetchMissingMediaMimeType extends Command
MediaService::del($media->status_id); MediaService::del($media->status_id);
StatusService::del($media->status_id); StatusService::del($media->status_id);
$this->info('mid:'.$media->id . ' (' . $res->header('content-type') . ':' . $res->header('content-length') . ' bytes)'); $this->info('mid:'.$media->id.' ('.$res->header('content-type').':'.$res->header('content-length').' bytes)');
} }
} }
} }

View file

@ -37,7 +37,7 @@ class FixMediaDriver extends Command
return Command::SUCCESS; return Command::SUCCESS;
} }
if(config_cache('pixelfed.cloud_storage') == false) { if((bool) config_cache('pixelfed.cloud_storage') == false) {
$this->error('Cloud storage not enabled, exiting...'); $this->error('Cloud storage not enabled, exiting...');
return Command::SUCCESS; return Command::SUCCESS;
} }

View file

@ -47,7 +47,7 @@ class MediaCloudUrlRewrite extends Command implements PromptsForMissingInput
protected function preflightCheck() protected function preflightCheck()
{ {
if(config_cache('pixelfed.cloud_storage') != true) { if(!(bool) config_cache('pixelfed.cloud_storage')) {
$this->info('Error: Cloud storage is not enabled!'); $this->info('Error: Cloud storage is not enabled!');
$this->error('Aborting...'); $this->error('Aborting...');
exit; exit;

View file

@ -45,7 +45,7 @@ class MediaS3GarbageCollector extends Command
*/ */
public function handle() public function handle()
{ {
$enabled = in_array(config_cache('pixelfed.cloud_storage'), ['1', true, 'true']); $enabled = (bool) config_cache('pixelfed.cloud_storage');
if(!$enabled) { if(!$enabled) {
$this->error('Cloud storage not enabled. Exiting...'); $this->error('Cloud storage not enabled. Exiting...');
return; return;

View file

@ -33,7 +33,7 @@ class Kernel extends ConsoleKernel
$schedule->command('gc:passwordreset')->dailyAt('09:41')->onOneServer(); $schedule->command('gc:passwordreset')->dailyAt('09:41')->onOneServer();
$schedule->command('gc:sessions')->twiceDaily(13, 23)->onOneServer(); $schedule->command('gc:sessions')->twiceDaily(13, 23)->onOneServer();
if (in_array(config_cache('pixelfed.cloud_storage'), ['1', true, 'true']) && config('media.delete_local_after_cloud')) { if ((bool) config_cache('pixelfed.cloud_storage') && (bool) config_cache('media.delete_local_after_cloud')) {
$schedule->command('media:s3gc')->hourlyAt(15); $schedule->command('media:s3gc')->hourlyAt(15);
} }

View file

@ -157,7 +157,7 @@ class AccountController extends Controller
$pid = $request->user()->profile_id; $pid = $request->user()->profile_id;
$count = UserFilterService::muteCount($pid); $count = UserFilterService::muteCount($pid);
$maxLimit = intval(config('instance.user_filters.max_user_mutes')); $maxLimit = (int) config_cache('instance.user_filters.max_user_mutes');
abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts'); abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts');
if($count == 0) { if($count == 0) {
$filterCount = UserFilter::whereUserId($pid)->count(); $filterCount = UserFilter::whereUserId($pid)->count();
@ -260,7 +260,7 @@ class AccountController extends Controller
]); ]);
$pid = $request->user()->profile_id; $pid = $request->user()->profile_id;
$count = UserFilterService::blockCount($pid); $count = UserFilterService::blockCount($pid);
$maxLimit = intval(config('instance.user_filters.max_user_blocks')); $maxLimit = (int) config_cache('instance.user_filters.max_user_blocks');
abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts'); abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts');
if($count == 0) { if($count == 0) {
$filterCount = UserFilter::whereUserId($pid)->whereFilterType('block')->count(); $filterCount = UserFilter::whereUserId($pid)->whereFilterType('block')->count();

View file

@ -2,30 +2,20 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use DB, Cache; use App\Http\Controllers\PixelfedDirectoryController;
use App\{
DiscoverCategory,
DiscoverCategoryHashtag,
Hashtag,
Media,
Profile,
Status,
StatusHashtag,
User
};
use App\Models\ConfigCache; use App\Models\ConfigCache;
use App\Services\AccountService; use App\Services\AccountService;
use App\Services\ConfigCacheService; use App\Services\ConfigCacheService;
use App\Services\StatusService; use App\Services\StatusService;
use Carbon\Carbon; use App\Status;
use App\User;
use Cache;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Validation\Rule; use Illuminate\Support\Facades\Http;
use League\ISO3166\ISO3166;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Str;
use App\Http\Controllers\PixelfedDirectoryController; use League\ISO3166\ISO3166;
trait AdminDirectoryController trait AdminDirectoryController
{ {
@ -41,37 +31,37 @@ trait AdminDirectoryController
$res['countries'] = collect((new ISO3166)->all())->pluck('name'); $res['countries'] = collect((new ISO3166)->all())->pluck('name');
$res['admins'] = User::whereIsAdmin(true) $res['admins'] = User::whereIsAdmin(true)
->where('2fa_enabled', true) ->where('2fa_enabled', true)
->get()->map(function($user) { ->get()->map(function ($user) {
return [ return [
'uid' => (string) $user->id, 'uid' => (string) $user->id,
'pid' => (string) $user->profile_id, 'pid' => (string) $user->profile_id,
'username' => $user->username, 'username' => $user->username,
'created_at' => $user->created_at 'created_at' => $user->created_at,
]; ];
}); });
$config = ConfigCache::whereK('pixelfed.directory')->first(); $config = ConfigCache::whereK('pixelfed.directory')->first();
if($config) { if ($config) {
$data = $config->v ? json_decode($config->v, true) : []; $data = $config->v ? json_decode($config->v, true) : [];
$res = array_merge($res, $data); $res = array_merge($res, $data);
} }
if(empty($res['summary'])) { if (empty($res['summary'])) {
$summary = ConfigCache::whereK('app.short_description')->pluck('v'); $summary = ConfigCache::whereK('app.short_description')->pluck('v');
$res['summary'] = $summary ? $summary[0] : null; $res['summary'] = $summary ? $summary[0] : null;
} }
if(isset($res['banner_image']) && !empty($res['banner_image'])) { if (isset($res['banner_image']) && ! empty($res['banner_image'])) {
$res['banner_image'] = url(Storage::url($res['banner_image'])); $res['banner_image'] = url(Storage::url($res['banner_image']));
} }
if(isset($res['favourite_posts'])) { if (isset($res['favourite_posts'])) {
$res['favourite_posts'] = collect($res['favourite_posts'])->map(function($id) { $res['favourite_posts'] = collect($res['favourite_posts'])->map(function ($id) {
return StatusService::get($id); return StatusService::get($id);
}) })
->filter(function($post) { ->filter(function ($post) {
return $post && isset($post['account']); return $post && isset($post['account']);
}) })
->values(); ->values();
} }
$res['community_guidelines'] = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : []; $res['community_guidelines'] = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : [];
@ -84,22 +74,22 @@ trait AdminDirectoryController
$res['feature_config'] = [ $res['feature_config'] = [
'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','), 'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','),
'image_quality' => config_cache('pixelfed.image_quality'), 'image_quality' => config_cache('pixelfed.image_quality'),
'optimize_image' => config_cache('pixelfed.optimize_image'), 'optimize_image' => (bool) config_cache('pixelfed.optimize_image'),
'max_photo_size' => config_cache('pixelfed.max_photo_size'), 'max_photo_size' => config_cache('pixelfed.max_photo_size'),
'max_caption_length' => config_cache('pixelfed.max_caption_length'), 'max_caption_length' => config_cache('pixelfed.max_caption_length'),
'max_altext_length' => config_cache('pixelfed.max_altext_length'), 'max_altext_length' => config_cache('pixelfed.max_altext_length'),
'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit'), 'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit'),
'max_account_size' => config_cache('pixelfed.max_account_size'), 'max_account_size' => config_cache('pixelfed.max_account_size'),
'max_album_length' => config_cache('pixelfed.max_album_length'), 'max_album_length' => config_cache('pixelfed.max_album_length'),
'account_deletion' => config_cache('pixelfed.account_deletion'), 'account_deletion' => (bool) config_cache('pixelfed.account_deletion'),
]; ];
if(config_cache('pixelfed.directory.testimonials')) { if (config_cache('pixelfed.directory.testimonials')) {
$testimonials = collect(json_decode(config_cache('pixelfed.directory.testimonials'),true)) $testimonials = collect(json_decode(config_cache('pixelfed.directory.testimonials'), true))
->map(function($t) { ->map(function ($t) {
return [ return [
'profile' => AccountService::get($t['profile_id']), 'profile' => AccountService::get($t['profile_id']),
'body' => $t['body'] 'body' => $t['body'],
]; ];
}); });
$res['testimonials'] = $testimonials; $res['testimonials'] = $testimonials;
@ -108,8 +98,8 @@ trait AdminDirectoryController
$validator = Validator::make($res['feature_config'], [ $validator = Validator::make($res['feature_config'], [
'media_types' => [ 'media_types' => [
'required', 'required',
function ($attribute, $value, $fail) { function ($attribute, $value, $fail) {
if (!in_array('image/jpeg', $value->toArray()) || !in_array('image/png', $value->toArray())) { if (! in_array('image/jpeg', $value->toArray()) || ! in_array('image/png', $value->toArray())) {
$fail('You must enable image/jpeg and image/png support.'); $fail('You must enable image/jpeg and image/png support.');
} }
}, },
@ -120,7 +110,7 @@ trait AdminDirectoryController
'max_account_size' => 'required_if:enforce_account_limit,true|integer|min:1000000', 'max_account_size' => 'required_if:enforce_account_limit,true|integer|min:1000000',
'max_album_length' => 'required|integer|min:4|max:20', 'max_album_length' => 'required|integer|min:4|max:20',
'account_deletion' => 'required|accepted', 'account_deletion' => 'required|accepted',
'max_caption_length' => 'required|integer|min:500|max:10000' 'max_caption_length' => 'required|integer|min:500|max:10000',
]); ]);
$res['requirements_validator'] = $validator->errors(); $res['requirements_validator'] = $validator->errors();
@ -146,11 +136,11 @@ trait AdminDirectoryController
foreach (new \DirectoryIterator($path) as $io) { foreach (new \DirectoryIterator($path) as $io) {
$name = $io->getFilename(); $name = $io->getFilename();
$skip = ['vendor']; $skip = ['vendor'];
if($io->isDot() || in_array($name, $skip)) { if ($io->isDot() || in_array($name, $skip)) {
continue; continue;
} }
if($io->isDir()) { if ($io->isDir()) {
$langs->push(['code' => $name, 'name' => locale_get_display_name($name)]); $langs->push(['code' => $name, 'name' => locale_get_display_name($name)]);
} }
} }
@ -159,25 +149,26 @@ trait AdminDirectoryController
$res['primary_locale'] = config('app.locale'); $res['primary_locale'] = config('app.locale');
$submissionState = Http::withoutVerifying() $submissionState = Http::withoutVerifying()
->post('https://pixelfed.org/api/v1/directory/check-submission', [ ->post('https://pixelfed.org/api/v1/directory/check-submission', [
'domain' => config('pixelfed.domain.app') 'domain' => config('pixelfed.domain.app'),
]); ]);
$res['submission_state'] = $submissionState->json(); $res['submission_state'] = $submissionState->json();
return $res; return $res;
} }
protected function validVal($res, $val, $count = false, $minLen = false) protected function validVal($res, $val, $count = false, $minLen = false)
{ {
if(!isset($res[$val])) { if (! isset($res[$val])) {
return false; return false;
} }
if($count) { if ($count) {
return count($res[$val]) >= $count; return count($res[$val]) >= $count;
} }
if($minLen) { if ($minLen) {
return strlen($res[$val]) >= $minLen; return strlen($res[$val]) >= $minLen;
} }
@ -194,11 +185,11 @@ trait AdminDirectoryController
'favourite_posts' => 'array|max:12', 'favourite_posts' => 'array|max:12',
'favourite_posts.*' => 'distinct', 'favourite_posts.*' => 'distinct',
'privacy_pledge' => 'sometimes', 'privacy_pledge' => 'sometimes',
'banner_image' => 'sometimes|mimes:jpg,png|dimensions:width=1920,height:1080|max:5000' 'banner_image' => 'sometimes|mimes:jpg,png|dimensions:width=1920,height:1080|max:5000',
]); ]);
$config = ConfigCache::firstOrNew([ $config = ConfigCache::firstOrNew([
'k' => 'pixelfed.directory' 'k' => 'pixelfed.directory',
]); ]);
$res = $config->v ? json_decode($config->v, true) : []; $res = $config->v ? json_decode($config->v, true) : [];
@ -208,26 +199,27 @@ trait AdminDirectoryController
$res['contact_email'] = $request->input('contact_email'); $res['contact_email'] = $request->input('contact_email');
$res['privacy_pledge'] = (bool) $request->input('privacy_pledge'); $res['privacy_pledge'] = (bool) $request->input('privacy_pledge');
if($request->filled('location')) { if ($request->filled('location')) {
$exists = (new ISO3166)->name($request->location); $exists = (new ISO3166)->name($request->location);
if($exists) { if ($exists) {
$res['location'] = $request->input('location'); $res['location'] = $request->input('location');
} }
} }
if($request->hasFile('banner_image')) { if ($request->hasFile('banner_image')) {
collect(Storage::files('public/headers')) collect(Storage::files('public/headers'))
->filter(function($name) { ->filter(function ($name) {
$protected = [ $protected = [
'public/headers/.gitignore', 'public/headers/.gitignore',
'public/headers/default.jpg', 'public/headers/default.jpg',
'public/headers/missing.png' 'public/headers/missing.png',
]; ];
return !in_array($name, $protected);
}) return ! in_array($name, $protected);
->each(function($name) { })
Storage::delete($name); ->each(function ($name) {
}); Storage::delete($name);
});
$path = $request->file('banner_image')->storePublicly('public/headers'); $path = $request->file('banner_image')->storePublicly('public/headers');
$res['banner_image'] = $path; $res['banner_image'] = $path;
ConfigCacheService::put('app.banner_image', url(Storage::url($path))); ConfigCacheService::put('app.banner_image', url(Storage::url($path)));
@ -240,9 +232,10 @@ trait AdminDirectoryController
ConfigCacheService::put('pixelfed.directory', $config->v); ConfigCacheService::put('pixelfed.directory', $config->v);
$updated = json_decode($config->v, true); $updated = json_decode($config->v, true);
if(isset($updated['banner_image'])) { if (isset($updated['banner_image'])) {
$updated['banner_image'] = url(Storage::url($updated['banner_image'])); $updated['banner_image'] = url(Storage::url($updated['banner_image']));
} }
return $updated; return $updated;
} }
@ -253,7 +246,7 @@ trait AdminDirectoryController
'open_registration' => (bool) config_cache('pixelfed.open_registration'), 'open_registration' => (bool) config_cache('pixelfed.open_registration'),
'curated_onboarding' => (bool) config_cache('instance.curated_registration.enabled'), 'curated_onboarding' => (bool) config_cache('instance.curated_registration.enabled'),
'activitypub_enabled' => config_cache('federation.activitypub.enabled'), 'activitypub_enabled' => config_cache('federation.activitypub.enabled'),
'oauth_enabled' => config_cache('pixelfed.oauth_enabled'), 'oauth_enabled' => (bool) config_cache('pixelfed.oauth_enabled'),
'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','), 'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','),
'image_quality' => config_cache('pixelfed.image_quality'), 'image_quality' => config_cache('pixelfed.image_quality'),
'optimize_image' => config_cache('pixelfed.optimize_image'), 'optimize_image' => config_cache('pixelfed.optimize_image'),
@ -273,8 +266,8 @@ trait AdminDirectoryController
'oauth_enabled' => 'required|accepted', 'oauth_enabled' => 'required|accepted',
'media_types' => [ 'media_types' => [
'required', 'required',
function ($attribute, $value, $fail) { function ($attribute, $value, $fail) {
if (!in_array('image/jpeg', $value->toArray()) || !in_array('image/png', $value->toArray())) { if (! in_array('image/jpeg', $value->toArray()) || ! in_array('image/png', $value->toArray())) {
$fail('You must enable image/jpeg and image/png support.'); $fail('You must enable image/jpeg and image/png support.');
} }
}, },
@ -285,10 +278,10 @@ trait AdminDirectoryController
'max_account_size' => 'required_if:enforce_account_limit,true|integer|min:1000000', 'max_account_size' => 'required_if:enforce_account_limit,true|integer|min:1000000',
'max_album_length' => 'required|integer|min:4|max:20', 'max_album_length' => 'required|integer|min:4|max:20',
'account_deletion' => 'required|accepted', 'account_deletion' => 'required|accepted',
'max_caption_length' => 'required|integer|min:500|max:10000' 'max_caption_length' => 'required|integer|min:500|max:10000',
]); ]);
if(!$validator->validate()) { if (! $validator->validate()) {
return response()->json($validator->errors(), 422); return response()->json($validator->errors(), 422);
} }
@ -297,6 +290,7 @@ trait AdminDirectoryController
$data = (new PixelfedDirectoryController())->buildListing(); $data = (new PixelfedDirectoryController())->buildListing();
$res = Http::withoutVerifying()->post('https://pixelfed.org/api/v1/directory/submission', $data); $res = Http::withoutVerifying()->post('https://pixelfed.org/api/v1/directory/submission', $data);
return 200; return 200;
} }
@ -304,7 +298,7 @@ trait AdminDirectoryController
{ {
$bannerImage = ConfigCache::whereK('app.banner_image')->first(); $bannerImage = ConfigCache::whereK('app.banner_image')->first();
$directory = ConfigCache::whereK('pixelfed.directory')->first(); $directory = ConfigCache::whereK('pixelfed.directory')->first();
if(!$bannerImage && !$directory || empty($directory->v)) { if (! $bannerImage && ! $directory || empty($directory->v)) {
return; return;
} }
$directoryArr = json_decode($directory->v, true); $directoryArr = json_decode($directory->v, true);
@ -312,12 +306,12 @@ trait AdminDirectoryController
$protected = [ $protected = [
'public/headers/.gitignore', 'public/headers/.gitignore',
'public/headers/default.jpg', 'public/headers/default.jpg',
'public/headers/missing.png' 'public/headers/missing.png',
]; ];
if(!$path || in_array($path, $protected)) { if (! $path || in_array($path, $protected)) {
return; return;
} }
if(Storage::exists($directoryArr['banner_image'])) { if (Storage::exists($directoryArr['banner_image'])) {
Storage::delete($directoryArr['banner_image']); Storage::delete($directoryArr['banner_image']);
} }
@ -328,12 +322,13 @@ trait AdminDirectoryController
$bannerImage->save(); $bannerImage->save();
Cache::forget('api:v1:instance-data-response-v1'); Cache::forget('api:v1:instance-data-response-v1');
ConfigCacheService::put('pixelfed.directory', $directory); ConfigCacheService::put('pixelfed.directory', $directory);
return $bannerImage->v; return $bannerImage->v;
} }
public function directoryGetPopularPosts(Request $request) public function directoryGetPopularPosts(Request $request)
{ {
$ids = Cache::remember('admin:api:popular_posts', 86400, function() { $ids = Cache::remember('admin:api:popular_posts', 86400, function () {
return Status::whereLocal(true) return Status::whereLocal(true)
->whereScope('public') ->whereScope('public')
->whereType('photo') ->whereType('photo')
@ -343,21 +338,21 @@ trait AdminDirectoryController
->pluck('id'); ->pluck('id');
}); });
$res = $ids->map(function($id) { $res = $ids->map(function ($id) {
return StatusService::get($id); return StatusService::get($id);
}) })
->filter(function($post) { ->filter(function ($post) {
return $post && isset($post['account']); return $post && isset($post['account']);
}) })
->values(); ->values();
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
} }
public function directoryGetAddPostByIdSearch(Request $request) public function directoryGetAddPostByIdSearch(Request $request)
{ {
$this->validate($request, [ $this->validate($request, [
'q' => 'required|integer' 'q' => 'required|integer',
]); ]);
$id = $request->input('q'); $id = $request->input('q');
@ -380,11 +375,12 @@ trait AdminDirectoryController
$profile_id = $request->input('profile_id'); $profile_id = $request->input('profile_id');
$testimonials = ConfigCache::whereK('pixelfed.directory.testimonials')->firstOrFail(); $testimonials = ConfigCache::whereK('pixelfed.directory.testimonials')->firstOrFail();
$existing = collect(json_decode($testimonials->v, true)) $existing = collect(json_decode($testimonials->v, true))
->filter(function($t) use($profile_id) { ->filter(function ($t) use ($profile_id) {
return $t['profile_id'] !== $profile_id; return $t['profile_id'] !== $profile_id;
}) })
->values(); ->values();
ConfigCacheService::put('pixelfed.directory.testimonials', $existing); ConfigCacheService::put('pixelfed.directory.testimonials', $existing);
return $existing; return $existing;
} }
@ -392,13 +388,13 @@ trait AdminDirectoryController
{ {
$this->validate($request, [ $this->validate($request, [
'username' => 'required', 'username' => 'required',
'body' => 'required|string|min:5|max:500' 'body' => 'required|string|min:5|max:500',
]); ]);
$user = User::whereUsername($request->input('username'))->whereNull('status')->firstOrFail(); $user = User::whereUsername($request->input('username'))->whereNull('status')->firstOrFail();
$configCache = ConfigCache::firstOrCreate([ $configCache = ConfigCache::firstOrCreate([
'k' => 'pixelfed.directory.testimonials' 'k' => 'pixelfed.directory.testimonials',
]); ]);
$testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]); $testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]);
@ -409,7 +405,7 @@ trait AdminDirectoryController
$testimonials->push([ $testimonials->push([
'profile_id' => (string) $user->profile_id, 'profile_id' => (string) $user->profile_id,
'username' => $request->input('username'), 'username' => $request->input('username'),
'body' => $request->input('body') 'body' => $request->input('body'),
]); ]);
$configCache->v = json_encode($testimonials->toArray()); $configCache->v = json_encode($testimonials->toArray());
@ -417,8 +413,9 @@ trait AdminDirectoryController
ConfigCacheService::put('pixelfed.directory.testimonials', $configCache->v); ConfigCacheService::put('pixelfed.directory.testimonials', $configCache->v);
$res = [ $res = [
'profile' => AccountService::get($user->profile_id), 'profile' => AccountService::get($user->profile_id),
'body' => $request->input('body') 'body' => $request->input('body'),
]; ];
return $res; return $res;
} }
@ -426,7 +423,7 @@ trait AdminDirectoryController
{ {
$this->validate($request, [ $this->validate($request, [
'profile_id' => 'required', 'profile_id' => 'required',
'body' => 'required|string|min:5|max:500' 'body' => 'required|string|min:5|max:500',
]); ]);
$profile_id = $request->input('profile_id'); $profile_id = $request->input('profile_id');
@ -434,18 +431,19 @@ trait AdminDirectoryController
$user = User::whereProfileId($profile_id)->firstOrFail(); $user = User::whereProfileId($profile_id)->firstOrFail();
$configCache = ConfigCache::firstOrCreate([ $configCache = ConfigCache::firstOrCreate([
'k' => 'pixelfed.directory.testimonials' 'k' => 'pixelfed.directory.testimonials',
]); ]);
$testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]); $testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]);
$updated = $testimonials->map(function($t) use($profile_id, $body) { $updated = $testimonials->map(function ($t) use ($profile_id, $body) {
if($t['profile_id'] == $profile_id) { if ($t['profile_id'] == $profile_id) {
$t['body'] = $body; $t['body'] = $body;
} }
return $t; return $t;
}) })
->values(); ->values();
$configCache->v = json_encode($updated); $configCache->v = json_encode($updated);
$configCache->save(); $configCache->save();

View file

@ -7,7 +7,9 @@ use App\Models\InstanceActor;
use App\Page; use App\Page;
use App\Profile; use App\Profile;
use App\Services\AccountService; use App\Services\AccountService;
use App\Services\AdminSettingsService;
use App\Services\ConfigCacheService; use App\Services\ConfigCacheService;
use App\Services\FilesystemService;
use App\User; use App\User;
use App\Util\Site\Config; use App\Util\Site\Config;
use Artisan; use Artisan;
@ -71,6 +73,7 @@ trait AdminSettingsController
'admin_account_id' => 'nullable', 'admin_account_id' => 'nullable',
'regs' => 'required|in:open,filtered,closed', 'regs' => 'required|in:open,filtered,closed',
'account_migration' => 'nullable', 'account_migration' => 'nullable',
'rule_delete' => 'sometimes',
]); ]);
$orb = false; $orb = false;
@ -310,4 +313,573 @@ trait AdminSettingsController
return view('admin.settings.system', compact('sys')); return view('admin.settings.system', compact('sys'));
} }
public function settingsApiFetch(Request $request)
{
$cloud_storage = ConfigCacheService::get('pixelfed.cloud_storage');
$cloud_disk = config('filesystems.cloud');
$cloud_ready = ! empty(config('filesystems.disks.'.$cloud_disk.'.key')) && ! empty(config('filesystems.disks.'.$cloud_disk.'.secret'));
$types = explode(',', ConfigCacheService::get('pixelfed.media_types'));
$rules = ConfigCacheService::get('app.rules') ? json_decode(ConfigCacheService::get('app.rules'), true) : [];
$jpeg = in_array('image/jpg', $types) || in_array('image/jpeg', $types);
$png = in_array('image/png', $types);
$gif = in_array('image/gif', $types);
$mp4 = in_array('video/mp4', $types);
$webp = in_array('image/webp', $types);
$availableAdmins = User::whereIsAdmin(true)->get();
$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');
$accountMigration = (bool) config_cache('federation.migration');
$autoFollow = config_cache('account.autofollow_usernames');
if (strlen($autoFollow) > 3) {
$autoFollow = explode(',', $autoFollow);
}
$res = AdminSettingsService::getAll();
return response()->json($res);
}
public function settingsApiRulesAdd(Request $request)
{
$this->validate($request, [
'rule' => 'required|string|min:5|max:1000',
]);
$rules = ConfigCacheService::get('app.rules');
$val = $request->input('rule');
if (! $rules) {
ConfigCacheService::put('app.rules', json_encode([$val]));
} else {
$json = json_decode($rules, true);
$count = count($json);
if ($count >= 30) {
return response()->json(['message' => 'Max rules limit reached, you can set up to 30 rules at a time.'], 400);
}
$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('api:v2:instance-data-response-v2');
Config::refresh();
return [$val];
}
public function settingsApiRulesDelete(Request $request)
{
$this->validate($request, [
'rule' => 'required|string',
]);
$rules = ConfigCacheService::get('app.rules');
$val = $request->input('rule');
if (! $rules) {
return [];
} else {
$json = json_decode($rules, true);
$idx = array_search($val, $json);
if ($idx !== false) {
unset($json[$idx]);
$json = array_values($json);
}
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('api:v2:instance-data-response-v2');
Config::refresh();
return response()->json($json);
}
public function settingsApiRulesDeleteAll(Request $request)
{
$rules = ConfigCacheService::get('app.rules');
if (! $rules) {
return [];
} else {
ConfigCacheService::put('app.rules', json_encode([]));
}
Cache::forget('api:v1:instance-data:rules');
Cache::forget('api:v1:instance-data-response-v1');
Cache::forget('api:v2:instance-data-response-v2');
Config::refresh();
return response()->json([]);
}
public function settingsApiAutofollowDelete(Request $request)
{
$this->validate($request, [
'username' => 'required|string',
]);
$username = $request->input('username');
$names = [];
$existing = config_cache('account.autofollow_usernames');
if ($existing) {
$names = explode(',', $existing);
}
if (in_array($username, $names)) {
$key = array_search($username, $names);
if ($key !== false) {
unset($names[$key]);
}
}
ConfigCacheService::put('account.autofollow_usernames', implode(',', $names));
return response()->json(['accounts' => array_values($names)]);
}
public function settingsApiAutofollowAdd(Request $request)
{
$this->validate($request, [
'username' => 'required|string',
]);
$username = $request->input('username');
$names = [];
$existing = config_cache('account.autofollow_usernames');
if ($existing) {
$names = explode(',', $existing);
}
if ($existing && count($names)) {
if (count($names) >= 5) {
return response()->json(['message' => 'You can only add up to 5 accounts to be autofollowed.'], 400);
}
if (in_array(strtolower($username), array_map('strtolower', $names))) {
return response()->json(['message' => 'User already exists, please try again.'], 400);
}
}
$p = User::whereUsername($username)->whereNull('status')->first();
if (! $p || in_array($p->username, $names)) {
abort(404);
}
array_push($names, $p->username);
ConfigCacheService::put('account.autofollow_usernames', implode(',', $names));
return response()->json(['accounts' => array_values($names)]);
}
public function settingsApiUpdateType(Request $request, $type)
{
abort_unless(in_array($type, [
'posts',
'platform',
'home',
'landing',
'branding',
'media',
'users',
'storage',
]), 400);
switch ($type) {
case 'home':
return $this->settingsApiUpdateHomeType($request);
break;
case 'landing':
return $this->settingsApiUpdateLandingType($request);
break;
case 'posts':
return $this->settingsApiUpdatePostsType($request);
break;
case 'platform':
return $this->settingsApiUpdatePlatformType($request);
break;
case 'branding':
return $this->settingsApiUpdateBrandingType($request);
break;
case 'media':
return $this->settingsApiUpdateMediaType($request);
break;
case 'users':
return $this->settingsApiUpdateUsersType($request);
break;
case 'storage':
return $this->settingsApiUpdateStorageType($request);
break;
default:
abort(404);
break;
}
}
public function settingsApiUpdateHomeType($request)
{
$this->validate($request, [
'registration_status' => 'required|in:open,filtered,closed',
'cloud_storage' => 'required',
'activitypub_enabled' => 'required',
'account_migration' => 'required',
'mobile_apis' => 'required',
'stories' => 'required',
'instagram_import' => 'required',
'autospam_enabled' => 'required',
]);
$regStatus = $request->input('registration_status');
ConfigCacheService::put('pixelfed.open_registration', $regStatus === 'open');
ConfigCacheService::put('instance.curated_registration.enabled', $regStatus === 'filtered');
$cloudStorage = $request->boolean('cloud_storage');
if ($cloudStorage !== (bool) config_cache('pixelfed.cloud_storage')) {
if (! $cloudStorage) {
ConfigCacheService::put('pixelfed.cloud_storage', false);
} else {
$cloud_disk = config('filesystems.cloud');
$cloud_ready = ! empty(config('filesystems.disks.'.$cloud_disk.'.key')) && ! empty(config('filesystems.disks.'.$cloud_disk.'.secret'));
if (! $cloud_ready) {
return redirect()->back()->withErrors(['cloud_storage' => 'Must configure cloud storage before enabling!']);
} else {
ConfigCacheService::put('pixelfed.cloud_storage', true);
}
}
}
ConfigCacheService::put('federation.activitypub.enabled', $request->boolean('activitypub_enabled'));
ConfigCacheService::put('federation.migration', $request->boolean('account_migration'));
ConfigCacheService::put('pixelfed.oauth_enabled', $request->boolean('mobile_apis'));
ConfigCacheService::put('instance.stories.enabled', $request->boolean('stories'));
ConfigCacheService::put('pixelfed.import.instagram.enabled', $request->boolean('instagram_import'));
ConfigCacheService::put('pixelfed.bouncer.enabled', $request->boolean('autospam_enabled'));
Cache::forget('api:v1:instance-data-response-v1');
Cache::forget('api:v2:instance-data-response-v2');
Cache::forget('api:v1:instance-data:contact');
Config::refresh();
return $request->all();
}
public function settingsApiUpdateLandingType($request)
{
$this->validate($request, [
'current_admin' => 'required',
'show_directory' => 'required',
'show_explore' => 'required',
]);
ConfigCacheService::put('instance.admin.pid', $request->input('current_admin'));
ConfigCacheService::put('instance.landing.show_directory', $request->boolean('show_directory'));
ConfigCacheService::put('instance.landing.show_explore', $request->boolean('show_explore'));
Cache::forget('api:v1:instance-data:rules');
Cache::forget('api:v1:instance-data-response-v1');
Cache::forget('api:v2:instance-data-response-v2');
Cache::forget('api:v1:instance-data:contact');
Config::refresh();
return $request->all();
}
public function settingsApiUpdateMediaType($request)
{
$this->validate($request, [
'image_quality' => 'required|integer|min:1|max:100',
'max_album_length' => 'required|integer|min:1|max:20',
'max_photo_size' => 'required|integer|min:100|max:50000',
'media_types' => 'required',
'optimize_image' => 'required',
'optimize_video' => 'required',
]);
$mediaTypes = $request->input('media_types');
$mediaArray = explode(',', $mediaTypes);
foreach ($mediaArray as $mediaType) {
if (! in_array($mediaType, ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'video/mp4'])) {
return redirect()->back()->withErrors(['media_types' => 'Invalid media type']);
}
}
ConfigCacheService::put('pixelfed.media_types', $request->input('media_types'));
ConfigCacheService::put('pixelfed.image_quality', $request->input('image_quality'));
ConfigCacheService::put('pixelfed.max_album_length', $request->input('max_album_length'));
ConfigCacheService::put('pixelfed.max_photo_size', $request->input('max_photo_size'));
ConfigCacheService::put('pixelfed.optimize_image', $request->boolean('optimize_image'));
ConfigCacheService::put('pixelfed.optimize_video', $request->boolean('optimize_video'));
Cache::forget('api:v1:instance-data:rules');
Cache::forget('api:v1:instance-data-response-v1');
Cache::forget('api:v2:instance-data-response-v2');
Cache::forget('api:v1:instance-data:contact');
Config::refresh();
return $request->all();
}
public function settingsApiUpdateBrandingType($request)
{
$this->validate($request, [
'name' => 'required',
'short_description' => 'required',
'long_description' => 'required',
]);
ConfigCacheService::put('app.name', $request->input('name'));
ConfigCacheService::put('app.short_description', $request->input('short_description'));
ConfigCacheService::put('app.description', $request->input('long_description'));
Cache::forget('api:v1:instance-data:rules');
Cache::forget('api:v1:instance-data-response-v1');
Cache::forget('api:v2:instance-data-response-v2');
Cache::forget('api:v1:instance-data:contact');
Config::refresh();
return $request->all();
}
public function settingsApiUpdatePostsType($request)
{
$this->validate($request, [
'max_caption_length' => 'required|integer|min:5|max:10000',
'max_altext_length' => 'required|integer|min:5|max:40000',
]);
ConfigCacheService::put('pixelfed.max_caption_length', $request->input('max_caption_length'));
ConfigCacheService::put('pixelfed.max_altext_length', $request->input('max_altext_length'));
$res = [
'max_caption_length' => $request->input('max_caption_length'),
'max_altext_length' => $request->input('max_altext_length'),
];
Cache::forget('api:v1:instance-data:rules');
Cache::forget('api:v1:instance-data-response-v1');
Cache::forget('api:v2:instance-data-response-v2');
Config::refresh();
return $res;
}
public function settingsApiUpdatePlatformType($request)
{
$this->validate($request, [
'allow_app_registration' => 'required',
'app_registration_rate_limit_attempts' => 'required|integer|min:1',
'app_registration_rate_limit_decay' => 'required|integer|min:1',
'app_registration_confirm_rate_limit_attempts' => 'required|integer|min:1',
'app_registration_confirm_rate_limit_decay' => 'required|integer|min:1',
'allow_post_embeds' => 'required',
'allow_profile_embeds' => 'required',
'captcha_enabled' => 'required',
'captcha_on_login' => 'required_if_accepted:captcha_enabled',
'captcha_on_register' => 'required_if_accepted:captcha_enabled',
'captcha_secret' => 'required_if_accepted:captcha_enabled',
'captcha_sitekey' => 'required_if_accepted:captcha_enabled',
'custom_emoji_enabled' => 'required',
]);
ConfigCacheService::put('pixelfed.allow_app_registration', $request->boolean('allow_app_registration'));
ConfigCacheService::put('pixelfed.app_registration_rate_limit_attempts', $request->input('app_registration_rate_limit_attempts'));
ConfigCacheService::put('pixelfed.app_registration_rate_limit_decay', $request->input('app_registration_rate_limit_decay'));
ConfigCacheService::put('pixelfed.app_registration_confirm_rate_limit_attempts', $request->input('app_registration_confirm_rate_limit_attempts'));
ConfigCacheService::put('pixelfed.app_registration_confirm_rate_limit_decay', $request->input('app_registration_confirm_rate_limit_decay'));
ConfigCacheService::put('instance.embed.post', $request->boolean('allow_post_embeds'));
ConfigCacheService::put('instance.embed.profile', $request->boolean('allow_profile_embeds'));
ConfigCacheService::put('federation.custom_emoji.enabled', $request->boolean('custom_emoji_enabled'));
$captcha = $request->boolean('captcha_enabled');
if ($captcha) {
$secret = $request->input('captcha_secret');
$sitekey = $request->input('captcha_sitekey');
if (config_cache('captcha.secret') != $secret && strpos($secret, '*') === false) {
ConfigCacheService::put('captcha.secret', $secret);
}
if (config_cache('captcha.sitekey') != $sitekey && strpos($sitekey, '*') === false) {
ConfigCacheService::put('captcha.sitekey', $sitekey);
}
ConfigCacheService::put('captcha.active.login', $request->boolean('captcha_on_login'));
ConfigCacheService::put('captcha.active.register', $request->boolean('captcha_on_register'));
ConfigCacheService::put('captcha.triggers.login.enabled', $request->boolean('captcha_on_login'));
ConfigCacheService::put('captcha.enabled', true);
} else {
ConfigCacheService::put('captcha.enabled', false);
}
$res = [
'allow_app_registration' => $request->boolean('allow_app_registration'),
'app_registration_rate_limit_attempts' => $request->input('app_registration_rate_limit_attempts'),
'app_registration_rate_limit_decay' => $request->input('app_registration_rate_limit_decay'),
'app_registration_confirm_rate_limit_attempts' => $request->input('app_registration_confirm_rate_limit_attempts'),
'app_registration_confirm_rate_limit_decay' => $request->input('app_registration_confirm_rate_limit_decay'),
'allow_post_embeds' => $request->boolean('allow_post_embeds'),
'allow_profile_embeds' => $request->boolean('allow_profile_embeds'),
'captcha_enabled' => $request->boolean('captcha_enabled'),
'captcha_on_login' => $request->boolean('captcha_on_login'),
'captcha_on_register' => $request->boolean('captcha_on_register'),
'captcha_secret' => $request->input('captcha_secret'),
'captcha_sitekey' => $request->input('captcha_sitekey'),
'custom_emoji_enabled' => $request->boolean('custom_emoji_enabled'),
];
Cache::forget('api:v1:instance-data:rules');
Cache::forget('api:v1:instance-data-response-v1');
Cache::forget('api:v2:instance-data-response-v2');
Config::refresh();
return $res;
}
public function settingsApiUpdateUsersType($request)
{
$this->validate($request, [
'require_email_verification' => 'required',
'enforce_account_limit' => 'required',
'max_account_size' => 'required|integer|min:50000',
'admin_autofollow' => 'required',
'admin_autofollow_accounts' => 'sometimes',
'max_user_blocks' => 'required|integer|min:0|max:5000',
'max_user_mutes' => 'required|integer|min:0|max:5000',
'max_domain_blocks' => 'required|integer|min:0|max:5000',
]);
$adminAutofollow = $request->boolean('admin_autofollow');
$adminAutofollowAccounts = $request->input('admin_autofollow_accounts');
if ($adminAutofollow) {
if ($request->filled('admin_autofollow_accounts')) {
$names = [];
$existing = config_cache('account.autofollow_usernames');
if ($existing) {
$names = explode(',', $existing);
foreach (array_map('strtolower', $adminAutofollowAccounts) as $afc) {
if (in_array(strtolower($afc), array_map('strtolower', $names))) {
continue;
}
$names[] = $afc;
}
} else {
$names = $adminAutofollowAccounts;
}
if (! $names || count($names) == 0) {
return response()->json(['message' => 'You need to assign autofollow accounts before you can enable it.'], 400);
}
if (count($names) > 5) {
return response()->json(['message' => 'You can only add up to 5 accounts to be autofollowed.'.json_encode($names)], 400);
}
$autofollows = User::whereIn('username', $names)->whereNull('status')->pluck('username');
$adminAutofollowAccounts = $autofollows->implode(',');
ConfigCacheService::put('account.autofollow_usernames', $adminAutofollowAccounts);
} else {
return response()->json(['message' => 'You need to assign autofollow accounts before you can enable it.'], 400);
}
}
ConfigCacheService::put('pixelfed.enforce_email_verification', $request->boolean('require_email_verification'));
ConfigCacheService::put('pixelfed.enforce_account_limit', $request->boolean('enforce_account_limit'));
ConfigCacheService::put('pixelfed.max_account_size', $request->input('max_account_size'));
ConfigCacheService::put('account.autofollow', $request->boolean('admin_autofollow'));
ConfigCacheService::put('instance.user_filters.max_user_blocks', (int) $request->input('max_user_blocks'));
ConfigCacheService::put('instance.user_filters.max_user_mutes', (int) $request->input('max_user_mutes'));
ConfigCacheService::put('instance.user_filters.max_domain_blocks', (int) $request->input('max_domain_blocks'));
$res = [
'require_email_verification' => $request->boolean('require_email_verification'),
'enforce_account_limit' => $request->boolean('enforce_account_limit'),
'admin_autofollow' => $request->boolean('admin_autofollow'),
'admin_autofollow_accounts' => $adminAutofollowAccounts,
'max_user_blocks' => $request->input('max_user_blocks'),
'max_user_mutes' => $request->input('max_user_mutes'),
'max_domain_blocks' => $request->input('max_domain_blocks'),
];
Cache::forget('api:v1:instance-data:rules');
Cache::forget('api:v1:instance-data-response-v1');
Cache::forget('api:v2:instance-data-response-v2');
Config::refresh();
return $res;
}
public function settingsApiUpdateStorageType($request)
{
$this->validate($request, [
'primary_disk' => 'required|in:local,cloud',
'update_disk' => 'sometimes',
'disk_config' => 'required_if_accepted:update_disk',
'disk_config.driver' => 'required|in:s3,spaces',
'disk_config.key' => 'required',
'disk_config.secret' => 'required',
'disk_config.region' => 'required',
'disk_config.bucket' => 'required',
'disk_config.visibility' => 'required',
'disk_config.endpoint' => 'required',
'disk_config.url' => 'nullable',
]);
ConfigCacheService::put('pixelfed.cloud_storage', $request->input('primary_disk') === 'cloud');
$res = [
'primary_disk' => $request->input('primary_disk'),
];
if ($request->has('update_disk')) {
$res['disk_config'] = $request->input('disk_config');
$changes = [];
$dkey = $request->input('disk_config.driver') === 's3' ? 'filesystems.disks.s3.' : 'filesystems.disks.spaces.';
$key = $request->input('disk_config.key');
$ckey = null;
$secret = $request->input('disk_config.secret');
$csecret = null;
$region = $request->input('disk_config.region');
$bucket = $request->input('disk_config.bucket');
$visibility = $request->input('disk_config.visibility');
$url = $request->input('disk_config.url');
$endpoint = $request->input('disk_config.endpoint');
if (strpos($key, '*') === false && $key != config_cache($dkey.'key')) {
array_push($changes, 'key');
} else {
$ckey = config_cache($dkey.'key');
}
if (strpos($secret, '*') === false && $secret != config_cache($dkey.'secret')) {
array_push($changes, 'secret');
} else {
$csecret = config_cache($dkey.'secret');
}
if ($region != config_cache($dkey.'region')) {
array_push($changes, 'region');
}
if ($bucket != config_cache($dkey.'bucket')) {
array_push($changes, 'bucket');
}
if ($visibility != config_cache($dkey.'visibility')) {
array_push($changes, 'visibility');
}
if ($url != config_cache($dkey.'url')) {
array_push($changes, 'url');
}
if ($endpoint != config_cache($dkey.'endpoint')) {
array_push($changes, 'endpoint');
}
if ($changes && count($changes)) {
$isValid = FilesystemService::getVerifyCredentials(
$ckey ?? $key,
$csecret ?? $secret,
$region,
$bucket,
$endpoint,
);
if (! $isValid) {
return response()->json(['error' => true, 's3_vce' => true, 'message' => "<div class='border border-danger text-danger p-3 font-weight-bold rounded-lg'>The S3/Spaces credentials you provided are invalid, or the bucket does not have the proper permissions.</div><br/>Please check all fields and try again.<br/><br/><strong>Any cloud storage configuration changes you made have NOT been saved due to invalid credentials.</strong>"], 400);
}
}
$res['changes'] = json_encode($changes);
}
Cache::forget('api:v1:instance-data:rules');
Cache::forget('api:v1:instance-data-response-v1');
Cache::forget('api:v2:instance-data-response-v2');
Config::refresh();
return $res;
}
} }

View file

@ -424,7 +424,7 @@ class AdminController extends Controller
public function customEmojiHome(Request $request) public function customEmojiHome(Request $request)
{ {
if(!config('federation.custom_emoji.enabled')) { if(!(bool) config_cache('federation.custom_emoji.enabled')) {
return view('admin.custom-emoji.not-enabled'); return view('admin.custom-emoji.not-enabled');
} }
$this->validate($request, [ $this->validate($request, [
@ -497,7 +497,7 @@ class AdminController extends Controller
public function customEmojiToggleActive(Request $request, $id) public function customEmojiToggleActive(Request $request, $id)
{ {
abort_unless(config('federation.custom_emoji.enabled'), 404); abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
$emoji = CustomEmoji::findOrFail($id); $emoji = CustomEmoji::findOrFail($id);
$emoji->disabled = !$emoji->disabled; $emoji->disabled = !$emoji->disabled;
$emoji->save(); $emoji->save();
@ -508,13 +508,13 @@ class AdminController extends Controller
public function customEmojiAdd(Request $request) public function customEmojiAdd(Request $request)
{ {
abort_unless(config('federation.custom_emoji.enabled'), 404); abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
return view('admin.custom-emoji.add'); return view('admin.custom-emoji.add');
} }
public function customEmojiStore(Request $request) public function customEmojiStore(Request $request)
{ {
abort_unless(config('federation.custom_emoji.enabled'), 404); abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
$this->validate($request, [ $this->validate($request, [
'shortcode' => [ 'shortcode' => [
'required', 'required',
@ -545,7 +545,7 @@ class AdminController extends Controller
public function customEmojiDelete(Request $request, $id) public function customEmojiDelete(Request $request, $id)
{ {
abort_unless(config('federation.custom_emoji.enabled'), 404); abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
$emoji = CustomEmoji::findOrFail($id); $emoji = CustomEmoji::findOrFail($id);
Storage::delete("public/{$emoji->media_path}"); Storage::delete("public/{$emoji->media_path}");
Cache::forget('pf:custom_emoji'); Cache::forget('pf:custom_emoji');
@ -555,7 +555,7 @@ class AdminController extends Controller
public function customEmojiShowDuplicates(Request $request, $id) public function customEmojiShowDuplicates(Request $request, $id)
{ {
abort_unless(config('federation.custom_emoji.enabled'), 404); abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
$emoji = CustomEmoji::orderBy('id')->whereDisabled(false)->whereShortcode($id)->firstOrFail(); $emoji = CustomEmoji::orderBy('id')->whereDisabled(false)->whereShortcode($id)->firstOrFail();
$emojis = CustomEmoji::whereShortcode($id)->where('id', '!=', $emoji->id)->cursorPaginate(10); $emojis = CustomEmoji::whereShortcode($id)->where('id', '!=', $emoji->id)->cursorPaginate(10);
return view('admin.custom-emoji.duplicates', compact('emoji', 'emojis')); return view('admin.custom-emoji.duplicates', compact('emoji', 'emojis'));

View file

@ -131,7 +131,7 @@ class ApiV1Controller extends Controller
*/ */
public function apps(Request $request) public function apps(Request $request)
{ {
abort_if(! config_cache('pixelfed.oauth_enabled'), 404); abort_if(! (bool) config_cache('pixelfed.oauth_enabled'), 404);
$this->validate($request, [ $this->validate($request, [
'client_name' => 'required', 'client_name' => 'required',
@ -1103,7 +1103,7 @@ class ApiV1Controller extends Controller
} }
$count = UserFilterService::blockCount($pid); $count = UserFilterService::blockCount($pid);
$maxLimit = intval(config('instance.user_filters.max_user_blocks')); $maxLimit = (int) config_cache('instance.user_filters.max_user_blocks');
if ($count == 0) { if ($count == 0) {
$filterCount = UserFilter::whereUserId($pid) $filterCount = UserFilter::whereUserId($pid)
->whereFilterType('block') ->whereFilterType('block')
@ -1632,7 +1632,7 @@ class ApiV1Controller extends Controller
return [ return [
'uri' => config('pixelfed.domain.app'), 'uri' => config('pixelfed.domain.app'),
'title' => config('app.name'), 'title' => config_cache('app.name'),
'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'),
@ -1650,11 +1650,11 @@ class ApiV1Controller extends Controller
'configuration' => [ 'configuration' => [
'media_attachments' => [ 'media_attachments' => [
'image_matrix_limit' => 16777216, 'image_matrix_limit' => 16777216,
'image_size_limit' => config('pixelfed.max_photo_size') * 1024, 'image_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
'supported_mime_types' => explode(',', config('pixelfed.media_types')), 'supported_mime_types' => explode(',', config_cache('pixelfed.media_types')),
'video_frame_rate_limit' => 120, 'video_frame_rate_limit' => 120,
'video_matrix_limit' => 2304000, 'video_matrix_limit' => 2304000,
'video_size_limit' => config('pixelfed.max_photo_size') * 1024, 'video_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
], ],
'polls' => [ 'polls' => [
'max_characters_per_option' => 50, 'max_characters_per_option' => 50,
@ -1665,7 +1665,7 @@ class ApiV1Controller extends Controller
'statuses' => [ 'statuses' => [
'characters_reserved_per_url' => 23, 'characters_reserved_per_url' => 23,
'max_characters' => (int) config_cache('pixelfed.max_caption_length'), 'max_characters' => (int) config_cache('pixelfed.max_caption_length'),
'max_media_attachments' => (int) config('pixelfed.max_album_length'), 'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'),
], ],
], ],
]; ];
@ -2145,7 +2145,7 @@ class ApiV1Controller extends Controller
} }
$count = UserFilterService::muteCount($pid); $count = UserFilterService::muteCount($pid);
$maxLimit = intval(config('instance.user_filters.max_user_mutes')); $maxLimit = (int) config_cache('instance.user_filters.max_user_mutes');
if ($count == 0) { if ($count == 0) {
$filterCount = UserFilter::whereUserId($pid) $filterCount = UserFilter::whereUserId($pid)
->whereFilterType('mute') ->whereFilterType('mute')
@ -3308,9 +3308,9 @@ class ApiV1Controller extends Controller
abort_unless($request->user()->tokenCan('write'), 403); abort_unless($request->user()->tokenCan('write'), 403);
$this->validate($request, [ $this->validate($request, [
'status' => 'nullable|string|max:' . config_cache('pixelfed.max_caption_length'), 'status' => 'nullable|string|max:'.(int) config_cache('pixelfed.max_caption_length'),
'in_reply_to_id' => 'nullable', 'in_reply_to_id' => 'nullable',
'media_ids' => 'sometimes|array|max:'.config_cache('pixelfed.max_album_length'), 'media_ids' => 'sometimes|array|max:'.(int) config_cache('pixelfed.max_album_length'),
'sensitive' => 'nullable', 'sensitive' => 'nullable',
'visibility' => 'string|in:private,unlisted,public', 'visibility' => 'string|in:private,unlisted,public',
'spoiler_text' => 'sometimes|max:140', 'spoiler_text' => 'sometimes|max:140',
@ -3436,7 +3436,7 @@ class ApiV1Controller extends Controller
$mimes = []; $mimes = [];
foreach ($ids as $k => $v) { foreach ($ids as $k => $v) {
if ($k + 1 > config_cache('pixelfed.max_album_length')) { if ($k + 1 > (int) config_cache('pixelfed.max_album_length')) {
continue; continue;
} }
$m = Media::whereUserId($user->id)->whereNull('status_id')->findOrFail($v); $m = Media::whereUserId($user->id)->whereNull('status_id')->findOrFail($v);

View file

@ -72,7 +72,7 @@ class DomainBlockController extends Controller
abort_if(config_cache('pixelfed.domain.app') == $domain, 400, 'Cannot ban your own server'); abort_if(config_cache('pixelfed.domain.app') == $domain, 400, 'Cannot ban your own server');
$existingCount = UserDomainBlock::whereProfileId($pid)->count(); $existingCount = UserDomainBlock::whereProfileId($pid)->count();
$maxLimit = config('instance.user_filters.max_domain_blocks'); $maxLimit = (int) config_cache('instance.user_filters.max_domain_blocks');
$errorMsg = __('profile.block.domain.max', ['max' => $maxLimit]); $errorMsg = __('profile.block.domain.max', ['max' => $maxLimit]);
abort_if($existingCount >= $maxLimit, 400, $errorMsg); abort_if($existingCount >= $maxLimit, 400, $errorMsg);

View file

@ -62,7 +62,7 @@ class ForgotPasswordController extends Controller
usleep(random_int(100000, 3000000)); usleep(random_int(100000, 3000000));
if(config('captcha.enabled')) { if((bool) config_cache('captcha.enabled')) {
$rules = [ $rules = [
'email' => 'required|email', 'email' => 'required|email',
'h-captcha-response' => 'required|captcha' 'h-captcha-response' => 'required|captcha'

View file

@ -74,10 +74,10 @@ class LoginController extends Controller
$messages = []; $messages = [];
if( if(
config('captcha.enabled') || (bool) config_cache('captcha.enabled') &&
config('captcha.active.login') || (bool) config_cache('captcha.active.login') ||
( (
config('captcha.triggers.login.enabled') && (bool) config_cache('captcha.triggers.login.enabled') &&
request()->session()->has('login_attempts') && request()->session()->has('login_attempts') &&
request()->session()->get('login_attempts') >= config('captcha.triggers.login.attempts') request()->session()->get('login_attempts') >= config('captcha.triggers.login.attempts')
) )

View file

@ -137,7 +137,7 @@ class RegisterController extends Controller
'password' => 'required|string|min:'.config('pixelfed.min_password_length').'|confirmed', 'password' => 'required|string|min:'.config('pixelfed.min_password_length').'|confirmed',
]; ];
if(config('captcha.enabled') || config('captcha.active.register')) { if((bool) config_cache('captcha.enabled') && (bool) config_cache('captcha.active.register')) {
$rules['h-captcha-response'] = 'required|captcha'; $rules['h-captcha-response'] = 'required|captcha';
} }

View file

@ -50,7 +50,7 @@ class ResetPasswordController extends Controller
{ {
usleep(random_int(100000, 3000000)); usleep(random_int(100000, 3000000));
if(config('captcha.enabled')) { if((bool) config_cache('captcha.enabled')) {
return [ return [
'token' => 'required', 'token' => 'required',
'email' => 'required|email', 'email' => 'required|email',

View file

@ -741,7 +741,7 @@ class ComposeController extends Controller
case 'image/jpeg': case 'image/jpeg':
case 'image/png': case 'image/png':
case 'video/mp4': case 'video/mp4':
$finished = config_cache('pixelfed.cloud_storage') ? (bool) $media->cdn_url : (bool) $media->processed_at; $finished = (bool) config_cache('pixelfed.cloud_storage') ? (bool) $media->cdn_url : (bool) $media->processed_at;
break; break;
default: default:

View file

@ -2,57 +2,42 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Jobs\InboxPipeline\{ use App\Jobs\InboxPipeline\DeleteWorker;
DeleteWorker, use App\Jobs\InboxPipeline\InboxValidator;
InboxWorker, use App\Jobs\InboxPipeline\InboxWorker;
InboxValidator use App\Profile;
};
use App\Jobs\RemoteFollowPipeline\RemoteFollowPipeline;
use App\{
AccountLog,
Like,
Profile,
Status,
User
};
use App\Util\Lexer\Nickname;
use App\Util\Webfinger\Webfinger;
use Auth;
use Cache;
use Carbon\Carbon;
use Illuminate\Http\Request;
use League\Fractal;
use App\Util\Site\Nodeinfo;
use App\Util\ActivityPub\{
Helpers,
HttpSignature,
Outbox
};
use Zttp\Zttp;
use App\Services\InstanceService;
use App\Services\AccountService; use App\Services\AccountService;
use App\Services\InstanceService;
use App\Status;
use App\Util\Lexer\Nickname;
use App\Util\Site\Nodeinfo;
use App\Util\Webfinger\Webfinger;
use Cache;
use Illuminate\Http\Request;
class FederationController extends Controller class FederationController extends Controller
{ {
public function nodeinfoWellKnown() public function nodeinfoWellKnown()
{ {
abort_if(!config('federation.nodeinfo.enabled'), 404); abort_if(! config('federation.nodeinfo.enabled'), 404);
return response()->json(Nodeinfo::wellKnown(), 200, [], JSON_UNESCAPED_SLASHES) return response()->json(Nodeinfo::wellKnown(), 200, [], JSON_UNESCAPED_SLASHES)
->header('Access-Control-Allow-Origin','*'); ->header('Access-Control-Allow-Origin', '*');
} }
public function nodeinfo() public function nodeinfo()
{ {
abort_if(!config('federation.nodeinfo.enabled'), 404); abort_if(! config('federation.nodeinfo.enabled'), 404);
return response()->json(Nodeinfo::get(), 200, [], JSON_UNESCAPED_SLASHES) return response()->json(Nodeinfo::get(), 200, [], JSON_UNESCAPED_SLASHES)
->header('Access-Control-Allow-Origin','*'); ->header('Access-Control-Allow-Origin', '*');
} }
public function webfinger(Request $request) public function webfinger(Request $request)
{ {
if (!config('federation.webfinger.enabled') || if (! config('federation.webfinger.enabled') ||
!$request->has('resource') || ! $request->has('resource') ||
!$request->filled('resource') ! $request->filled('resource')
) { ) {
return response('', 400); return response('', 400);
} }
@ -60,55 +45,56 @@ class FederationController extends Controller
$resource = $request->input('resource'); $resource = $request->input('resource');
$domain = config('pixelfed.domain.app'); $domain = config('pixelfed.domain.app');
if(config('federation.activitypub.sharedInbox') && if (config('federation.activitypub.sharedInbox') &&
$resource == 'acct:' . $domain . '@' . $domain) { $resource == 'acct:'.$domain.'@'.$domain) {
$res = [ $res = [
'subject' => 'acct:' . $domain . '@' . $domain, 'subject' => 'acct:'.$domain.'@'.$domain,
'aliases' => [ 'aliases' => [
'https://' . $domain . '/i/actor' 'https://'.$domain.'/i/actor',
], ],
'links' => [ 'links' => [
[ [
'rel' => 'http://webfinger.net/rel/profile-page', 'rel' => 'http://webfinger.net/rel/profile-page',
'type' => 'text/html', 'type' => 'text/html',
'href' => 'https://' . $domain . '/site/kb/instance-actor' 'href' => 'https://'.$domain.'/site/kb/instance-actor',
], ],
[ [
'rel' => 'self', 'rel' => 'self',
'type' => 'application/activity+json', 'type' => 'application/activity+json',
'href' => 'https://' . $domain . '/i/actor' 'href' => 'https://'.$domain.'/i/actor',
] ],
] ],
]; ];
return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES); return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
} }
$hash = hash('sha256', $resource); $hash = hash('sha256', $resource);
$key = 'federation:webfinger:sha256:' . $hash; $key = 'federation:webfinger:sha256:'.$hash;
if($cached = Cache::get($key)) { if ($cached = Cache::get($key)) {
return response()->json($cached, 200, [], JSON_UNESCAPED_SLASHES); return response()->json($cached, 200, [], JSON_UNESCAPED_SLASHES);
} }
if(strpos($resource, $domain) == false) { if (strpos($resource, $domain) == false) {
return response('', 400); return response('', 400);
} }
$parsed = Nickname::normalizeProfileUrl($resource); $parsed = Nickname::normalizeProfileUrl($resource);
if(empty($parsed) || $parsed['domain'] !== $domain) { if (empty($parsed) || $parsed['domain'] !== $domain) {
return response('', 400); return response('', 400);
} }
$username = $parsed['username']; $username = $parsed['username'];
$profile = Profile::whereNull('domain')->whereUsername($username)->first(); $profile = Profile::whereNull('domain')->whereUsername($username)->first();
if(!$profile || $profile->status !== null) { if (! $profile || $profile->status !== null) {
return response('', 400); return response('', 400);
} }
$webfinger = (new Webfinger($profile))->generate(); $webfinger = (new Webfinger($profile))->generate();
Cache::put($key, $webfinger, 1209600); Cache::put($key, $webfinger, 1209600);
return response()->json($webfinger, 200, [], JSON_UNESCAPED_SLASHES) return response()->json($webfinger, 200, [], JSON_UNESCAPED_SLASHES)
->header('Access-Control-Allow-Origin','*'); ->header('Access-Control-Allow-Origin', '*');
} }
public function hostMeta(Request $request) public function hostMeta(Request $request)
{ {
abort_if(!config('federation.webfinger.enabled'), 404); abort_if(! config('federation.webfinger.enabled'), 404);
$path = route('well-known.webfinger'); $path = route('well-known.webfinger');
$xml = '<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" type="application/xrd+xml" template="'.$path.'?resource={uri}"/></XRD>'; $xml = '<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" type="application/xrd+xml" template="'.$path.'?resource={uri}"/></XRD>';
@ -118,19 +104,19 @@ class FederationController extends Controller
public function userOutbox(Request $request, $username) public function userOutbox(Request $request, $username)
{ {
abort_if(!config_cache('federation.activitypub.enabled'), 404); abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
if(!$request->wantsJson()) { if (! $request->wantsJson()) {
return redirect('/' . $username); return redirect('/'.$username);
} }
$id = AccountService::usernameToId($username); $id = AccountService::usernameToId($username);
abort_if(!$id, 404); abort_if(! $id, 404);
$account = AccountService::get($id); $account = AccountService::get($id);
abort_if(!$account || !isset($account['statuses_count']), 404); abort_if(! $account || ! isset($account['statuses_count']), 404);
$res = [ $res = [
'@context' => 'https://www.w3.org/ns/activitystreams', '@context' => 'https://www.w3.org/ns/activitystreams',
'id' => 'https://' . config('pixelfed.domain.app') . '/users/' . $username . '/outbox', 'id' => 'https://'.config('pixelfed.domain.app').'/users/'.$username.'/outbox',
'type' => 'OrderedCollection', 'type' => 'OrderedCollection',
'totalItems' => $account['statuses_count'] ?? 0, 'totalItems' => $account['statuses_count'] ?? 0,
]; ];
@ -140,135 +126,145 @@ class FederationController extends Controller
public function userInbox(Request $request, $username) public function userInbox(Request $request, $username)
{ {
abort_if(!config_cache('federation.activitypub.enabled'), 404); abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
abort_if(!config('federation.activitypub.inbox'), 404); abort_if(! config('federation.activitypub.inbox'), 404);
$headers = $request->headers->all(); $headers = $request->headers->all();
$payload = $request->getContent(); $payload = $request->getContent();
if(!$payload || empty($payload)) { if (! $payload || empty($payload)) {
return; return;
} }
$obj = json_decode($payload, true, 8); $obj = json_decode($payload, true, 8);
if(!isset($obj['id'])) { if (! isset($obj['id'])) {
return; return;
} }
$domain = parse_url($obj['id'], PHP_URL_HOST); $domain = parse_url($obj['id'], PHP_URL_HOST);
if(in_array($domain, InstanceService::getBannedDomains())) { if (in_array($domain, InstanceService::getBannedDomains())) {
return; return;
} }
if(isset($obj['type']) && $obj['type'] === 'Delete') { if (isset($obj['type']) && $obj['type'] === 'Delete') {
if(isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) { if (isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) {
if($obj['object']['type'] === 'Person') { if ($obj['object']['type'] === 'Person') {
if(Profile::whereRemoteUrl($obj['object']['id'])->exists()) { if (Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox'); dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox');
return; return;
} }
} }
if($obj['object']['type'] === 'Tombstone') { if ($obj['object']['type'] === 'Tombstone') {
if(Status::whereObjectUrl($obj['object']['id'])->exists()) { if (Status::whereObjectUrl($obj['object']['id'])->exists()) {
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete'); dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
return; return;
} }
} }
if($obj['object']['type'] === 'Story') { if ($obj['object']['type'] === 'Story') {
dispatch(new DeleteWorker($headers, $payload))->onQueue('story'); dispatch(new DeleteWorker($headers, $payload))->onQueue('story');
return; return;
} }
} }
return; return;
} else if( isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) { } elseif (isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) {
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('follow'); dispatch(new InboxValidator($username, $headers, $payload))->onQueue('follow');
} else { } else {
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high'); dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high');
} }
return;
} }
public function sharedInbox(Request $request) public function sharedInbox(Request $request)
{ {
abort_if(!config_cache('federation.activitypub.enabled'), 404); abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
abort_if(!config('federation.activitypub.sharedInbox'), 404); abort_if(! config('federation.activitypub.sharedInbox'), 404);
$headers = $request->headers->all(); $headers = $request->headers->all();
$payload = $request->getContent(); $payload = $request->getContent();
if(!$payload || empty($payload)) { if (! $payload || empty($payload)) {
return; return;
} }
$obj = json_decode($payload, true, 8); $obj = json_decode($payload, true, 8);
if(!isset($obj['id'])) { if (! isset($obj['id'])) {
return; return;
} }
$domain = parse_url($obj['id'], PHP_URL_HOST); $domain = parse_url($obj['id'], PHP_URL_HOST);
if(in_array($domain, InstanceService::getBannedDomains())) { if (in_array($domain, InstanceService::getBannedDomains())) {
return; return;
} }
if(isset($obj['type']) && $obj['type'] === 'Delete') { if (isset($obj['type']) && $obj['type'] === 'Delete') {
if(isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) { if (isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) {
if($obj['object']['type'] === 'Person') { if ($obj['object']['type'] === 'Person') {
if(Profile::whereRemoteUrl($obj['object']['id'])->exists()) { if (Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox'); dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox');
return; return;
} }
} }
if($obj['object']['type'] === 'Tombstone') { if ($obj['object']['type'] === 'Tombstone') {
if(Status::whereObjectUrl($obj['object']['id'])->exists()) { if (Status::whereObjectUrl($obj['object']['id'])->exists()) {
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete'); dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
return; return;
} }
} }
if($obj['object']['type'] === 'Story') { if ($obj['object']['type'] === 'Story') {
dispatch(new DeleteWorker($headers, $payload))->onQueue('story'); dispatch(new DeleteWorker($headers, $payload))->onQueue('story');
return; return;
} }
} }
return; return;
} else if( isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) { } elseif (isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) {
dispatch(new InboxWorker($headers, $payload))->onQueue('follow'); dispatch(new InboxWorker($headers, $payload))->onQueue('follow');
} else { } else {
dispatch(new InboxWorker($headers, $payload))->onQueue('shared'); dispatch(new InboxWorker($headers, $payload))->onQueue('shared');
} }
return;
} }
public function userFollowing(Request $request, $username) public function userFollowing(Request $request, $username)
{ {
abort_if(!config_cache('federation.activitypub.enabled'), 404); abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
$id = AccountService::usernameToId($username); $id = AccountService::usernameToId($username);
abort_if(!$id, 404); abort_if(! $id, 404);
$account = AccountService::get($id); $account = AccountService::get($id);
abort_if(!$account || !isset($account['following_count']), 404); abort_if(! $account || ! isset($account['following_count']), 404);
$obj = [ $obj = [
'@context' => 'https://www.w3.org/ns/activitystreams', '@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $request->getUri(), 'id' => $request->getUri(),
'type' => 'OrderedCollection', 'type' => 'OrderedCollection',
'totalItems' => $account['following_count'] ?? 0, 'totalItems' => $account['following_count'] ?? 0,
]; ];
return response()->json($obj)->header('Content-Type', 'application/activity+json'); return response()->json($obj)->header('Content-Type', 'application/activity+json');
} }
public function userFollowers(Request $request, $username) public function userFollowers(Request $request, $username)
{ {
abort_if(!config_cache('federation.activitypub.enabled'), 404); abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
$id = AccountService::usernameToId($username); $id = AccountService::usernameToId($username);
abort_if(!$id, 404); abort_if(! $id, 404);
$account = AccountService::get($id); $account = AccountService::get($id);
abort_if(!$account || !isset($account['followers_count']), 404); abort_if(! $account || ! isset($account['followers_count']), 404);
$obj = [ $obj = [
'@context' => 'https://www.w3.org/ns/activitystreams', '@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $request->getUri(), 'id' => $request->getUri(),
'type' => 'OrderedCollection', 'type' => 'OrderedCollection',
'totalItems' => $account['followers_count'] ?? 0, 'totalItems' => $account['followers_count'] ?? 0,
]; ];
return response()->json($obj)->header('Content-Type', 'application/activity+json'); return response()->json($obj)->header('Content-Type', 'application/activity+json');
} }
} }

View file

@ -17,7 +17,7 @@ trait Instagram
{ {
public function instagram() public function instagram()
{ {
if(config_cache('pixelfed.import.instagram.enabled') != true) { if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled'); abort(404, 'Feature not enabled');
} }
return view('settings.import.instagram.home'); return view('settings.import.instagram.home');
@ -25,6 +25,9 @@ trait Instagram
public function instagramStart(Request $request) public function instagramStart(Request $request)
{ {
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
$completed = ImportJob::whereProfileId(Auth::user()->profile->id) $completed = ImportJob::whereProfileId(Auth::user()->profile->id)
->whereService('instagram') ->whereService('instagram')
->whereNotNull('completed_at') ->whereNotNull('completed_at')
@ -38,6 +41,9 @@ trait Instagram
protected function instagramRedirectOrNew() protected function instagramRedirectOrNew()
{ {
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
$profile = Auth::user()->profile; $profile = Auth::user()->profile;
$exists = ImportJob::whereProfileId($profile->id) $exists = ImportJob::whereProfileId($profile->id)
->whereService('instagram') ->whereService('instagram')
@ -61,6 +67,9 @@ trait Instagram
public function instagramStepOne(Request $request, $uuid) public function instagramStepOne(Request $request, $uuid)
{ {
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
$profile = Auth::user()->profile; $profile = Auth::user()->profile;
$job = ImportJob::whereProfileId($profile->id) $job = ImportJob::whereProfileId($profile->id)
->whereNull('completed_at') ->whereNull('completed_at')
@ -72,6 +81,9 @@ trait Instagram
public function instagramStepOneStore(Request $request, $uuid) public function instagramStepOneStore(Request $request, $uuid)
{ {
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
$max = 'max:' . config('pixelfed.import.instagram.limits.size'); $max = 'max:' . config('pixelfed.import.instagram.limits.size');
$this->validate($request, [ $this->validate($request, [
'media.*' => 'required|mimes:bin,jpeg,png,gif|'.$max, 'media.*' => 'required|mimes:bin,jpeg,png,gif|'.$max,
@ -114,6 +126,9 @@ trait Instagram
public function instagramStepTwo(Request $request, $uuid) public function instagramStepTwo(Request $request, $uuid)
{ {
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
$profile = Auth::user()->profile; $profile = Auth::user()->profile;
$job = ImportJob::whereProfileId($profile->id) $job = ImportJob::whereProfileId($profile->id)
->whereNull('completed_at') ->whereNull('completed_at')
@ -125,6 +140,9 @@ trait Instagram
public function instagramStepTwoStore(Request $request, $uuid) public function instagramStepTwoStore(Request $request, $uuid)
{ {
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
$this->validate($request, [ $this->validate($request, [
'media' => 'required|file|max:1000' 'media' => 'required|file|max:1000'
]); ]);
@ -150,6 +168,9 @@ trait Instagram
public function instagramStepThree(Request $request, $uuid) public function instagramStepThree(Request $request, $uuid)
{ {
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
$profile = Auth::user()->profile; $profile = Auth::user()->profile;
$job = ImportJob::whereProfileId($profile->id) $job = ImportJob::whereProfileId($profile->id)
->whereService('instagram') ->whereService('instagram')
@ -162,6 +183,9 @@ trait Instagram
public function instagramStepThreeStore(Request $request, $uuid) public function instagramStepThreeStore(Request $request, $uuid)
{ {
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
abort(404, 'Feature not enabled');
}
$profile = Auth::user()->profile; $profile = Auth::user()->profile;
try { try {

View file

@ -179,7 +179,7 @@ class ImportPostController extends Controller
'required', 'required',
'file', 'file',
$mimes, $mimes,
'max:' . config('pixelfed.max_photo_size') 'max:' . config_cache('pixelfed.max_photo_size')
] ]
]); ]);

View file

@ -2,44 +2,43 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Profile;
use App\Services\AccountService;
use App\Http\Resources\DirectoryProfile; use App\Http\Resources\DirectoryProfile;
use App\Profile;
use Illuminate\Http\Request;
class LandingController extends Controller class LandingController extends Controller
{ {
public function directoryRedirect(Request $request) public function directoryRedirect(Request $request)
{ {
if($request->user()) { if ($request->user()) {
return redirect('/'); return redirect('/');
} }
abort_if(config_cache('instance.landing.show_directory') == false, 404); abort_if((bool) config_cache('instance.landing.show_directory') == false, 404);
return view('site.index'); return view('site.index');
} }
public function exploreRedirect(Request $request) public function exploreRedirect(Request $request)
{ {
if($request->user()) { if ($request->user()) {
return redirect('/'); return redirect('/');
} }
abort_if(config_cache('instance.landing.show_explore') == false, 404); abort_if((bool) config_cache('instance.landing.show_explore') == false, 404);
return view('site.index'); return view('site.index');
} }
public function getDirectoryApi(Request $request) public function getDirectoryApi(Request $request)
{ {
abort_if(config_cache('instance.landing.show_directory') == false, 404); abort_if((bool) config_cache('instance.landing.show_directory') == false, 404);
return DirectoryProfile::collection( return DirectoryProfile::collection(
Profile::whereNull('domain') Profile::whereNull('domain')
->whereIsSuggestable(true) ->whereIsSuggestable(true)
->orderByDesc('updated_at') ->orderByDesc('updated_at')
->cursorPaginate(20) ->cursorPaginate(20)
); );
} }
} }

View file

@ -2,30 +2,31 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Media; use App\Media;
use Illuminate\Http\Request;
class MediaController extends Controller class MediaController extends Controller
{ {
public function index(Request $request) public function index(Request $request)
{ {
//return view('settings.drive.index'); //return view('settings.drive.index');
} abort(404);
}
public function composeUpdate(Request $request, $id) public function composeUpdate(Request $request, $id)
{ {
abort(400, 'Endpoint deprecated'); abort(400, 'Endpoint deprecated');
} }
public function fallbackRedirect(Request $request, $pid, $mhash, $uhash, $f) public function fallbackRedirect(Request $request, $pid, $mhash, $uhash, $f)
{ {
abort_if(!config_cache('pixelfed.cloud_storage'), 404); abort_if(! (bool) config_cache('pixelfed.cloud_storage'), 404);
$path = 'public/m/_v2/' . $pid . '/' . $mhash . '/' . $uhash . '/' . $f; $path = 'public/m/_v2/'.$pid.'/'.$mhash.'/'.$uhash.'/'.$f;
$media = Media::whereProfileId($pid) $media = Media::whereProfileId($pid)
->whereMediaPath($path) ->whereMediaPath($path)
->whereNotNull('cdn_url') ->whereNotNull('cdn_url')
->firstOrFail(); ->firstOrFail();
return redirect()->away($media->cdn_url); return redirect()->away($media->cdn_url);
} }
} }

View file

@ -2,37 +2,41 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\ConfigCache; use App\Models\ConfigCache;
use Storage;
use App\Services\AccountService; use App\Services\AccountService;
use App\Services\StatusService; use App\Services\StatusService;
use Illuminate\Http\Request;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Cache;
use Storage;
use App\Status;
use App\User;
class PixelfedDirectoryController extends Controller class PixelfedDirectoryController extends Controller
{ {
public function get(Request $request) public function get(Request $request)
{ {
if(!$request->filled('sk')) { if (! $request->filled('sk')) {
abort(404); abort(404);
} }
if(!config_cache('pixelfed.directory.submission-key')) { if (! config_cache('pixelfed.directory.submission-key')) {
abort(404); abort(404);
} }
if(!hash_equals(config_cache('pixelfed.directory.submission-key'), $request->input('sk'))) { if (! hash_equals(config_cache('pixelfed.directory.submission-key'), $request->input('sk'))) {
abort(403); abort(403);
} }
$res = $this->buildListing(); $res = $this->buildListing();
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
} }
public function buildListing() public function buildListing()
{ {
$res = config_cache('pixelfed.directory'); $res = config_cache('pixelfed.directory');
if($res) { if ($res) {
$res = is_string($res) ? json_decode($res, true) : $res; $res = is_string($res) ? json_decode($res, true) : $res;
} }
@ -41,40 +45,40 @@ class PixelfedDirectoryController extends Controller
$res['_ts'] = config_cache('pixelfed.directory.submission-ts'); $res['_ts'] = config_cache('pixelfed.directory.submission-ts');
$res['version'] = config_cache('pixelfed.version'); $res['version'] = config_cache('pixelfed.version');
if(empty($res['summary'])) { if (empty($res['summary'])) {
$summary = ConfigCache::whereK('app.short_description')->pluck('v'); $summary = ConfigCache::whereK('app.short_description')->pluck('v');
$res['summary'] = $summary ? $summary[0] : null; $res['summary'] = $summary ? $summary[0] : null;
} }
if(isset($res['admin'])) { if (isset($res['admin'])) {
$res['admin'] = AccountService::get($res['admin'], true); $res['admin'] = AccountService::get($res['admin'], true);
} }
if(isset($res['banner_image']) && !empty($res['banner_image'])) { if (isset($res['banner_image']) && ! empty($res['banner_image'])) {
$res['banner_image'] = url(Storage::url($res['banner_image'])); $res['banner_image'] = url(Storage::url($res['banner_image']));
} }
if(isset($res['favourite_posts'])) { if (isset($res['favourite_posts'])) {
$res['favourite_posts'] = collect($res['favourite_posts'])->map(function($id) { $res['favourite_posts'] = collect($res['favourite_posts'])->map(function ($id) {
return StatusService::get($id); return StatusService::get($id);
}) })
->filter(function($post) { ->filter(function ($post) {
return $post && isset($post['account']); return $post && isset($post['account']);
}) })
->map(function($post) { ->map(function ($post) {
return [ return [
'avatar' => $post['account']['avatar'], 'avatar' => $post['account']['avatar'],
'display_name' => $post['account']['display_name'], 'display_name' => $post['account']['display_name'],
'username' => $post['account']['username'], 'username' => $post['account']['username'],
'media' => $post['media_attachments'][0]['url'], 'media' => $post['media_attachments'][0]['url'],
'url' => $post['url'] 'url' => $post['url'],
]; ];
}) })
->values(); ->values();
} }
$guidelines = ConfigCache::whereK('app.rules')->first(); $guidelines = ConfigCache::whereK('app.rules')->first();
if($guidelines) { if ($guidelines) {
$res['community_guidelines'] = json_decode($guidelines->v, true); $res['community_guidelines'] = json_decode($guidelines->v, true);
} }
@ -85,27 +89,27 @@ class PixelfedDirectoryController extends Controller
$res['curated_onboarding'] = $curatedOnboarding; $res['curated_onboarding'] = $curatedOnboarding;
$oauthEnabled = ConfigCache::whereK('pixelfed.oauth_enabled')->first(); $oauthEnabled = ConfigCache::whereK('pixelfed.oauth_enabled')->first();
if($oauthEnabled) { if ($oauthEnabled) {
$keys = file_exists(storage_path('oauth-public.key')) && file_exists(storage_path('oauth-private.key')); $keys = file_exists(storage_path('oauth-public.key')) && file_exists(storage_path('oauth-private.key'));
$res['oauth_enabled'] = (bool) $oauthEnabled && $keys; $res['oauth_enabled'] = (bool) $oauthEnabled && $keys;
} }
$activityPubEnabled = ConfigCache::whereK('federation.activitypub.enabled')->first(); $activityPubEnabled = ConfigCache::whereK('federation.activitypub.enabled')->first();
if($activityPubEnabled) { if ($activityPubEnabled) {
$res['activitypub_enabled'] = (bool) $activityPubEnabled; $res['activitypub_enabled'] = (bool) $activityPubEnabled;
} }
$res['feature_config'] = [ $res['feature_config'] = [
'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','), 'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','),
'image_quality' => config_cache('pixelfed.image_quality'), 'image_quality' => config_cache('pixelfed.image_quality'),
'optimize_image' => config_cache('pixelfed.optimize_image'), 'optimize_image' => (bool) config_cache('pixelfed.optimize_image'),
'max_photo_size' => config_cache('pixelfed.max_photo_size'), 'max_photo_size' => config_cache('pixelfed.max_photo_size'),
'max_caption_length' => config_cache('pixelfed.max_caption_length'), 'max_caption_length' => config_cache('pixelfed.max_caption_length'),
'max_altext_length' => config_cache('pixelfed.max_altext_length'), 'max_altext_length' => config_cache('pixelfed.max_altext_length'),
'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit'), 'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit'),
'max_account_size' => config_cache('pixelfed.max_account_size'), 'max_account_size' => config_cache('pixelfed.max_account_size'),
'max_album_length' => config_cache('pixelfed.max_album_length'), 'max_album_length' => config_cache('pixelfed.max_album_length'),
'account_deletion' => config_cache('pixelfed.account_deletion'), 'account_deletion' => (bool) config_cache('pixelfed.account_deletion'),
]; ];
$res['is_eligible'] = $this->validVal($res, 'admin') && $res['is_eligible'] = $this->validVal($res, 'admin') &&
@ -115,29 +119,36 @@ class PixelfedDirectoryController extends Controller
$this->validVal($res, 'privacy_pledge') && $this->validVal($res, 'privacy_pledge') &&
$this->validVal($res, 'location'); $this->validVal($res, 'location');
if(config_cache('pixelfed.directory.testimonials')) { if (config_cache('pixelfed.directory.testimonials')) {
$res['testimonials'] = collect(json_decode(config_cache('pixelfed.directory.testimonials'), true)) $res['testimonials'] = collect(json_decode(config_cache('pixelfed.directory.testimonials'), true))
->map(function($testimonial) { ->map(function ($testimonial) {
$profile = AccountService::get($testimonial['profile_id']); $profile = AccountService::get($testimonial['profile_id']);
return [ return [
'profile' => [ 'profile' => [
'username' => $profile['username'], 'username' => $profile['username'],
'display_name' => $profile['display_name'], 'display_name' => $profile['display_name'],
'avatar' => $profile['avatar'], 'avatar' => $profile['avatar'],
'created_at' => $profile['created_at'] 'created_at' => $profile['created_at'],
], ],
'body' => $testimonial['body'] 'body' => $testimonial['body'],
]; ];
}); });
} }
$res['features_enabled'] = [ $res['features_enabled'] = [
'stories' => (bool) config_cache('instance.stories.enabled') 'stories' => (bool) config_cache('instance.stories.enabled'),
]; ];
$statusesCount = Cache::remember('api:nodeinfo:statuses', 21600, function() {
return Status::whereLocal(true)->count();
});
$usersCount = Cache::remember('api:nodeinfo:users', 43200, function() {
return User::count();
});
$res['stats'] = [ $res['stats'] = [
'user_count' => \App\User::count(), 'user_count' => (int) $usersCount,
'post_count' => \App\Status::whereNull('uri')->count(), 'post_count' => (int) $statusesCount,
]; ];
$res['primary_locale'] = config('app.locale'); $res['primary_locale'] = config('app.locale');
@ -150,19 +161,18 @@ class PixelfedDirectoryController extends Controller
protected function validVal($res, $val, $count = false, $minLen = false) protected function validVal($res, $val, $count = false, $minLen = false)
{ {
if(!isset($res[$val])) { if (! isset($res[$val])) {
return false; return false;
} }
if($count) { if ($count) {
return count($res[$val]) >= $count; return count($res[$val]) >= $count;
} }
if($minLen) { if ($minLen) {
return strlen($res[$val]) >= $minLen; return strlen($res[$val]) >= $minLen;
} }
return $res[$val]; return $res[$val];
} }
} }

View file

@ -172,6 +172,8 @@ class ProfileController extends Controller
$user = $this->getCachedUser($username); $user = $this->getCachedUser($username);
abort_if(!$user, 404);
return redirect($user->url()); return redirect($user->url());
} }
@ -371,7 +373,7 @@ class ProfileController extends Controller
public function stories(Request $request, $username) public function stories(Request $request, $username)
{ {
abort_if(! config_cache('instance.stories.enabled') || ! $request->user(), 404); abort_if(!(bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail(); $profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
$pid = $profile->id; $pid = $profile->id;
$authed = Auth::user()->profile_id; $authed = Auth::user()->profile_id;

View file

@ -2,22 +2,20 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use App\Services\Account\RemoteAuthService;
use App\Models\RemoteAuth; use App\Models\RemoteAuth;
use App\Profile; use App\Services\Account\RemoteAuthService;
use App\Instance;
use App\User;
use Purify;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Auth\Events\Registered;
use App\Util\Lexer\RestrictedNames;
use App\Services\EmailService; use App\Services\EmailService;
use App\Services\MediaStorageService; use App\Services\MediaStorageService;
use App\User;
use App\Util\ActivityPub\Helpers; use App\Util\ActivityPub\Helpers;
use App\Util\Lexer\RestrictedNames;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use InvalidArgumentException; use InvalidArgumentException;
use Purify;
class RemoteAuthController extends Controller class RemoteAuthController extends Controller
{ {
@ -30,9 +28,10 @@ class RemoteAuthController extends Controller
config('remote-auth.mastodon.ignore_closed_state') && config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled') config('remote-auth.mastodon.enabled')
), 404); ), 404);
if($request->user()) { if ($request->user()) {
return redirect('/'); return redirect('/');
} }
return view('auth.remote.start'); return view('auth.remote.start');
} }
@ -51,25 +50,27 @@ class RemoteAuthController extends Controller
config('remote-auth.mastodon.enabled') config('remote-auth.mastodon.enabled')
), 404); ), 404);
if(config('remote-auth.mastodon.domains.only_custom')) { if (config('remote-auth.mastodon.domains.only_custom')) {
$res = config('remote-auth.mastodon.domains.custom'); $res = config('remote-auth.mastodon.domains.custom');
if(!$res || !strlen($res)) { if (! $res || ! strlen($res)) {
return []; return [];
} }
$res = explode(',', $res); $res = explode(',', $res);
return response()->json($res); return response()->json($res);
} }
if( config('remote-auth.mastodon.domains.custom') && if (config('remote-auth.mastodon.domains.custom') &&
!config('remote-auth.mastodon.domains.only_default') && ! config('remote-auth.mastodon.domains.only_default') &&
strlen(config('remote-auth.mastodon.domains.custom')) > 3 && strlen(config('remote-auth.mastodon.domains.custom')) > 3 &&
strpos(config('remote-auth.mastodon.domains.custom'), '.') > -1 strpos(config('remote-auth.mastodon.domains.custom'), '.') > -1
) { ) {
$res = config('remote-auth.mastodon.domains.custom'); $res = config('remote-auth.mastodon.domains.custom');
if(!$res || !strlen($res)) { if (! $res || ! strlen($res)) {
return []; return [];
} }
$res = explode(',', $res); $res = explode(',', $res);
return response()->json($res); return response()->json($res);
} }
@ -93,57 +94,62 @@ class RemoteAuthController extends Controller
$domain = $request->input('domain'); $domain = $request->input('domain');
if(str_starts_with(strtolower($domain), 'http')) { if (str_starts_with(strtolower($domain), 'http')) {
$res = [ $res = [
'domain' => $domain, 'domain' => $domain,
'ready' => false, 'ready' => false,
'action' => 'incompatible_domain' 'action' => 'incompatible_domain',
]; ];
return response()->json($res); return response()->json($res);
} }
$validateInstance = Helpers::validateUrl('https://' . $domain . '/?block-check=' . time()); $validateInstance = Helpers::validateUrl('https://'.$domain.'/?block-check='.time());
if(!$validateInstance) { if (! $validateInstance) {
$res = [ $res = [
'domain' => $domain, 'domain' => $domain,
'ready' => false, 'ready' => false,
'action' => 'blocked_domain' 'action' => 'blocked_domain',
]; ];
return response()->json($res); return response()->json($res);
} }
$compatible = RemoteAuthService::isDomainCompatible($domain); $compatible = RemoteAuthService::isDomainCompatible($domain);
if(!$compatible) { if (! $compatible) {
$res = [ $res = [
'domain' => $domain, 'domain' => $domain,
'ready' => false, 'ready' => false,
'action' => 'incompatible_domain' 'action' => 'incompatible_domain',
]; ];
return response()->json($res); return response()->json($res);
} }
if(config('remote-auth.mastodon.domains.only_default')) { if (config('remote-auth.mastodon.domains.only_default')) {
$defaultDomains = explode(',', config('remote-auth.mastodon.domains.default')); $defaultDomains = explode(',', config('remote-auth.mastodon.domains.default'));
if(!in_array($domain, $defaultDomains)) { if (! in_array($domain, $defaultDomains)) {
$res = [ $res = [
'domain' => $domain, 'domain' => $domain,
'ready' => false, 'ready' => false,
'action' => 'incompatible_domain' 'action' => 'incompatible_domain',
]; ];
return response()->json($res); return response()->json($res);
} }
} }
if(config('remote-auth.mastodon.domains.only_custom') && config('remote-auth.mastodon.domains.custom')) { if (config('remote-auth.mastodon.domains.only_custom') && config('remote-auth.mastodon.domains.custom')) {
$customDomains = explode(',', config('remote-auth.mastodon.domains.custom')); $customDomains = explode(',', config('remote-auth.mastodon.domains.custom'));
if(!in_array($domain, $customDomains)) { if (! in_array($domain, $customDomains)) {
$res = [ $res = [
'domain' => $domain, 'domain' => $domain,
'ready' => false, 'ready' => false,
'action' => 'incompatible_domain' 'action' => 'incompatible_domain',
]; ];
return response()->json($res); return response()->json($res);
} }
} }
@ -163,13 +169,13 @@ class RemoteAuthController extends Controller
'state' => $state, 'state' => $state,
]); ]);
$request->session()->put('oauth_redirect_to', 'https://' . $domain . '/oauth/authorize?' . $query); $request->session()->put('oauth_redirect_to', 'https://'.$domain.'/oauth/authorize?'.$query);
$dsh = Str::random(17); $dsh = Str::random(17);
$res = [ $res = [
'domain' => $domain, 'domain' => $domain,
'ready' => true, 'ready' => true,
'dsh' => $dsh 'dsh' => $dsh,
]; ];
return response()->json($res); return response()->json($res);
@ -185,7 +191,7 @@ class RemoteAuthController extends Controller
config('remote-auth.mastodon.enabled') config('remote-auth.mastodon.enabled')
), 404); ), 404);
if(!$request->filled('d') || !$request->filled('dsh') || !$request->session()->exists('oauth_redirect_to')) { if (! $request->filled('d') || ! $request->filled('dsh') || ! $request->session()->exists('oauth_redirect_to')) {
return redirect('/login'); return redirect('/login');
} }
@ -204,7 +210,7 @@ class RemoteAuthController extends Controller
$domain = $request->session()->get('oauth_domain'); $domain = $request->session()->get('oauth_domain');
if($request->filled('code')) { if ($request->filled('code')) {
$code = $request->input('code'); $code = $request->input('code');
$state = $request->session()->pull('state'); $state = $request->session()->pull('state');
@ -216,12 +222,14 @@ class RemoteAuthController extends Controller
$res = RemoteAuthService::getToken($domain, $code); $res = RemoteAuthService::getToken($domain, $code);
if(!$res || !isset($res['access_token'])) { if (! $res || ! isset($res['access_token'])) {
$request->session()->regenerate(); $request->session()->regenerate();
return redirect('/login'); return redirect('/login');
} }
$request->session()->put('oauth_remote_session_token', $res['access_token']); $request->session()->put('oauth_remote_session_token', $res['access_token']);
return redirect('/auth/mastodon/getting-started'); return redirect('/auth/mastodon/getting-started');
} }
@ -237,9 +245,10 @@ class RemoteAuthController extends Controller
config('remote-auth.mastodon.ignore_closed_state') && config('remote-auth.mastodon.ignore_closed_state') &&
config('remote-auth.mastodon.enabled') config('remote-auth.mastodon.enabled')
), 404); ), 404);
if($request->user()) { if ($request->user()) {
return redirect('/'); return redirect('/');
} }
return view('auth.remote.onboarding'); return view('auth.remote.onboarding');
} }
@ -261,36 +270,36 @@ class RemoteAuthController extends Controller
$res = RemoteAuthService::getVerifyCredentials($domain, $token); $res = RemoteAuthService::getVerifyCredentials($domain, $token);
abort_if(!$res || !isset($res['acct']), 403, 'Invalid credentials'); abort_if(! $res || ! isset($res['acct']), 403, 'Invalid credentials');
$webfinger = strtolower('@' . $res['acct'] . '@' . $domain); $webfinger = strtolower('@'.$res['acct'].'@'.$domain);
$request->session()->put('oauth_masto_webfinger', $webfinger); $request->session()->put('oauth_masto_webfinger', $webfinger);
if(config('remote-auth.mastodon.max_uses.enabled')) { if (config('remote-auth.mastodon.max_uses.enabled')) {
$limit = config('remote-auth.mastodon.max_uses.limit'); $limit = config('remote-auth.mastodon.max_uses.limit');
$uses = RemoteAuthService::lookupWebfingerUses($webfinger); $uses = RemoteAuthService::lookupWebfingerUses($webfinger);
if($uses >= $limit) { if ($uses >= $limit) {
return response()->json([ return response()->json([
'code' => 200, 'code' => 200,
'msg' => 'Success!', 'msg' => 'Success!',
'action' => 'max_uses_reached' 'action' => 'max_uses_reached',
]); ]);
} }
} }
$exists = RemoteAuth::whereDomain($domain)->where('webfinger', $webfinger)->whereNotNull('user_id')->first(); $exists = RemoteAuth::whereDomain($domain)->where('webfinger', $webfinger)->whereNotNull('user_id')->first();
if($exists && $exists->user_id) { if ($exists && $exists->user_id) {
return response()->json([ return response()->json([
'code' => 200, 'code' => 200,
'msg' => 'Success!', 'msg' => 'Success!',
'action' => 'redirect_existing_user' 'action' => 'redirect_existing_user',
]); ]);
} }
return response()->json([ return response()->json([
'code' => 200, 'code' => 200,
'msg' => 'Success!', 'msg' => 'Success!',
'action' => 'onboard' 'action' => 'onboard',
]); ]);
} }
@ -311,7 +320,7 @@ class RemoteAuthController extends Controller
$token = $request->session()->get('oauth_remote_session_token'); $token = $request->session()->get('oauth_remote_session_token');
$res = RemoteAuthService::getVerifyCredentials($domain, $token); $res = RemoteAuthService::getVerifyCredentials($domain, $token);
$res['_webfinger'] = strtolower('@' . $res['acct'] . '@' . $domain); $res['_webfinger'] = strtolower('@'.$res['acct'].'@'.$domain);
$res['_domain'] = strtolower($domain); $res['_domain'] = strtolower($domain);
$request->session()->put('oauth_remasto_id', $res['id']); $request->session()->put('oauth_remasto_id', $res['id']);
@ -324,7 +333,7 @@ class RemoteAuthController extends Controller
'bearer_token' => $token, 'bearer_token' => $token,
'verify_credentials' => $res, 'verify_credentials' => $res,
'last_verify_credentials_at' => now(), 'last_verify_credentials_at' => now(),
'last_successful_login_at' => now() 'last_successful_login_at' => now(),
]); ]);
$request->session()->put('oauth_masto_raid', $ra->id); $request->session()->put('oauth_masto_raid', $ra->id);
@ -355,24 +364,24 @@ class RemoteAuthController extends Controller
$underscore = substr_count($value, '_'); $underscore = substr_count($value, '_');
$period = substr_count($value, '.'); $period = substr_count($value, '.');
if(ends_with($value, ['.php', '.js', '.css'])) { if (ends_with($value, ['.php', '.js', '.css'])) {
return $fail('Username is invalid.'); return $fail('Username is invalid.');
} }
if(($dash + $underscore + $period) > 1) { if (($dash + $underscore + $period) > 1) {
return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).'); return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
} }
if (!ctype_alnum($value[0])) { if (! ctype_alnum($value[0])) {
return $fail('Username is invalid. Must start with a letter or number.'); return $fail('Username is invalid. Must start with a letter or number.');
} }
if (!ctype_alnum($value[strlen($value) - 1])) { if (! ctype_alnum($value[strlen($value) - 1])) {
return $fail('Username is invalid. Must end with a letter or number.'); return $fail('Username is invalid. Must end with a letter or number.');
} }
$val = str_replace(['_', '.', '-'], '', $value); $val = str_replace(['_', '.', '-'], '', $value);
if(!ctype_alnum($val)) { if (! ctype_alnum($val)) {
return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).'); return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
} }
@ -380,8 +389,8 @@ class RemoteAuthController extends Controller
if (in_array(strtolower($value), array_map('strtolower', $restricted))) { if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
return $fail('Username cannot be used.'); return $fail('Username cannot be used.');
} }
} },
] ],
]); ]);
$username = strtolower($request->input('username')); $username = strtolower($request->input('username'));
@ -390,7 +399,7 @@ class RemoteAuthController extends Controller
return response()->json([ return response()->json([
'code' => 200, 'code' => 200,
'username' => $username, 'username' => $username,
'exists' => $exists 'exists' => $exists,
]); ]);
} }
@ -411,7 +420,7 @@ class RemoteAuthController extends Controller
'email' => [ 'email' => [
'required', 'required',
'email:strict,filter_unicode,dns,spoof', 'email:strict,filter_unicode,dns,spoof',
] ],
]); ]);
$email = $request->input('email'); $email = $request->input('email');
@ -422,7 +431,7 @@ class RemoteAuthController extends Controller
'code' => 200, 'code' => 200,
'email' => $email, 'email' => $email,
'exists' => $exists, 'exists' => $exists,
'banned' => $banned 'banned' => $banned,
]); ]);
} }
@ -445,18 +454,18 @@ class RemoteAuthController extends Controller
$res = RemoteAuthService::getFollowing($domain, $token, $id); $res = RemoteAuthService::getFollowing($domain, $token, $id);
if(!$res) { if (! $res) {
return response()->json([ return response()->json([
'code' => 200, 'code' => 200,
'following' => [] 'following' => [],
]); ]);
} }
$res = collect($res)->filter(fn($acct) => Helpers::validateUrl($acct['url']))->values()->toArray(); $res = collect($res)->filter(fn ($acct) => Helpers::validateUrl($acct['url']))->values()->toArray();
return response()->json([ return response()->json([
'code' => 200, 'code' => 200,
'following' => $res 'following' => $res,
]); ]);
} }
@ -487,24 +496,24 @@ class RemoteAuthController extends Controller
$underscore = substr_count($value, '_'); $underscore = substr_count($value, '_');
$period = substr_count($value, '.'); $period = substr_count($value, '.');
if(ends_with($value, ['.php', '.js', '.css'])) { if (ends_with($value, ['.php', '.js', '.css'])) {
return $fail('Username is invalid.'); return $fail('Username is invalid.');
} }
if(($dash + $underscore + $period) > 1) { if (($dash + $underscore + $period) > 1) {
return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).'); return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
} }
if (!ctype_alnum($value[0])) { if (! ctype_alnum($value[0])) {
return $fail('Username is invalid. Must start with a letter or number.'); return $fail('Username is invalid. Must start with a letter or number.');
} }
if (!ctype_alnum($value[strlen($value) - 1])) { if (! ctype_alnum($value[strlen($value) - 1])) {
return $fail('Username is invalid. Must end with a letter or number.'); return $fail('Username is invalid. Must end with a letter or number.');
} }
$val = str_replace(['_', '.', '-'], '', $value); $val = str_replace(['_', '.', '-'], '', $value);
if(!ctype_alnum($val)) { if (! ctype_alnum($val)) {
return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).'); return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
} }
@ -512,10 +521,10 @@ class RemoteAuthController extends Controller
if (in_array(strtolower($value), array_map('strtolower', $restricted))) { if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
return $fail('Username cannot be used.'); return $fail('Username cannot be used.');
} }
} },
], ],
'password' => 'required|string|min:8|confirmed', 'password' => 'required|string|min:8|confirmed',
'name' => 'nullable|max:30' 'name' => 'nullable|max:30',
]); ]);
$email = $request->input('email'); $email = $request->input('email');
@ -527,7 +536,7 @@ class RemoteAuthController extends Controller
'name' => $name, 'name' => $name,
'username' => $username, 'username' => $username,
'password' => $password, 'password' => $password,
'email' => $email 'email' => $email,
]); ]);
$raid = $request->session()->pull('oauth_masto_raid'); $raid = $request->session()->pull('oauth_masto_raid');
@ -541,7 +550,7 @@ class RemoteAuthController extends Controller
return [ return [
'code' => 200, 'code' => 200,
'msg' => 'Success', 'msg' => 'Success',
'token' => $token 'token' => $token,
]; ];
} }
@ -585,7 +594,7 @@ class RemoteAuthController extends Controller
abort_unless($request->session()->exists('oauth_remasto_id'), 403); abort_unless($request->session()->exists('oauth_remasto_id'), 403);
$this->validate($request, [ $this->validate($request, [
'account' => 'required|url' 'account' => 'required|url',
]); ]);
$account = $request->input('account'); $account = $request->input('account');
@ -594,10 +603,10 @@ class RemoteAuthController extends Controller
$host = strtolower(config('pixelfed.domain.app')); $host = strtolower(config('pixelfed.domain.app'));
$domain = strtolower(parse_url($account, PHP_URL_HOST)); $domain = strtolower(parse_url($account, PHP_URL_HOST));
if($domain == $host) { if ($domain == $host) {
$username = Str::of($account)->explode('/')->last(); $username = Str::of($account)->explode('/')->last();
$user = User::where('username', $username)->first(); $user = User::where('username', $username)->first();
if($user) { if ($user) {
return ['id' => (string) $user->profile_id]; return ['id' => (string) $user->profile_id];
} else { } else {
return []; return [];
@ -605,7 +614,7 @@ class RemoteAuthController extends Controller
} else { } else {
try { try {
$profile = Helpers::profileFetch($account); $profile = Helpers::profileFetch($account);
if($profile) { if ($profile) {
return ['id' => (string) $profile->id]; return ['id' => (string) $profile->id];
} else { } else {
return []; return [];
@ -635,13 +644,13 @@ class RemoteAuthController extends Controller
$user = $request->user(); $user = $request->user();
$profile = $user->profile; $profile = $user->profile;
abort_if(!$profile->avatar, 404, 'Missing avatar'); abort_if(! $profile->avatar, 404, 'Missing avatar');
$avatar = $profile->avatar; $avatar = $profile->avatar;
$avatar->remote_url = $request->input('avatar_url'); $avatar->remote_url = $request->input('avatar_url');
$avatar->save(); $avatar->save();
MediaStorageService::avatar($avatar, config_cache('pixelfed.cloud_storage') == false); MediaStorageService::avatar($avatar, (bool) config_cache('pixelfed.cloud_storage') == false);
return [200]; return [200];
} }
@ -657,7 +666,7 @@ class RemoteAuthController extends Controller
), 404); ), 404);
abort_unless($request->user(), 404); abort_unless($request->user(), 404);
$currentWebfinger = '@' . $request->user()->username . '@' . config('pixelfed.domain.app'); $currentWebfinger = '@'.$request->user()->username.'@'.config('pixelfed.domain.app');
$ra = RemoteAuth::where('user_id', $request->user()->id)->firstOrFail(); $ra = RemoteAuth::where('user_id', $request->user()->id)->firstOrFail();
RemoteAuthService::submitToBeagle( RemoteAuthService::submitToBeagle(
$ra->webfinger, $ra->webfinger,
@ -691,19 +700,20 @@ class RemoteAuthController extends Controller
$user = User::findOrFail($ra->user_id); $user = User::findOrFail($ra->user_id);
abort_if($user->is_admin || $user->status != null, 422, 'Invalid auth action'); abort_if($user->is_admin || $user->status != null, 422, 'Invalid auth action');
Auth::loginUsingId($ra->user_id); Auth::loginUsingId($ra->user_id);
return [200]; return [200];
} }
protected function createUser($data) protected function createUser($data)
{ {
event(new Registered($user = User::create([ event(new Registered($user = User::create([
'name' => Purify::clean($data['name']), 'name' => Purify::clean($data['name']),
'username' => $data['username'], 'username' => $data['username'],
'email' => $data['email'], 'email' => $data['email'],
'password' => Hash::make($data['password']), 'password' => Hash::make($data['password']),
'email_verified_at' => config('remote-auth.mastodon.contraints.skip_email_verification') ? now() : null, 'email_verified_at' => config('remote-auth.mastodon.contraints.skip_email_verification') ? now() : null,
'app_register_ip' => request()->ip(), 'app_register_ip' => request()->ip(),
'register_source' => 'mastodon' 'register_source' => 'mastodon',
]))); ])));
$this->guarder()->login($user); $this->guarder()->login($user);

View file

@ -2,368 +2,367 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Auth;
use App\Hashtag; use App\Hashtag;
use App\Place; use App\Place;
use App\Profile; use App\Profile;
use App\Services\WebfingerService;
use App\Status; use App\Status;
use Illuminate\Http\Request;
use App\Util\ActivityPub\Helpers; use App\Util\ActivityPub\Helpers;
use Auth;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use App\Transformer\Api\{
AccountTransformer,
HashtagTransformer,
StatusTransformer,
};
use App\Services\WebfingerService;
class SearchController extends Controller class SearchController extends Controller
{ {
public $tokens = []; public $tokens = [];
public $term = '';
public $hash = '';
public $cacheKey = 'api:search:tag:';
public function __construct() public $term = '';
{
$this->middleware('auth');
}
public function searchAPI(Request $request) public $hash = '';
{
$this->validate($request, [
'q' => 'required|string|min:3|max:120',
'src' => 'required|string|in:metro',
'v' => 'required|integer|in:2',
'scope' => 'required|in:all,hashtag,profile,remote,webfinger'
]);
$scope = $request->input('scope') ?? 'all'; public $cacheKey = 'api:search:tag:';
$this->term = e(urldecode($request->input('q')));
$this->hash = hash('sha256', $this->term);
switch ($scope) { public function __construct()
case 'all': {
$this->getHashtags(); $this->middleware('auth');
$this->getPosts(); }
$this->getProfiles();
// $this->getPlaces();
break;
case 'hashtag': public function searchAPI(Request $request)
$this->getHashtags(); {
break; $this->validate($request, [
'q' => 'required|string|min:3|max:120',
'src' => 'required|string|in:metro',
'v' => 'required|integer|in:2',
'scope' => 'required|in:all,hashtag,profile,remote,webfinger',
]);
case 'profile': $scope = $request->input('scope') ?? 'all';
$this->getProfiles(); $this->term = e(urldecode($request->input('q')));
break; $this->hash = hash('sha256', $this->term);
case 'webfinger': switch ($scope) {
$this->webfingerSearch(); case 'all':
break; $this->getHashtags();
$this->getPosts();
$this->getProfiles();
// $this->getPlaces();
break;
case 'remote': case 'hashtag':
$this->remoteLookupSearch(); $this->getHashtags();
break; break;
case 'place': case 'profile':
$this->getPlaces(); $this->getProfiles();
break; break;
default: case 'webfinger':
break; $this->webfingerSearch();
} break;
return response()->json($this->tokens, 200, [], JSON_PRETTY_PRINT); case 'remote':
} $this->remoteLookupSearch();
break;
protected function getPosts() case 'place':
{ $this->getPlaces();
$tag = $this->term; break;
$hash = hash('sha256', $tag);
if( Helpers::validateUrl($tag) != false &&
Helpers::validateLocalUrl($tag) != true &&
config_cache('federation.activitypub.enabled') == true &&
config('federation.activitypub.remoteFollow') == true
) {
$remote = Helpers::fetchFromUrl($tag);
if( isset($remote['type']) &&
$remote['type'] == 'Note') {
$item = Helpers::statusFetch($tag);
$this->tokens['posts'] = [[
'count' => 0,
'url' => $item->url(),
'type' => 'status',
'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
'tokens' => [$item->caption],
'name' => $item->caption,
'thumb' => $item->thumb(),
]];
}
} else {
$posts = Status::select('id', 'profile_id', 'caption', 'created_at')
->whereHas('media')
->whereNull('in_reply_to_id')
->whereNull('reblog_of_id')
->whereProfileId(Auth::user()->profile_id)
->where('caption', 'like', '%'.$tag.'%')
->latest()
->limit(10)
->get();
if($posts->count() > 0) { default:
$posts = $posts->map(function($item, $key) { break;
return [ }
'count' => 0,
'url' => $item->url(),
'type' => 'status',
'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
'tokens' => [$item->caption],
'name' => $item->caption,
'thumb' => $item->thumb(),
'filter' => $item->firstMedia()->filter_class
];
});
$this->tokens['posts'] = $posts;
}
}
}
protected function getHashtags() return response()->json($this->tokens, 200, [], JSON_PRETTY_PRINT);
{ }
$tag = $this->term;
$key = $this->cacheKey . 'hashtags:' . $this->hash;
$ttl = now()->addMinutes(1);
$tokens = Cache::remember($key, $ttl, function() use($tag) {
$htag = Str::startsWith($tag, '#') == true ? mb_substr($tag, 1) : $tag;
$hashtags = Hashtag::select('id', 'name', 'slug')
->where('slug', 'like', '%'.$htag.'%')
->whereHas('posts')
->limit(20)
->get();
if($hashtags->count() > 0) {
$tags = $hashtags->map(function ($item, $key) {
return [
'count' => $item->posts()->count(),
'url' => $item->url(),
'type' => 'hashtag',
'value' => $item->name,
'tokens' => '',
'name' => null,
];
});
return $tags;
}
});
$this->tokens['hashtags'] = $tokens;
}
protected function getPlaces() protected function getPosts()
{ {
$tag = $this->term; $tag = $this->term;
// $key = $this->cacheKey . 'places:' . $this->hash; $hash = hash('sha256', $tag);
// $ttl = now()->addHours(12); if (Helpers::validateUrl($tag) != false &&
// $tokens = Cache::remember($key, $ttl, function() use($tag) { Helpers::validateLocalUrl($tag) != true &&
$htag = Str::contains($tag, ',') == true ? explode(',', $tag) : [$tag]; (bool) config_cache('federation.activitypub.enabled') == true &&
$hashtags = Place::select('id', 'name', 'slug', 'country') config('federation.activitypub.remoteFollow') == true
->where('name', 'like', '%'.$htag[0].'%') ) {
->paginate(20); $remote = Helpers::fetchFromUrl($tag);
$tags = []; if (isset($remote['type']) &&
if($hashtags->count() > 0) { in_array($remote['type'], ['Note', 'Question'])
$tags = $hashtags->map(function ($item, $key) { ) {
return [ $item = Helpers::statusFetch($tag);
'count' => null, $this->tokens['posts'] = [[
'url' => $item->url(), 'count' => 0,
'type' => 'place', 'url' => $item->url(),
'value' => $item->name . ', ' . $item->country, 'type' => 'status',
'tokens' => '', 'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
'name' => null, 'tokens' => [$item->caption],
'city' => $item->name, 'name' => $item->caption,
'country' => $item->country 'thumb' => $item->thumb(),
]; ]];
}); }
// return $tags; } else {
} $posts = Status::select('id', 'profile_id', 'caption', 'created_at')
// }); ->whereHas('media')
$this->tokens['places'] = $tags; ->whereNull('in_reply_to_id')
$this->tokens['placesPagination'] = [ ->whereNull('reblog_of_id')
'total' => $hashtags->total(), ->whereProfileId(Auth::user()->profile_id)
'current_page' => $hashtags->currentPage(), ->where('caption', 'like', '%'.$tag.'%')
'last_page' => $hashtags->lastPage() ->latest()
]; ->limit(10)
} ->get();
protected function getProfiles() if ($posts->count() > 0) {
{ $posts = $posts->map(function ($item, $key) {
$tag = $this->term; return [
$remoteKey = $this->cacheKey . 'profiles:remote:' . $this->hash; 'count' => 0,
$key = $this->cacheKey . 'profiles:' . $this->hash; 'url' => $item->url(),
$remoteTtl = now()->addMinutes(15); 'type' => 'status',
$ttl = now()->addHours(2); 'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
if( Helpers::validateUrl($tag) != false && 'tokens' => [$item->caption],
Helpers::validateLocalUrl($tag) != true && 'name' => $item->caption,
config_cache('federation.activitypub.enabled') == true && 'thumb' => $item->thumb(),
config('federation.activitypub.remoteFollow') == true 'filter' => $item->firstMedia()->filter_class,
) { ];
$remote = Helpers::fetchFromUrl($tag); });
if( isset($remote['type']) && $this->tokens['posts'] = $posts;
$remote['type'] == 'Person' }
) { }
$this->tokens['profiles'] = Cache::remember($remoteKey, $remoteTtl, function() use($tag) { }
$item = Helpers::profileFirstOrNew($tag);
$tokens = [[
'count' => 1,
'url' => $item->url(),
'type' => 'profile',
'value' => $item->username,
'tokens' => [$item->username],
'name' => $item->name,
'entity' => [
'id' => (string) $item->id,
'following' => $item->followedBy(Auth::user()->profile),
'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
'thumb' => $item->avatarUrl(),
'local' => (bool) !$item->domain,
'post_count' => $item->statuses()->count()
]
]];
return $tokens;
});
}
}
else { protected function getHashtags()
$this->tokens['profiles'] = Cache::remember($key, $ttl, function() use($tag) { {
if(Str::startsWith($tag, '@')) { $tag = $this->term;
$tag = substr($tag, 1); $key = $this->cacheKey.'hashtags:'.$this->hash;
} $ttl = now()->addMinutes(1);
$users = Profile::select('status', 'domain', 'username', 'name', 'id') $tokens = Cache::remember($key, $ttl, function () use ($tag) {
->whereNull('status') $htag = Str::startsWith($tag, '#') == true ? mb_substr($tag, 1) : $tag;
->where('username', 'like', '%'.$tag.'%') $hashtags = Hashtag::select('id', 'name', 'slug')
->limit(20) ->where('slug', 'like', '%'.$htag.'%')
->orderBy('domain') ->whereHas('posts')
->get(); ->limit(20)
->get();
if ($hashtags->count() > 0) {
$tags = $hashtags->map(function ($item, $key) {
return [
'count' => $item->posts()->count(),
'url' => $item->url(),
'type' => 'hashtag',
'value' => $item->name,
'tokens' => '',
'name' => null,
];
});
if($users->count() > 0) { return $tags;
return $users->map(function ($item, $key) { }
return [ });
'count' => 0, $this->tokens['hashtags'] = $tokens;
'url' => $item->url(), }
'type' => 'profile',
'value' => $item->username,
'tokens' => [$item->username],
'name' => $item->name,
'avatar' => $item->avatarUrl(),
'id' => (string) $item->id,
'entity' => [
'id' => (string) $item->id,
'following' => $item->followedBy(Auth::user()->profile),
'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
'thumb' => $item->avatarUrl(),
'local' => (bool) !$item->domain,
'post_count' => $item->statuses()->count()
]
];
});
}
});
}
}
public function results(Request $request) protected function getPlaces()
{ {
$this->validate($request, [ $tag = $this->term;
'q' => 'required|string|min:1', // $key = $this->cacheKey . 'places:' . $this->hash;
]); // $ttl = now()->addHours(12);
// $tokens = Cache::remember($key, $ttl, function() use($tag) {
$htag = Str::contains($tag, ',') == true ? explode(',', $tag) : [$tag];
$hashtags = Place::select('id', 'name', 'slug', 'country')
->where('name', 'like', '%'.$htag[0].'%')
->paginate(20);
$tags = [];
if ($hashtags->count() > 0) {
$tags = $hashtags->map(function ($item, $key) {
return [
'count' => null,
'url' => $item->url(),
'type' => 'place',
'value' => $item->name.', '.$item->country,
'tokens' => '',
'name' => null,
'city' => $item->name,
'country' => $item->country,
];
});
// return $tags;
}
// });
$this->tokens['places'] = $tags;
$this->tokens['placesPagination'] = [
'total' => $hashtags->total(),
'current_page' => $hashtags->currentPage(),
'last_page' => $hashtags->lastPage(),
];
}
return view('search.results'); protected function getProfiles()
} {
$tag = $this->term;
$remoteKey = $this->cacheKey.'profiles:remote:'.$this->hash;
$key = $this->cacheKey.'profiles:'.$this->hash;
$remoteTtl = now()->addMinutes(15);
$ttl = now()->addHours(2);
if (Helpers::validateUrl($tag) != false &&
Helpers::validateLocalUrl($tag) != true &&
(bool) config_cache('federation.activitypub.enabled') == true &&
config('federation.activitypub.remoteFollow') == true
) {
$remote = Helpers::fetchFromUrl($tag);
if (isset($remote['type']) &&
$remote['type'] == 'Person'
) {
$this->tokens['profiles'] = Cache::remember($remoteKey, $remoteTtl, function () use ($tag) {
$item = Helpers::profileFirstOrNew($tag);
$tokens = [[
'count' => 1,
'url' => $item->url(),
'type' => 'profile',
'value' => $item->username,
'tokens' => [$item->username],
'name' => $item->name,
'entity' => [
'id' => (string) $item->id,
'following' => $item->followedBy(Auth::user()->profile),
'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
'thumb' => $item->avatarUrl(),
'local' => (bool) ! $item->domain,
'post_count' => $item->statuses()->count(),
],
]];
protected function webfingerSearch() return $tokens;
{ });
$wfs = WebfingerService::lookup($this->term); }
} else {
$this->tokens['profiles'] = Cache::remember($key, $ttl, function () use ($tag) {
if (Str::startsWith($tag, '@')) {
$tag = substr($tag, 1);
}
$users = Profile::select('status', 'domain', 'username', 'name', 'id')
->whereNull('status')
->where('username', 'like', '%'.$tag.'%')
->limit(20)
->orderBy('domain')
->get();
if(empty($wfs)) { if ($users->count() > 0) {
return; return $users->map(function ($item, $key) {
} return [
'count' => 0,
'url' => $item->url(),
'type' => 'profile',
'value' => $item->username,
'tokens' => [$item->username],
'name' => $item->name,
'avatar' => $item->avatarUrl(),
'id' => (string) $item->id,
'entity' => [
'id' => (string) $item->id,
'following' => $item->followedBy(Auth::user()->profile),
'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
'thumb' => $item->avatarUrl(),
'local' => (bool) ! $item->domain,
'post_count' => $item->statuses()->count(),
],
];
});
}
});
}
}
$this->tokens['profiles'] = [ public function results(Request $request)
[ {
'count' => 1, $this->validate($request, [
'url' => $wfs['url'], 'q' => 'required|string|min:1',
'type' => 'profile', ]);
'value' => $wfs['username'],
'tokens' => [$wfs['username']],
'name' => $wfs['display_name'],
'entity' => [
'id' => (string) $wfs['id'],
'following' => null,
'follow_request' => null,
'thumb' => $wfs['avatar'],
'local' => (bool) $wfs['local']
]
]
];
return;
}
protected function remotePostLookup() return view('search.results');
{ }
$tag = $this->term;
$hash = hash('sha256', $tag);
$local = Helpers::validateLocalUrl($tag);
$valid = Helpers::validateUrl($tag);
if($valid == false || $local == true) { protected function webfingerSearch()
return; {
} $wfs = WebfingerService::lookup($this->term);
if(Status::whereUri($tag)->whereLocal(false)->exists()) { if (empty($wfs)) {
$item = Status::whereUri($tag)->first(); return;
$media = $item->firstMedia(); }
$url = null;
if($media) {
$url = $media->remote_url;
}
$this->tokens['posts'] = [[
'count' => 0,
'url' => "/i/web/post/_/$item->profile_id/$item->id",
'type' => 'status',
'username' => $item->profile->username,
'caption' => $item->rendered ?? $item->caption,
'thumb' => $url,
'timestamp' => $item->created_at->diffForHumans()
]];
}
$remote = Helpers::fetchFromUrl($tag); $this->tokens['profiles'] = [
[
'count' => 1,
'url' => $wfs['url'],
'type' => 'profile',
'value' => $wfs['username'],
'tokens' => [$wfs['username']],
'name' => $wfs['display_name'],
'entity' => [
'id' => (string) $wfs['id'],
'following' => null,
'follow_request' => null,
'thumb' => $wfs['avatar'],
'local' => (bool) $wfs['local'],
],
],
];
if(isset($remote['type']) && $remote['type'] == 'Note') { }
$item = Helpers::statusFetch($tag);
$media = $item->firstMedia();
$url = null;
if($media) {
$url = $media->remote_url;
}
$this->tokens['posts'] = [[
'count' => 0,
'url' => "/i/web/post/_/$item->profile_id/$item->id",
'type' => 'status',
'username' => $item->profile->username,
'caption' => $item->rendered ?? $item->caption,
'thumb' => $url,
'timestamp' => $item->created_at->diffForHumans()
]];
}
}
protected function remoteLookupSearch() protected function remotePostLookup()
{ {
if(!Helpers::validateUrl($this->term)) { $tag = $this->term;
return; $hash = hash('sha256', $tag);
} $local = Helpers::validateLocalUrl($tag);
$this->getProfiles(); $valid = Helpers::validateUrl($tag);
$this->remotePostLookup();
} if ($valid == false || $local == true) {
return;
}
if (Status::whereUri($tag)->whereLocal(false)->exists()) {
$item = Status::whereUri($tag)->first();
$media = $item->firstMedia();
$url = null;
if ($media) {
$url = $media->remote_url;
}
$this->tokens['posts'] = [[
'count' => 0,
'url' => "/i/web/post/_/$item->profile_id/$item->id",
'type' => 'status',
'username' => $item->profile->username,
'caption' => $item->rendered ?? $item->caption,
'thumb' => $url,
'timestamp' => $item->created_at->diffForHumans(),
]];
}
$remote = Helpers::fetchFromUrl($tag);
if (isset($remote['type']) && $remote['type'] == 'Note') {
$item = Helpers::statusFetch($tag);
$media = $item->firstMedia();
$url = null;
if ($media) {
$url = $media->remote_url;
}
$this->tokens['posts'] = [[
'count' => 0,
'url' => "/i/web/post/_/$item->profile_id/$item->id",
'type' => 'status',
'username' => $item->profile->username,
'caption' => $item->rendered ?? $item->caption,
'thumb' => $url,
'timestamp' => $item->created_at->diffForHumans(),
]];
}
}
protected function remoteLookupSearch()
{
if (! Helpers::validateUrl($this->term)) {
return;
}
$this->getProfiles();
$this->remotePostLookup();
}
} }

View file

@ -78,7 +78,7 @@ class StatusController extends Controller
]); ]);
} }
if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) { if ($request->wantsJson() && (bool) config_cache('federation.activitypub.enabled')) {
return $this->showActivityPub($request, $status); return $this->showActivityPub($request, $status);
} }

View file

@ -2,54 +2,56 @@
namespace App\Http\Controllers\Stories; namespace App\Http\Controllers\Stories;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use App\Models\Conversation;
use App\DirectMessage; use App\DirectMessage;
use App\Notification; use App\Http\Controllers\Controller;
use App\Story; use App\Http\Resources\StoryView as StoryViewResource;
use App\Status;
use App\StoryView;
use App\Jobs\StoryPipeline\StoryDelete; use App\Jobs\StoryPipeline\StoryDelete;
use App\Jobs\StoryPipeline\StoryFanout; use App\Jobs\StoryPipeline\StoryFanout;
use App\Jobs\StoryPipeline\StoryReplyDeliver; use App\Jobs\StoryPipeline\StoryReplyDeliver;
use App\Jobs\StoryPipeline\StoryViewDeliver; use App\Jobs\StoryPipeline\StoryViewDeliver;
use App\Models\Conversation;
use App\Notification;
use App\Services\AccountService; use App\Services\AccountService;
use App\Services\MediaPathService; use App\Services\MediaPathService;
use App\Services\StoryService; use App\Services\StoryService;
use App\Http\Resources\StoryView as StoryViewResource; use App\Status;
use App\Story;
use App\StoryView;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class StoryApiV1Controller extends Controller class StoryApiV1Controller extends Controller
{ {
const RECENT_KEY = 'pf:stories:recent-by-id:'; const RECENT_KEY = 'pf:stories:recent-by-id:';
const RECENT_TTL = 300; const RECENT_TTL = 300;
public function carousel(Request $request) public function carousel(Request $request)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$pid = $request->user()->profile_id; $pid = $request->user()->profile_id;
if(config('database.default') == 'pgsql') { if (config('database.default') == 'pgsql') {
$s = Cache::remember(self::RECENT_KEY . $pid, self::RECENT_TTL, function() use($pid) { $s = Cache::remember(self::RECENT_KEY.$pid, self::RECENT_TTL, function () use ($pid) {
return Story::select('stories.*', 'followers.following_id') return Story::select('stories.*', 'followers.following_id')
->leftJoin('followers', 'followers.following_id', 'stories.profile_id') ->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
->where('followers.profile_id', $pid) ->where('followers.profile_id', $pid)
->where('stories.active', true) ->where('stories.active', true)
->map(function($s) { ->map(function ($s) {
$r = new \StdClass; $r = new \StdClass;
$r->id = $s->id; $r->id = $s->id;
$r->profile_id = $s->profile_id; $r->profile_id = $s->profile_id;
$r->type = $s->type; $r->type = $s->type;
$r->path = $s->path; $r->path = $s->path;
return $r; return $r;
}) })
->unique('profile_id'); ->unique('profile_id');
}); });
} else { } else {
$s = Cache::remember(self::RECENT_KEY . $pid, self::RECENT_TTL, function() use($pid) { $s = Cache::remember(self::RECENT_KEY.$pid, self::RECENT_TTL, function () use ($pid) {
return Story::select('stories.*', 'followers.following_id') return Story::select('stories.*', 'followers.following_id')
->leftJoin('followers', 'followers.following_id', 'stories.profile_id') ->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
->where('followers.profile_id', $pid) ->where('followers.profile_id', $pid)
@ -59,9 +61,9 @@ class StoryApiV1Controller extends Controller
}); });
} }
$nodes = $s->map(function($s) use($pid) { $nodes = $s->map(function ($s) use ($pid) {
$profile = AccountService::get($s->profile_id, true); $profile = AccountService::get($s->profile_id, true);
if(!$profile || !isset($profile['id'])) { if (! $profile || ! isset($profile['id'])) {
return false; return false;
} }
@ -72,50 +74,51 @@ class StoryApiV1Controller extends Controller
'src' => url(Storage::url($s->path)), 'src' => url(Storage::url($s->path)),
'duration' => $s->duration ?? 3, 'duration' => $s->duration ?? 3,
'seen' => StoryService::hasSeen($pid, $s->id), 'seen' => StoryService::hasSeen($pid, $s->id),
'created_at' => $s->created_at->format('c') 'created_at' => $s->created_at->format('c'),
]; ];
}) })
->filter() ->filter()
->groupBy('pid') ->groupBy('pid')
->map(function($item) use($pid) { ->map(function ($item) use ($pid) {
$profile = AccountService::get($item[0]['pid'], true); $profile = AccountService::get($item[0]['pid'], true);
$url = $profile['local'] ? url("/stories/{$profile['username']}") : $url = $profile['local'] ? url("/stories/{$profile['username']}") :
url("/i/rs/{$profile['id']}"); url("/i/rs/{$profile['id']}");
return [
'id' => 'pfs:' . $profile['id'], return [
'user' => [ 'id' => 'pfs:'.$profile['id'],
'id' => (string) $profile['id'], 'user' => [
'username' => $profile['username'], 'id' => (string) $profile['id'],
'username_acct' => $profile['acct'], 'username' => $profile['username'],
'avatar' => $profile['avatar'], 'username_acct' => $profile['acct'],
'local' => $profile['local'], 'avatar' => $profile['avatar'],
'is_author' => $profile['id'] == $pid 'local' => $profile['local'],
], 'is_author' => $profile['id'] == $pid,
'nodes' => $item, ],
'url' => $url, 'nodes' => $item,
'seen' => StoryService::hasSeen($pid, StoryService::latest($profile['id'])), 'url' => $url,
]; 'seen' => StoryService::hasSeen($pid, StoryService::latest($profile['id'])),
}) ];
->sortBy('seen') })
->values(); ->sortBy('seen')
->values();
$res = [ $res = [
'self' => [], 'self' => [],
'nodes' => $nodes, 'nodes' => $nodes,
]; ];
if(Story::whereProfileId($pid)->whereActive(true)->exists()) { if (Story::whereProfileId($pid)->whereActive(true)->exists()) {
$selfStories = Story::whereProfileId($pid) $selfStories = Story::whereProfileId($pid)
->whereActive(true) ->whereActive(true)
->get() ->get()
->map(function($s) use($pid) { ->map(function ($s) {
return [ return [
'id' => (string) $s->id, 'id' => (string) $s->id,
'type' => $s->type, 'type' => $s->type,
'src' => url(Storage::url($s->path)), 'src' => url(Storage::url($s->path)),
'duration' => $s->duration, 'duration' => $s->duration,
'seen' => true, 'seen' => true,
'created_at' => $s->created_at->format('c') 'created_at' => $s->created_at->format('c'),
]; ];
}) })
->sortBy('id') ->sortBy('id')
@ -127,38 +130,40 @@ class StoryApiV1Controller extends Controller
'username' => $selfProfile['acct'], 'username' => $selfProfile['acct'],
'avatar' => $selfProfile['avatar'], 'avatar' => $selfProfile['avatar'],
'local' => $selfProfile['local'], 'local' => $selfProfile['local'],
'is_author' => true 'is_author' => true,
], ],
'nodes' => $selfStories, 'nodes' => $selfStories,
]; ];
} }
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
} }
public function selfCarousel(Request $request) public function selfCarousel(Request $request)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$pid = $request->user()->profile_id; $pid = $request->user()->profile_id;
if(config('database.default') == 'pgsql') { if (config('database.default') == 'pgsql') {
$s = Cache::remember(self::RECENT_KEY . $pid, self::RECENT_TTL, function() use($pid) { $s = Cache::remember(self::RECENT_KEY.$pid, self::RECENT_TTL, function () use ($pid) {
return Story::select('stories.*', 'followers.following_id') return Story::select('stories.*', 'followers.following_id')
->leftJoin('followers', 'followers.following_id', 'stories.profile_id') ->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
->where('followers.profile_id', $pid) ->where('followers.profile_id', $pid)
->where('stories.active', true) ->where('stories.active', true)
->map(function($s) { ->map(function ($s) {
$r = new \StdClass; $r = new \StdClass;
$r->id = $s->id; $r->id = $s->id;
$r->profile_id = $s->profile_id; $r->profile_id = $s->profile_id;
$r->type = $s->type; $r->type = $s->type;
$r->path = $s->path; $r->path = $s->path;
return $r; return $r;
}) })
->unique('profile_id'); ->unique('profile_id');
}); });
} else { } else {
$s = Cache::remember(self::RECENT_KEY . $pid, self::RECENT_TTL, function() use($pid) { $s = Cache::remember(self::RECENT_KEY.$pid, self::RECENT_TTL, function () use ($pid) {
return Story::select('stories.*', 'followers.following_id') return Story::select('stories.*', 'followers.following_id')
->leftJoin('followers', 'followers.following_id', 'stories.profile_id') ->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
->where('followers.profile_id', $pid) ->where('followers.profile_id', $pid)
@ -168,9 +173,9 @@ class StoryApiV1Controller extends Controller
}); });
} }
$nodes = $s->map(function($s) use($pid) { $nodes = $s->map(function ($s) use ($pid) {
$profile = AccountService::get($s->profile_id, true); $profile = AccountService::get($s->profile_id, true);
if(!$profile || !isset($profile['id'])) { if (! $profile || ! isset($profile['id'])) {
return false; return false;
} }
@ -181,32 +186,33 @@ class StoryApiV1Controller extends Controller
'src' => url(Storage::url($s->path)), 'src' => url(Storage::url($s->path)),
'duration' => $s->duration ?? 3, 'duration' => $s->duration ?? 3,
'seen' => StoryService::hasSeen($pid, $s->id), 'seen' => StoryService::hasSeen($pid, $s->id),
'created_at' => $s->created_at->format('c') 'created_at' => $s->created_at->format('c'),
]; ];
}) })
->filter() ->filter()
->groupBy('pid') ->groupBy('pid')
->map(function($item) use($pid) { ->map(function ($item) use ($pid) {
$profile = AccountService::get($item[0]['pid'], true); $profile = AccountService::get($item[0]['pid'], true);
$url = $profile['local'] ? url("/stories/{$profile['username']}") : $url = $profile['local'] ? url("/stories/{$profile['username']}") :
url("/i/rs/{$profile['id']}"); url("/i/rs/{$profile['id']}");
return [
'id' => 'pfs:' . $profile['id'], return [
'user' => [ 'id' => 'pfs:'.$profile['id'],
'id' => (string) $profile['id'], 'user' => [
'username' => $profile['username'], 'id' => (string) $profile['id'],
'username_acct' => $profile['acct'], 'username' => $profile['username'],
'avatar' => $profile['avatar'], 'username_acct' => $profile['acct'],
'local' => $profile['local'], 'avatar' => $profile['avatar'],
'is_author' => $profile['id'] == $pid 'local' => $profile['local'],
], 'is_author' => $profile['id'] == $pid,
'nodes' => $item, ],
'url' => $url, 'nodes' => $item,
'seen' => StoryService::hasSeen($pid, StoryService::latest($profile['id'])), 'url' => $url,
]; 'seen' => StoryService::hasSeen($pid, StoryService::latest($profile['id'])),
}) ];
->sortBy('seen') })
->values(); ->sortBy('seen')
->values();
$selfProfile = AccountService::get($pid, true); $selfProfile = AccountService::get($pid, true);
$res = [ $res = [
@ -216,7 +222,7 @@ class StoryApiV1Controller extends Controller
'username' => $selfProfile['acct'], 'username' => $selfProfile['acct'],
'avatar' => $selfProfile['avatar'], 'avatar' => $selfProfile['avatar'],
'local' => $selfProfile['local'], 'local' => $selfProfile['local'],
'is_author' => true 'is_author' => true,
], ],
'nodes' => [], 'nodes' => [],
@ -224,40 +230,41 @@ class StoryApiV1Controller extends Controller
'nodes' => $nodes, 'nodes' => $nodes,
]; ];
if(Story::whereProfileId($pid)->whereActive(true)->exists()) { if (Story::whereProfileId($pid)->whereActive(true)->exists()) {
$selfStories = Story::whereProfileId($pid) $selfStories = Story::whereProfileId($pid)
->whereActive(true) ->whereActive(true)
->get() ->get()
->map(function($s) use($pid) { ->map(function ($s) {
return [ return [
'id' => (string) $s->id, 'id' => (string) $s->id,
'type' => $s->type, 'type' => $s->type,
'src' => url(Storage::url($s->path)), 'src' => url(Storage::url($s->path)),
'duration' => $s->duration, 'duration' => $s->duration,
'seen' => true, 'seen' => true,
'created_at' => $s->created_at->format('c') 'created_at' => $s->created_at->format('c'),
]; ];
}) })
->sortBy('id') ->sortBy('id')
->values(); ->values();
$res['self']['nodes'] = $selfStories; $res['self']['nodes'] = $selfStories;
} }
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
} }
public function add(Request $request) public function add(Request $request)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$this->validate($request, [ $this->validate($request, [
'file' => function() { 'file' => function () {
return [ return [
'required', 'required',
'mimetypes:image/jpeg,image/png,video/mp4', 'mimetypes:image/jpeg,image/png,video/mp4',
'max:' . config_cache('pixelfed.max_photo_size'), 'max:'.config_cache('pixelfed.max_photo_size'),
]; ];
}, },
'duration' => 'sometimes|integer|min:0|max:30' 'duration' => 'sometimes|integer|min:0|max:30',
]); ]);
$user = $request->user(); $user = $request->user();
@ -267,7 +274,7 @@ class StoryApiV1Controller extends Controller
->where('expires_at', '>', now()) ->where('expires_at', '>', now())
->count(); ->count();
if($count >= Story::MAX_PER_DAY) { if ($count >= Story::MAX_PER_DAY) {
abort(418, 'You have reached your limit for new Stories today.'); abort(418, 'You have reached your limit for new Stories today.');
} }
@ -277,7 +284,7 @@ class StoryApiV1Controller extends Controller
$story = new Story(); $story = new Story();
$story->duration = $request->input('duration', 3); $story->duration = $request->input('duration', 3);
$story->profile_id = $user->profile_id; $story->profile_id = $user->profile_id;
$story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' :'photo'; $story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' : 'photo';
$story->mime = $photo->getMimeType(); $story->mime = $photo->getMimeType();
$story->path = $path; $story->path = $path;
$story->local = true; $story->local = true;
@ -290,10 +297,10 @@ class StoryApiV1Controller extends Controller
$res = [ $res = [
'code' => 200, 'code' => 200,
'msg' => 'Successfully added', 'msg' => 'Successfully added',
'media_id' => (string) $story->id, 'media_id' => (string) $story->id,
'media_url' => url(Storage::url($url)) . '?v=' . time(), 'media_url' => url(Storage::url($url)).'?v='.time(),
'media_type' => $story->type 'media_type' => $story->type,
]; ];
return $res; return $res;
@ -301,13 +308,13 @@ class StoryApiV1Controller extends Controller
public function publish(Request $request) public function publish(Request $request)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$this->validate($request, [ $this->validate($request, [
'media_id' => 'required', 'media_id' => 'required',
'duration' => 'required|integer|min:0|max:30', 'duration' => 'required|integer|min:0|max:30',
'can_reply' => 'required|boolean', 'can_reply' => 'required|boolean',
'can_react' => 'required|boolean' 'can_react' => 'required|boolean',
]); ]);
$id = $request->input('media_id'); $id = $request->input('media_id');
@ -327,13 +334,13 @@ class StoryApiV1Controller extends Controller
return [ return [
'code' => 200, 'code' => 200,
'msg' => 'Successfully published', 'msg' => 'Successfully published',
]; ];
} }
public function delete(Request $request, $id) public function delete(Request $request, $id)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$user = $request->user(); $user = $request->user();
@ -346,16 +353,16 @@ class StoryApiV1Controller extends Controller
return [ return [
'code' => 200, 'code' => 200,
'msg' => 'Successfully deleted' 'msg' => 'Successfully deleted',
]; ];
} }
public function viewed(Request $request) public function viewed(Request $request)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$this->validate($request, [ $this->validate($request, [
'id' => 'required|min:1', 'id' => 'required|min:1',
]); ]);
$id = $request->input('id'); $id = $request->input('id');
@ -367,44 +374,45 @@ class StoryApiV1Controller extends Controller
$profile = $story->profile; $profile = $story->profile;
if($story->profile_id == $authed->id) { if ($story->profile_id == $authed->id) {
return []; return [];
} }
$publicOnly = (bool) $profile->followedBy($authed); $publicOnly = (bool) $profile->followedBy($authed);
abort_if(!$publicOnly, 403); abort_if(! $publicOnly, 403);
$v = StoryView::firstOrCreate([ $v = StoryView::firstOrCreate([
'story_id' => $id, 'story_id' => $id,
'profile_id' => $authed->id 'profile_id' => $authed->id,
]); ]);
if($v->wasRecentlyCreated) { if ($v->wasRecentlyCreated) {
Story::findOrFail($story->id)->increment('view_count'); Story::findOrFail($story->id)->increment('view_count');
if($story->local == false) { if ($story->local == false) {
StoryViewDeliver::dispatch($story, $authed)->onQueue('story'); StoryViewDeliver::dispatch($story, $authed)->onQueue('story');
} }
} }
Cache::forget('stories:recent:by_id:' . $authed->id); Cache::forget('stories:recent:by_id:'.$authed->id);
StoryService::addSeen($authed->id, $story->id); StoryService::addSeen($authed->id, $story->id);
return ['code' => 200]; return ['code' => 200];
} }
public function comment(Request $request) public function comment(Request $request)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$this->validate($request, [ $this->validate($request, [
'sid' => 'required', 'sid' => 'required',
'caption' => 'required|string' 'caption' => 'required|string',
]); ]);
$pid = $request->user()->profile_id; $pid = $request->user()->profile_id;
$text = $request->input('caption'); $text = $request->input('caption');
$story = Story::findOrFail($request->input('sid')); $story = Story::findOrFail($request->input('sid'));
abort_if(!$story->can_reply, 422); abort_if(! $story->can_reply, 422);
$status = new Status; $status = new Status;
$status->type = 'story:reply'; $status->type = 'story:reply';
@ -415,7 +423,7 @@ class StoryApiV1Controller extends Controller
$status->visibility = 'direct'; $status->visibility = 'direct';
$status->in_reply_to_profile_id = $story->profile_id; $status->in_reply_to_profile_id = $story->profile_id;
$status->entities = json_encode([ $status->entities = json_encode([
'story_id' => $story->id 'story_id' => $story->id,
]); ]);
$status->save(); $status->save();
@ -429,24 +437,24 @@ class StoryApiV1Controller extends Controller
'story_actor_username' => $request->user()->username, 'story_actor_username' => $request->user()->username,
'story_id' => $story->id, 'story_id' => $story->id,
'story_media_url' => url(Storage::url($story->path)), 'story_media_url' => url(Storage::url($story->path)),
'caption' => $text 'caption' => $text,
]); ]);
$dm->save(); $dm->save();
Conversation::updateOrInsert( Conversation::updateOrInsert(
[ [
'to_id' => $story->profile_id, 'to_id' => $story->profile_id,
'from_id' => $pid 'from_id' => $pid,
], ],
[ [
'type' => 'story:comment', 'type' => 'story:comment',
'status_id' => $status->id, 'status_id' => $status->id,
'dm_id' => $dm->id, 'dm_id' => $dm->id,
'is_hidden' => false 'is_hidden' => false,
] ]
); );
if($story->local) { if ($story->local) {
$n = new Notification; $n = new Notification;
$n->profile_id = $dm->to_id; $n->profile_id = $dm->to_id;
$n->actor_id = $dm->from_id; $n->actor_id = $dm->from_id;
@ -460,33 +468,35 @@ class StoryApiV1Controller extends Controller
return [ return [
'code' => 200, 'code' => 200,
'msg' => 'Sent!' 'msg' => 'Sent!',
]; ];
} }
protected function storeMedia($photo, $user) protected function storeMedia($photo, $user)
{ {
$mimes = explode(',', config_cache('pixelfed.media_types')); $mimes = explode(',', config_cache('pixelfed.media_types'));
if(in_array($photo->getMimeType(), [ if (in_array($photo->getMimeType(), [
'image/jpeg', 'image/jpeg',
'image/png', 'image/png',
'video/mp4' 'video/mp4',
]) == false) { ]) == false) {
abort(400, 'Invalid media type'); abort(400, 'Invalid media type');
return; return;
} }
$storagePath = MediaPathService::story($user->profile); $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()); $path = $photo->storePubliclyAs($storagePath, Str::random(random_int(2, 12)).'_'.Str::random(random_int(32, 35)).'_'.Str::random(random_int(1, 14)).'.'.$photo->extension());
return $path; return $path;
} }
public function viewers(Request $request) public function viewers(Request $request)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$this->validate($request, [ $this->validate($request, [
'sid' => 'required|string|min:1|max:50' 'sid' => 'required|string|min:1|max:50',
]); ]);
$pid = $request->user()->profile_id; $pid = $request->user()->profile_id;

View file

@ -2,59 +2,52 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use App\Media;
use App\Profile;
use App\Report;
use App\DirectMessage; use App\DirectMessage;
use App\Notification; use App\Jobs\StoryPipeline\StoryDelete;
use App\Status; use App\Jobs\StoryPipeline\StoryFanout;
use App\Story;
use App\StoryView;
use App\Models\Poll;
use App\Models\PollVote;
use App\Services\ProfileService;
use App\Services\StoryService;
use Cache, Storage;
use Image as Intervention;
use App\Services\FollowerService;
use App\Services\MediaPathService;
use FFMpeg;
use FFMpeg\Coordinate\Dimension;
use FFMpeg\Format\Video\X264;
use App\Jobs\StoryPipeline\StoryReactionDeliver; use App\Jobs\StoryPipeline\StoryReactionDeliver;
use App\Jobs\StoryPipeline\StoryReplyDeliver; use App\Jobs\StoryPipeline\StoryReplyDeliver;
use App\Jobs\StoryPipeline\StoryFanout;
use App\Jobs\StoryPipeline\StoryDelete;
use ImageOptimizer;
use App\Models\Conversation; use App\Models\Conversation;
use App\Models\Poll;
use App\Models\PollVote;
use App\Notification;
use App\Report;
use App\Services\FollowerService;
use App\Services\MediaPathService;
use App\Services\StoryService;
use App\Services\UserRoleService; use App\Services\UserRoleService;
use App\Status;
use App\Story;
use FFMpeg;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Image as Intervention;
use Storage;
class StoryComposeController extends Controller class StoryComposeController extends Controller
{ {
public function apiV1Add(Request $request) public function apiV1Add(Request $request)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$this->validate($request, [ $this->validate($request, [
'file' => function() { 'file' => function () {
return [ return [
'required', 'required',
'mimetypes:image/jpeg,image/png,video/mp4', 'mimetypes:image/jpeg,image/png,video/mp4',
'max:' . config_cache('pixelfed.max_photo_size'), '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'); abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
$count = Story::whereProfileId($user->profile_id) $count = Story::whereProfileId($user->profile_id)
->whereActive(true) ->whereActive(true)
->where('expires_at', '>', now()) ->where('expires_at', '>', now())
->count(); ->count();
if($count >= Story::MAX_PER_DAY) { if ($count >= Story::MAX_PER_DAY) {
abort(418, 'You have reached your limit for new Stories today.'); abort(418, 'You have reached your limit for new Stories today.');
} }
@ -64,7 +57,7 @@ class StoryComposeController extends Controller
$story = new Story(); $story = new Story();
$story->duration = 3; $story->duration = 3;
$story->profile_id = $user->profile_id; $story->profile_id = $user->profile_id;
$story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' :'photo'; $story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' : 'photo';
$story->mime = $photo->getMimeType(); $story->mime = $photo->getMimeType();
$story->path = $path; $story->path = $path;
$story->local = true; $story->local = true;
@ -77,21 +70,22 @@ class StoryComposeController extends Controller
$res = [ $res = [
'code' => 200, 'code' => 200,
'msg' => 'Successfully added', 'msg' => 'Successfully added',
'media_id' => (string) $story->id, 'media_id' => (string) $story->id,
'media_url' => url(Storage::url($url)) . '?v=' . time(), 'media_url' => url(Storage::url($url)).'?v='.time(),
'media_type' => $story->type 'media_type' => $story->type,
]; ];
if($story->type === 'video') { if ($story->type === 'video') {
$video = FFMpeg::open($path); $video = FFMpeg::open($path);
$duration = $video->getDurationInSeconds(); $duration = $video->getDurationInSeconds();
$res['media_duration'] = $duration; $res['media_duration'] = $duration;
if($duration > 500) { if ($duration > 500) {
Storage::delete($story->path); Storage::delete($story->path);
$story->delete(); $story->delete();
return response()->json([ return response()->json([
'message' => 'Video duration cannot exceed 60 seconds' 'message' => 'Video duration cannot exceed 60 seconds',
], 422); ], 422);
} }
} }
@ -102,37 +96,39 @@ class StoryComposeController extends Controller
protected function storePhoto($photo, $user) protected function storePhoto($photo, $user)
{ {
$mimes = explode(',', config_cache('pixelfed.media_types')); $mimes = explode(',', config_cache('pixelfed.media_types'));
if(in_array($photo->getMimeType(), [ if (in_array($photo->getMimeType(), [
'image/jpeg', 'image/jpeg',
'image/png', 'image/png',
'video/mp4' 'video/mp4',
]) == false) { ]) == false) {
abort(400, 'Invalid media type'); abort(400, 'Invalid media type');
return; return;
} }
$storagePath = MediaPathService::story($user->profile); $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()); $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'])) { if (in_array($photo->getMimeType(), ['image/jpeg', 'image/png'])) {
$fpath = storage_path('app/' . $path); $fpath = storage_path('app/'.$path);
$img = Intervention::make($fpath); $img = Intervention::make($fpath);
$img->orientate(); $img->orientate();
$img->save($fpath, config_cache('pixelfed.image_quality')); $img->save($fpath, config_cache('pixelfed.image_quality'));
$img->destroy(); $img->destroy();
} }
return $path; return $path;
} }
public function cropPhoto(Request $request) public function cropPhoto(Request $request)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$this->validate($request, [ $this->validate($request, [
'media_id' => 'required|integer|min:1', 'media_id' => 'required|integer|min:1',
'width' => 'required', 'width' => 'required',
'height' => 'required', 'height' => 'required',
'x' => 'required', 'x' => 'required',
'y' => 'required' 'y' => 'required',
]); ]);
$user = $request->user(); $user = $request->user();
@ -144,13 +140,13 @@ class StoryComposeController extends Controller
$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)) { if (! is_file($path)) {
abort(400, 'Invalid or missing media.'); abort(400, 'Invalid or missing media.');
} }
if($story->type === 'photo') { if ($story->type === 'photo') {
$img = Intervention::make($path); $img = Intervention::make($path);
$img->crop($width, $height, $x, $y); $img->crop($width, $height, $x, $y);
$img->resize(1080, 1920, function ($constraint) { $img->resize(1080, 1920, function ($constraint) {
@ -161,24 +157,24 @@ class StoryComposeController extends Controller
return [ return [
'code' => 200, 'code' => 200,
'msg' => 'Successfully cropped', 'msg' => 'Successfully cropped',
]; ];
} }
public function publishStory(Request $request) public function publishStory(Request $request)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$this->validate($request, [ $this->validate($request, [
'media_id' => 'required', 'media_id' => 'required',
'duration' => 'required|integer|min:3|max:120', 'duration' => 'required|integer|min:3|max:120',
'can_reply' => 'required|boolean', 'can_reply' => 'required|boolean',
'can_react' => 'required|boolean' 'can_react' => 'required|boolean',
]); ]);
$id = $request->input('media_id'); $id = $request->input('media_id');
$user = $request->user(); $user = $request->user();
abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
$story = Story::whereProfileId($user->profile_id) $story = Story::whereProfileId($user->profile_id)
->findOrFail($id); ->findOrFail($id);
@ -194,13 +190,13 @@ class StoryComposeController extends Controller
return [ return [
'code' => 200, 'code' => 200,
'msg' => 'Successfully published', 'msg' => 'Successfully published',
]; ];
} }
public function apiV1Delete(Request $request, $id) public function apiV1Delete(Request $request, $id)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$user = $request->user(); $user = $request->user();
@ -213,40 +209,40 @@ class StoryComposeController extends Controller
return [ return [
'code' => 200, 'code' => 200,
'msg' => 'Successfully deleted' 'msg' => 'Successfully deleted',
]; ];
} }
public function compose(Request $request) public function compose(Request $request)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$user = $request->user(); $user = $request->user();
abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); abort_if($user->has_roles && ! UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action');
return view('stories.compose'); return view('stories.compose');
} }
public function createPoll(Request $request) public function createPoll(Request $request)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
abort_if(!config_cache('instance.polls.enabled'), 404); abort_if(! config_cache('instance.polls.enabled'), 404);
return $request->all(); return $request->all();
} }
public function publishStoryPoll(Request $request) public function publishStoryPoll(Request $request)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$this->validate($request, [ $this->validate($request, [
'question' => 'required|string|min:6|max:140', 'question' => 'required|string|min:6|max:140',
'options' => 'required|array|min:2|max:4', 'options' => 'required|array|min:2|max:4',
'can_reply' => 'required|boolean', 'can_reply' => 'required|boolean',
'can_react' => 'required|boolean' 'can_react' => 'required|boolean',
]); ]);
$user = $request->user(); $user = $request->user();
abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); 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) $count = Story::whereProfileId($pid)
@ -254,7 +250,7 @@ class StoryComposeController extends Controller
->where('expires_at', '>', now()) ->where('expires_at', '>', now())
->count(); ->count();
if($count >= Story::MAX_PER_DAY) { if ($count >= Story::MAX_PER_DAY) {
abort(418, 'You have reached your limit for new Stories today.'); abort(418, 'You have reached your limit for new Stories today.');
} }
@ -262,7 +258,7 @@ class StoryComposeController extends Controller
$story->type = 'poll'; $story->type = 'poll';
$story->story = json_encode([ $story->story = json_encode([
'question' => $request->input('question'), 'question' => $request->input('question'),
'options' => $request->input('options') 'options' => $request->input('options'),
]); ]);
$story->public = false; $story->public = false;
$story->local = true; $story->local = true;
@ -278,7 +274,7 @@ class StoryComposeController extends Controller
$poll->profile_id = $pid; $poll->profile_id = $pid;
$poll->poll_options = $request->input('options'); $poll->poll_options = $request->input('options');
$poll->expires_at = $story->expires_at; $poll->expires_at = $story->expires_at;
$poll->cached_tallies = collect($poll->poll_options)->map(function($o) { $poll->cached_tallies = collect($poll->poll_options)->map(function ($o) {
return 0; return 0;
})->toArray(); })->toArray();
$poll->save(); $poll->save();
@ -290,23 +286,23 @@ class StoryComposeController extends Controller
return [ return [
'code' => 200, 'code' => 200,
'msg' => 'Successfully published', 'msg' => 'Successfully published',
]; ];
} }
public function storyPollVote(Request $request) public function storyPollVote(Request $request)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$this->validate($request, [ $this->validate($request, [
'sid' => 'required', 'sid' => 'required',
'ci' => 'required|integer|min:0|max:3' 'ci' => 'required|integer|min:0|max:3',
]); ]);
$pid = $request->user()->profile_id; $pid = $request->user()->profile_id;
$ci = $request->input('ci'); $ci = $request->input('ci');
$story = Story::findOrFail($request->input('sid')); $story = Story::findOrFail($request->input('sid'));
abort_if(!FollowerService::follows($pid, $story->profile_id), 403); abort_if(! FollowerService::follows($pid, $story->profile_id), 403);
$poll = Poll::whereStoryId($story->id)->firstOrFail(); $poll = Poll::whereStoryId($story->id)->firstOrFail();
$vote = new PollVote; $vote = new PollVote;
@ -318,7 +314,7 @@ class StoryComposeController extends Controller
$vote->save(); $vote->save();
$poll->votes_count = $poll->votes_count + 1; $poll->votes_count = $poll->votes_count + 1;
$poll->cached_tallies = collect($poll->getTallies())->map(function($tally, $key) use($ci) { $poll->cached_tallies = collect($poll->getTallies())->map(function ($tally, $key) use ($ci) {
return $ci == $key ? $tally + 1 : $tally; return $ci == $key ? $tally + 1 : $tally;
})->toArray(); })->toArray();
$poll->save(); $poll->save();
@ -328,15 +324,15 @@ class StoryComposeController extends Controller
public function storeReport(Request $request) public function storeReport(Request $request)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$this->validate($request, [ $this->validate($request, [
'type' => 'required|alpha_dash', 'type' => 'required|alpha_dash',
'id' => 'required|integer|min:1', 'id' => 'required|integer|min:1',
]); ]);
$user = $request->user(); $user = $request->user();
abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); 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;
$sid = $request->input('id'); $sid = $request->input('id');
@ -353,24 +349,24 @@ class StoryComposeController extends Controller
'copyright', 'copyright',
'impersonation', 'impersonation',
'scam', 'scam',
'terrorism' 'terrorism',
]; ];
abort_if(!in_array($type, $types), 422, 'Invalid story report type'); abort_if(! in_array($type, $types), 422, 'Invalid story report type');
$story = Story::findOrFail($sid); $story = Story::findOrFail($sid);
abort_if($story->profile_id == $pid, 422, 'Cannot report your own story'); abort_if($story->profile_id == $pid, 422, 'Cannot report your own story');
abort_if(!FollowerService::follows($pid, $story->profile_id), 422, 'Cannot report a story from an account you do not follow'); abort_if(! FollowerService::follows($pid, $story->profile_id), 422, 'Cannot report a story from an account you do not follow');
if( Report::whereProfileId($pid) if (Report::whereProfileId($pid)
->whereObjectType('App\Story') ->whereObjectType('App\Story')
->whereObjectId($story->id) ->whereObjectId($story->id)
->exists() ->exists()
) { ) {
return response()->json(['error' => [ return response()->json(['error' => [
'code' => 409, 'code' => 409,
'message' => 'Cannot report the same story again' 'message' => 'Cannot report the same story again',
]], 409); ]], 409);
} }
@ -389,18 +385,18 @@ class StoryComposeController extends Controller
public function react(Request $request) public function react(Request $request)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$this->validate($request, [ $this->validate($request, [
'sid' => 'required', 'sid' => 'required',
'reaction' => 'required|string' 'reaction' => 'required|string',
]); ]);
$pid = $request->user()->profile_id; $pid = $request->user()->profile_id;
$text = $request->input('reaction'); $text = $request->input('reaction');
$user = $request->user(); $user = $request->user();
abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); 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(! $story->can_react, 422);
abort_if(StoryService::reactCounter($story->id, $pid) >= 5, 422, 'You have already reacted to this story'); abort_if(StoryService::reactCounter($story->id, $pid) >= 5, 422, 'You have already reacted to this story');
$status = new Status; $status = new Status;
@ -413,7 +409,7 @@ class StoryComposeController extends Controller
$status->in_reply_to_profile_id = $story->profile_id; $status->in_reply_to_profile_id = $story->profile_id;
$status->entities = json_encode([ $status->entities = json_encode([
'story_id' => $story->id, 'story_id' => $story->id,
'reaction' => $text 'reaction' => $text,
]); ]);
$status->save(); $status->save();
@ -427,24 +423,24 @@ class StoryComposeController extends Controller
'story_actor_username' => $request->user()->username, 'story_actor_username' => $request->user()->username,
'story_id' => $story->id, 'story_id' => $story->id,
'story_media_url' => url(Storage::url($story->path)), 'story_media_url' => url(Storage::url($story->path)),
'reaction' => $text 'reaction' => $text,
]); ]);
$dm->save(); $dm->save();
Conversation::updateOrInsert( Conversation::updateOrInsert(
[ [
'to_id' => $story->profile_id, 'to_id' => $story->profile_id,
'from_id' => $pid 'from_id' => $pid,
], ],
[ [
'type' => 'story:react', 'type' => 'story:react',
'status_id' => $status->id, 'status_id' => $status->id,
'dm_id' => $dm->id, 'dm_id' => $dm->id,
'is_hidden' => false 'is_hidden' => false,
] ]
); );
if($story->local) { if ($story->local) {
// generate notification // generate notification
$n = new Notification; $n = new Notification;
$n->profile_id = $dm->to_id; $n->profile_id = $dm->to_id;
@ -464,18 +460,18 @@ class StoryComposeController extends Controller
public function comment(Request $request) public function comment(Request $request)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
$this->validate($request, [ $this->validate($request, [
'sid' => 'required', 'sid' => 'required',
'caption' => 'required|string' 'caption' => 'required|string',
]); ]);
$pid = $request->user()->profile_id; $pid = $request->user()->profile_id;
$text = $request->input('caption'); $text = $request->input('caption');
$user = $request->user(); $user = $request->user();
abort_if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id), 403, 'Invalid permissions for this action'); 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_reply, 422); abort_if(! $story->can_reply, 422);
$status = new Status; $status = new Status;
$status->type = 'story:reply'; $status->type = 'story:reply';
@ -486,7 +482,7 @@ class StoryComposeController extends Controller
$status->visibility = 'direct'; $status->visibility = 'direct';
$status->in_reply_to_profile_id = $story->profile_id; $status->in_reply_to_profile_id = $story->profile_id;
$status->entities = json_encode([ $status->entities = json_encode([
'story_id' => $story->id 'story_id' => $story->id,
]); ]);
$status->save(); $status->save();
@ -500,24 +496,24 @@ class StoryComposeController extends Controller
'story_actor_username' => $request->user()->username, 'story_actor_username' => $request->user()->username,
'story_id' => $story->id, 'story_id' => $story->id,
'story_media_url' => url(Storage::url($story->path)), 'story_media_url' => url(Storage::url($story->path)),
'caption' => $text 'caption' => $text,
]); ]);
$dm->save(); $dm->save();
Conversation::updateOrInsert( Conversation::updateOrInsert(
[ [
'to_id' => $story->profile_id, 'to_id' => $story->profile_id,
'from_id' => $pid 'from_id' => $pid,
], ],
[ [
'type' => 'story:comment', 'type' => 'story:comment',
'status_id' => $status->id, 'status_id' => $status->id,
'dm_id' => $dm->id, 'dm_id' => $dm->id,
'is_hidden' => false 'is_hidden' => false,
] ]
); );
if($story->local) { if ($story->local) {
// generate notification // generate notification
$n = new Notification; $n = new Notification;
$n->profile_id = $dm->to_id; $n->profile_id = $dm->to_id;

View file

@ -34,7 +34,7 @@ class StoryController extends StoryComposeController
{ {
public function recent(Request $request) public function recent(Request $request)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
$user = $request->user(); $user = $request->user();
if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) { if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) {
return []; return [];
@ -117,7 +117,7 @@ class StoryController extends StoryComposeController
public function profile(Request $request, $id) public function profile(Request $request, $id)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
$user = $request->user(); $user = $request->user();
if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) { if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) {
@ -176,7 +176,7 @@ class StoryController extends StoryComposeController
public function viewed(Request $request) public function viewed(Request $request)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [ $this->validate($request, [
'id' => 'required|min:1', 'id' => 'required|min:1',
@ -221,7 +221,7 @@ class StoryController extends StoryComposeController
public function exists(Request $request, $id) public function exists(Request $request, $id)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
$user = $request->user(); $user = $request->user();
if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) { if($user->has_roles && !UserRoleService::can('can-use-stories', $user->id)) {
return response()->json(false); return response()->json(false);
@ -233,7 +233,7 @@ class StoryController extends StoryComposeController
public function iRedirect(Request $request) public function iRedirect(Request $request)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
$user = $request->user(); $user = $request->user();
abort_if(!$user, 404); abort_if(!$user, 404);
@ -243,7 +243,7 @@ class StoryController extends StoryComposeController
public function viewers(Request $request) public function viewers(Request $request)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [ $this->validate($request, [
'sid' => 'required|string' 'sid' => 'required|string'
@ -274,7 +274,7 @@ class StoryController extends StoryComposeController
public function remoteStory(Request $request, $id) public function remoteStory(Request $request, $id)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
$profile = Profile::findOrFail($id); $profile = Profile::findOrFail($id);
if($profile->user_id != null || $profile->domain == null) { if($profile->user_id != null || $profile->domain == null) {
@ -286,7 +286,7 @@ class StoryController extends StoryComposeController
public function pollResults(Request $request) public function pollResults(Request $request)
{ {
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404); abort_if(!(bool) config_cache('instance.stories.enabled') || !$request->user(), 404);
$this->validate($request, [ $this->validate($request, [
'sid' => 'required|string' 'sid' => 'required|string'
@ -304,7 +304,7 @@ class StoryController extends StoryComposeController
public function getActivityObject(Request $request, $username, $id) public function getActivityObject(Request $request, $username, $id)
{ {
abort_if(!config_cache('instance.stories.enabled'), 404); abort_if(!(bool) config_cache('instance.stories.enabled'), 404);
if(!$request->wantsJson()) { if(!$request->wantsJson()) {
return redirect('/stories/' . $username); return redirect('/stories/' . $username);

View file

@ -34,7 +34,7 @@ class UserEmailForgotController extends Controller
'username.exists' => 'This username is no longer active or does not exist!' 'username.exists' => 'This username is no longer active or does not exist!'
]; ];
if(config('captcha.enabled') || config('captcha.active.login') || config('captcha.active.register')) { if((bool) config_cache('captcha.enabled')) {
$rules['h-captcha-response'] = 'required|captcha'; $rules['h-captcha-response'] = 'required|captcha';
$messages['h-captcha-response.required'] = 'You need to complete the captcha!'; $messages['h-captcha-response.required'] = 'You need to complete the captcha!';
} }

View file

@ -2,10 +2,10 @@
namespace App\Http\Requests\Status; namespace App\Http\Requests\Status;
use Illuminate\Foundation\Http\FormRequest;
use App\Media; use App\Media;
use App\Status; use App\Status;
use Closure; use Closure;
use Illuminate\Foundation\Http\FormRequest;
class StoreStatusEditRequest extends FormRequest class StoreStatusEditRequest extends FormRequest
{ {
@ -14,24 +14,25 @@ class StoreStatusEditRequest extends FormRequest
*/ */
public function authorize(): bool public function authorize(): bool
{ {
$profile = $this->user()->profile; $profile = $this->user()->profile;
if($profile->status != null) { if ($profile->status != null) {
return false; return false;
} }
if($profile->unlisted == true && $profile->cw == true) { if ($profile->unlisted == true && $profile->cw == true) {
return false; return false;
} }
$types = [ $types = [
"photo", 'photo',
"photo:album", 'photo:album',
"photo:video:album", 'photo:video:album',
"reply", 'reply',
"text", 'text',
"video", 'video',
"video:album" 'video:album',
]; ];
$scopes = ['public', 'unlisted', 'private']; $scopes = ['public', 'unlisted', 'private'];
$status = Status::whereNull('reblog_of_id')->whereIn('type', $types)->whereIn('scope', $scopes)->find($this->route('id')); $status = Status::whereNull('reblog_of_id')->whereIn('type', $types)->whereIn('scope', $scopes)->find($this->route('id'));
return $status && $this->user()->profile_id === $status->profile_id; return $status && $this->user()->profile_id === $status->profile_id;
} }
@ -47,18 +48,18 @@ class StoreStatusEditRequest extends FormRequest
'spoiler_text' => 'nullable|string|max:140', 'spoiler_text' => 'nullable|string|max:140',
'sensitive' => 'sometimes|boolean', 'sensitive' => 'sometimes|boolean',
'media_ids' => [ 'media_ids' => [
'nullable', 'nullable',
'required_without:status', 'required_without:status',
'array', 'array',
'max:' . config('pixelfed.max_album_length'), 'max:'.(int) config_cache('pixelfed.max_album_length'),
function (string $attribute, mixed $value, Closure $fail) { function (string $attribute, mixed $value, Closure $fail) {
Media::whereProfileId($this->user()->profile_id) Media::whereProfileId($this->user()->profile_id)
->where(function($query) { ->where(function ($query) {
return $query->whereNull('status_id') return $query->whereNull('status_id')
->orWhere('status_id', '=', $this->route('id')); ->orWhere('status_id', '=', $this->route('id'));
}) })
->findOrFail($value); ->findOrFail($value);
}, },
], ],
'location' => 'sometimes|nullable', 'location' => 'sometimes|nullable',
'location.id' => 'sometimes|integer|min:1|max:128769', 'location.id' => 'sometimes|integer|min:1|max:128769',

View file

@ -2,9 +2,9 @@
namespace App\Jobs\AvatarPipeline; namespace App\Jobs\AvatarPipeline;
use Cache;
use App\Avatar; use App\Avatar;
use App\Profile; use App\Profile;
use Cache;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
@ -17,88 +17,88 @@ use Storage;
class AvatarOptimize implements ShouldQueue class AvatarOptimize implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $profile; protected $profile;
protected $current;
/** protected $current;
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/** /**
* Create a new job instance. * Delete the job if its models no longer exist.
* *
* @return void * @var bool
*/ */
public function __construct(Profile $profile, $current) public $deleteWhenMissingModels = true;
{
$this->profile = $profile;
$this->current = $current;
}
/** /**
* Execute the job. * Create a new job instance.
* *
* @return void * @return void
*/ */
public function handle() public function __construct(Profile $profile, $current)
{ {
$avatar = $this->profile->avatar; $this->profile = $profile;
$file = storage_path("app/$avatar->media_path"); $this->current = $current;
}
try { /**
$img = Intervention::make($file)->orientate(); * Execute the job.
$img->fit(200, 200, function ($constraint) { *
$constraint->upsize(); * @return void
}); */
$quality = config_cache('pixelfed.image_quality'); public function handle()
$img->save($file, $quality); {
$avatar = $this->profile->avatar;
$file = storage_path("app/$avatar->media_path");
$avatar = Avatar::whereProfileId($this->profile->id)->firstOrFail(); try {
$avatar->change_count = ++$avatar->change_count; $img = Intervention::make($file)->orientate();
$avatar->last_processed_at = Carbon::now(); $img->fit(200, 200, function ($constraint) {
$avatar->save(); $constraint->upsize();
Cache::forget('avatar:' . $avatar->profile_id); });
$this->deleteOldAvatar($avatar->media_path, $this->current); $quality = config_cache('pixelfed.image_quality');
$img->save($file, $quality);
if(config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud')) { $avatar = Avatar::whereProfileId($this->profile->id)->firstOrFail();
$this->uploadToCloud($avatar); $avatar->change_count = ++$avatar->change_count;
} else { $avatar->last_processed_at = Carbon::now();
$avatar->cdn_url = null; $avatar->save();
$avatar->save(); Cache::forget('avatar:'.$avatar->profile_id);
} $this->deleteOldAvatar($avatar->media_path, $this->current);
} catch (Exception $e) {
}
}
protected function deleteOldAvatar($new, $current) if ((bool) config_cache('pixelfed.cloud_storage') && (bool) config_cache('instance.avatar.local_to_cloud')) {
{ $this->uploadToCloud($avatar);
if ( storage_path('app/'.$new) == $current || } else {
Str::endsWith($current, 'avatars/default.png') || $avatar->cdn_url = null;
Str::endsWith($current, 'avatars/default.jpg')) $avatar->save();
{ }
return; } catch (Exception $e) {
} }
if (is_file($current)) { }
@unlink($current);
}
}
protected function uploadToCloud($avatar) protected function deleteOldAvatar($new, $current)
{ {
$base = 'cache/avatars/' . $avatar->profile_id; if (storage_path('app/'.$new) == $current ||
$disk = Storage::disk(config('filesystems.cloud')); Str::endsWith($current, 'avatars/default.png') ||
$disk->deleteDirectory($base); Str::endsWith($current, 'avatars/default.jpg')) {
$path = $base . '/' . 'avatar_' . strtolower(Str::random(random_int(3,6))) . $avatar->change_count . '.' . pathinfo($avatar->media_path, PATHINFO_EXTENSION); return;
$url = $disk->put($path, Storage::get($avatar->media_path)); }
$avatar->media_path = $path; if (is_file($current)) {
$avatar->cdn_url = $disk->url($path); @unlink($current);
$avatar->save(); }
Storage::delete($avatar->media_path); }
Cache::forget('avatar:' . $avatar->profile_id);
} protected function uploadToCloud($avatar)
{
$base = 'cache/avatars/'.$avatar->profile_id;
$disk = Storage::disk(config('filesystems.cloud'));
$disk->deleteDirectory($base);
$path = $base.'/'.'avatar_'.strtolower(Str::random(random_int(3, 6))).$avatar->change_count.'.'.pathinfo($avatar->media_path, PATHINFO_EXTENSION);
$url = $disk->put($path, Storage::get($avatar->media_path));
$avatar->media_path = $path;
$avatar->cdn_url = $disk->url($path);
$avatar->save();
Storage::delete($avatar->media_path);
Cache::forget('avatar:'.$avatar->profile_id);
}
} }

View file

@ -4,112 +4,107 @@ namespace App\Jobs\AvatarPipeline;
use App\Avatar; use App\Avatar;
use App\Profile; use App\Profile;
use App\Services\MediaStorageService;
use App\Util\ActivityPub\Helpers;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use App\Util\ActivityPub\Helpers;
use Illuminate\Support\Str;
use Zttp\Zttp;
use App\Http\Controllers\AvatarController;
use Storage;
use Log;
use Illuminate\Http\File;
use App\Services\MediaStorageService;
use App\Services\ActivityPubFetchService;
class RemoteAvatarFetch implements ShouldQueue class RemoteAvatarFetch implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $profile; protected $profile;
/** /**
* Delete the job if its models no longer exist. * Delete the job if its models no longer exist.
* *
* @var bool * @var bool
*/ */
public $deleteWhenMissingModels = true; public $deleteWhenMissingModels = true;
/** /**
* The number of times the job may be attempted. * The number of times the job may be attempted.
* *
* @var int * @var int
*/ */
public $tries = 1; public $tries = 1;
public $timeout = 300;
public $maxExceptions = 1;
/** public $timeout = 300;
* Create a new job instance.
*
* @return void
*/
public function __construct(Profile $profile)
{
$this->profile = $profile;
}
/** public $maxExceptions = 1;
* Execute the job.
*
* @return void
*/
public function handle()
{
$profile = $this->profile;
if(boolval(config_cache('pixelfed.cloud_storage')) == false && boolval(config_cache('federation.avatars.store_local')) == false) { /**
return 1; * Create a new job instance.
} *
* @return void
*/
public function __construct(Profile $profile)
{
$this->profile = $profile;
}
if($profile->domain == null || $profile->private_key) { /**
return 1; * Execute the job.
} *
* @return void
*/
public function handle()
{
$profile = $this->profile;
$avatar = Avatar::whereProfileId($profile->id)->first(); if ((bool) config_cache('pixelfed.cloud_storage') == false && (bool) config_cache('federation.avatars.store_local') == false) {
return 1;
}
if(!$avatar) { if ($profile->domain == null || $profile->private_key) {
$avatar = new Avatar; return 1;
$avatar->profile_id = $profile->id; }
$avatar->save();
}
if($avatar->media_path == null && $avatar->remote_url == null) { $avatar = Avatar::whereProfileId($profile->id)->first();
$avatar->media_path = 'public/avatars/default.jpg';
$avatar->is_remote = true;
$avatar->save();
}
$person = Helpers::fetchFromUrl($profile->remote_url); if (! $avatar) {
$avatar = new Avatar;
$avatar->profile_id = $profile->id;
$avatar->save();
}
if(!$person || !isset($person['@context'])) { if ($avatar->media_path == null && $avatar->remote_url == null) {
return 1; $avatar->media_path = 'public/avatars/default.jpg';
} $avatar->is_remote = true;
$avatar->save();
}
if( !isset($person['icon']) || $person = Helpers::fetchFromUrl($profile->remote_url);
!isset($person['icon']['type']) ||
!isset($person['icon']['url'])
) {
return 1;
}
if($person['icon']['type'] !== 'Image') { if (! $person || ! isset($person['@context'])) {
return 1; return 1;
} }
if(!Helpers::validateUrl($person['icon']['url'])) { if (! isset($person['icon']) ||
return 1; ! isset($person['icon']['type']) ||
} ! isset($person['icon']['url'])
) {
return 1;
}
$icon = $person['icon']; if ($person['icon']['type'] !== 'Image') {
return 1;
}
$avatar->remote_url = $icon['url']; if (! Helpers::validateUrl($person['icon']['url'])) {
$avatar->save(); return 1;
}
MediaStorageService::avatar($avatar, boolval(config_cache('pixelfed.cloud_storage')) == false, true); $icon = $person['icon'];
return 1; $avatar->remote_url = $icon['url'];
} $avatar->save();
MediaStorageService::avatar($avatar, (bool) config_cache('pixelfed.cloud_storage') == false, true);
return 1;
}
} }

View file

@ -4,93 +4,88 @@ namespace App\Jobs\AvatarPipeline;
use App\Avatar; use App\Avatar;
use App\Profile; use App\Profile;
use App\Services\AccountService;
use App\Services\MediaStorageService;
use Cache;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use App\Util\ActivityPub\Helpers;
use Illuminate\Support\Str;
use Zttp\Zttp;
use App\Http\Controllers\AvatarController;
use Cache;
use Storage;
use Log;
use Illuminate\Http\File;
use App\Services\AccountService;
use App\Services\MediaStorageService;
use App\Services\ActivityPubFetchService;
class RemoteAvatarFetchFromUrl implements ShouldQueue class RemoteAvatarFetchFromUrl implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $profile; protected $profile;
protected $url;
/** protected $url;
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/** /**
* The number of times the job may be attempted. * Delete the job if its models no longer exist.
* *
* @var int * @var bool
*/ */
public $tries = 1; public $deleteWhenMissingModels = true;
public $timeout = 300;
public $maxExceptions = 1;
/** /**
* Create a new job instance. * The number of times the job may be attempted.
* *
* @return void * @var int
*/ */
public function __construct(Profile $profile, $url) public $tries = 1;
{
$this->profile = $profile;
$this->url = $url;
}
/** public $timeout = 300;
* Execute the job.
*
* @return void
*/
public function handle()
{
$profile = $this->profile;
Cache::forget('avatar:' . $profile->id); public $maxExceptions = 1;
AccountService::del($profile->id);
if(boolval(config_cache('pixelfed.cloud_storage')) == false && boolval(config_cache('federation.avatars.store_local')) == false) { /**
return 1; * Create a new job instance.
} *
* @return void
*/
public function __construct(Profile $profile, $url)
{
$this->profile = $profile;
$this->url = $url;
}
if($profile->domain == null || $profile->private_key) { /**
return 1; * Execute the job.
} *
* @return void
*/
public function handle()
{
$profile = $this->profile;
$avatar = Avatar::whereProfileId($profile->id)->first(); Cache::forget('avatar:'.$profile->id);
AccountService::del($profile->id);
if(!$avatar) { if ((bool) config_cache('pixelfed.cloud_storage') == false && (bool) config_cache('federation.avatars.store_local') == false) {
$avatar = new Avatar; return 1;
$avatar->profile_id = $profile->id; }
$avatar->is_remote = true;
$avatar->remote_url = $this->url;
$avatar->save();
} else {
$avatar->remote_url = $this->url;
$avatar->is_remote = true;
$avatar->save();
}
MediaStorageService::avatar($avatar, boolval(config_cache('pixelfed.cloud_storage')) == false, true); if ($profile->domain == null || $profile->private_key) {
return 1;
}
return 1; $avatar = Avatar::whereProfileId($profile->id)->first();
}
if (! $avatar) {
$avatar = new Avatar;
$avatar->profile_id = $profile->id;
$avatar->is_remote = true;
$avatar->remote_url = $this->url;
$avatar->save();
} else {
$avatar->remote_url = $this->url;
$avatar->is_remote = true;
$avatar->save();
}
MediaStorageService::avatar($avatar, (bool) config_cache('pixelfed.cloud_storage') == false, true);
return 1;
}
} }

View file

@ -45,7 +45,7 @@ class ImageOptimize implements ShouldQueue
return; return;
} }
if(config('pixelfed.optimize_image') == false) { if((bool) config_cache('pixelfed.optimize_image') == false) {
ImageThumbnail::dispatch($media)->onQueue('mmo'); ImageThumbnail::dispatch($media)->onQueue('mmo');
return; return;
} else { } else {

View file

@ -51,7 +51,7 @@ class ImageResize implements ShouldQueue
return; return;
} }
if(!config('pixelfed.optimize_image')) { if((bool) config_cache('pixelfed.optimize_image') === false) {
ImageThumbnail::dispatch($media)->onQueue('mmo'); ImageThumbnail::dispatch($media)->onQueue('mmo');
return; return;
} }

View file

@ -61,7 +61,7 @@ class ImageUpdate implements ShouldQueue
return; return;
} }
if(config('pixelfed.optimize_image')) { if((bool) config_cache('pixelfed.optimize_image')) {
if (in_array($media->mime, $this->protectedMimes) == true) { if (in_array($media->mime, $this->protectedMimes) == true) {
ImageOptimizer::optimize($thumb); ImageOptimizer::optimize($thumb);
if(!$media->skip_optimize) { if(!$media->skip_optimize) {

View file

@ -3,27 +3,30 @@
namespace App\Jobs\MediaPipeline; namespace App\Jobs\MediaPipeline;
use App\Media; use App\Media;
use App\Services\Media\MediaHlsService;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Storage;
use App\Services\Media\MediaHlsService;
use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
class MediaDeletePipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing class MediaDeletePipeline implements ShouldBeUniqueUntilProcessing, ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $media; protected $media;
public $timeout = 300; public $timeout = 300;
public $tries = 3; public $tries = 3;
public $maxExceptions = 1; public $maxExceptions = 1;
public $failOnTimeout = true; public $failOnTimeout = true;
public $deleteWhenMissingModels = true; public $deleteWhenMissingModels = true;
/** /**
@ -38,7 +41,7 @@ class MediaDeletePipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing
*/ */
public function uniqueId(): string public function uniqueId(): string
{ {
return 'media:purge-job:id-' . $this->media->id; return 'media:purge-job:id-'.$this->media->id;
} }
/** /**
@ -51,58 +54,58 @@ class MediaDeletePipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing
return [(new WithoutOverlapping("media:purge-job:id-{$this->media->id}"))->shared()->dontRelease()]; return [(new WithoutOverlapping("media:purge-job:id-{$this->media->id}"))->shared()->dontRelease()];
} }
public function __construct(Media $media) public function __construct(Media $media)
{ {
$this->media = $media; $this->media = $media;
} }
public function handle() public function handle()
{ {
$media = $this->media; $media = $this->media;
$path = $media->media_path; $path = $media->media_path;
$thumb = $media->thumbnail_path; $thumb = $media->thumbnail_path;
if(!$path) { if (! $path) {
return 1; return 1;
} }
$e = explode('/', $path); $e = explode('/', $path);
array_pop($e); array_pop($e);
$i = implode('/', $e); $i = implode('/', $e);
if(config_cache('pixelfed.cloud_storage') == true) { if ((bool) config_cache('pixelfed.cloud_storage') == true) {
$disk = Storage::disk(config('filesystems.cloud')); $disk = Storage::disk(config('filesystems.cloud'));
if($path && $disk->exists($path)) { if ($path && $disk->exists($path)) {
$disk->delete($path); $disk->delete($path);
} }
if($thumb && $disk->exists($thumb)) { if ($thumb && $disk->exists($thumb)) {
$disk->delete($thumb); $disk->delete($thumb);
} }
} }
$disk = Storage::disk(config('filesystems.local')); $disk = Storage::disk(config('filesystems.local'));
if($path && $disk->exists($path)) { if ($path && $disk->exists($path)) {
$disk->delete($path); $disk->delete($path);
} }
if($thumb && $disk->exists($thumb)) { if ($thumb && $disk->exists($thumb)) {
$disk->delete($thumb); $disk->delete($thumb);
} }
if($media->hls_path != null) { if ($media->hls_path != null) {
$files = MediaHlsService::allFiles($media); $files = MediaHlsService::allFiles($media);
if($files && count($files)) { if ($files && count($files)) {
foreach($files as $file) { foreach ($files as $file) {
$disk->delete($file); $disk->delete($file);
} }
} }
} }
$media->delete(); $media->delete();
return 1; return 1;
} }
} }

View file

@ -8,68 +8,69 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
class MediaFixLocalFilesystemCleanupPipeline implements ShouldQueue class MediaFixLocalFilesystemCleanupPipeline implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 1800; public $timeout = 1800;
public $tries = 5;
public $maxExceptions = 1;
public function handle() public $tries = 5;
{
if(config_cache('pixelfed.cloud_storage') == false) {
// Only run if cloud storage is enabled
return;
}
$disk = Storage::disk('local'); public $maxExceptions = 1;
$cloud = Storage::disk(config('filesystems.cloud'));
Media::whereNotNull(['status_id', 'cdn_url', 'replicated_at']) public function handle()
->chunk(20, function ($medias) use($disk, $cloud) { {
foreach($medias as $media) { if ((bool) config_cache('pixelfed.cloud_storage') == false) {
if(!str_starts_with($media->media_path, 'public')) { // Only run if cloud storage is enabled
continue; return;
} }
if($disk->exists($media->media_path) && $cloud->exists($media->media_path)) { $disk = Storage::disk('local');
$disk->delete($media->media_path); $cloud = Storage::disk(config('filesystems.cloud'));
}
if($media->thumbnail_path) { Media::whereNotNull(['status_id', 'cdn_url', 'replicated_at'])
if($disk->exists($media->thumbnail_path)) { ->chunk(20, function ($medias) use ($disk, $cloud) {
$disk->delete($media->thumbnail_path); foreach ($medias as $media) {
} if (! str_starts_with($media->media_path, 'public')) {
} continue;
}
$paths = explode('/', $media->media_path); if ($disk->exists($media->media_path) && $cloud->exists($media->media_path)) {
if(count($paths) === 7) { $disk->delete($media->media_path);
array_pop($paths); }
$baseDir = implode('/', $paths);
if(count($disk->allFiles($baseDir)) === 0) { if ($media->thumbnail_path) {
$disk->deleteDirectory($baseDir); if ($disk->exists($media->thumbnail_path)) {
$disk->delete($media->thumbnail_path);
}
}
array_pop($paths); $paths = explode('/', $media->media_path);
$baseDir = implode('/', $paths); if (count($paths) === 7) {
array_pop($paths);
$baseDir = implode('/', $paths);
if(count($disk->allFiles($baseDir)) === 0) { if (count($disk->allFiles($baseDir)) === 0) {
$disk->deleteDirectory($baseDir); $disk->deleteDirectory($baseDir);
array_pop($paths); array_pop($paths);
$baseDir = implode('/', $paths); $baseDir = implode('/', $paths);
if(count($disk->allFiles($baseDir)) === 0) { if (count($disk->allFiles($baseDir)) === 0) {
$disk->deleteDirectory($baseDir); $disk->deleteDirectory($baseDir);
}
} array_pop($paths);
} $baseDir = implode('/', $paths);
}
} if (count($disk->allFiles($baseDir)) === 0) {
}); $disk->deleteDirectory($baseDir);
} }
}
}
}
}
});
}
} }

View file

@ -17,6 +17,7 @@ use Log;
use Storage; use Storage;
use Zttp\Zttp; use Zttp\Zttp;
use App\Util\ActivityPub\Helpers; use App\Util\ActivityPub\Helpers;
use App\Services\MediaPathService;
class RemoteFollowImportRecent implements ShouldQueue class RemoteFollowImportRecent implements ShouldQueue
{ {
@ -45,7 +46,6 @@ class RemoteFollowImportRecent implements ShouldQueue
'image/jpg', 'image/jpg',
'image/jpeg', 'image/jpeg',
'image/png', 'image/png',
'image/gif',
]; ];
} }
@ -208,9 +208,7 @@ class RemoteFollowImportRecent implements ShouldQueue
public function importMedia($url, $mime, $status) public function importMedia($url, $mime, $status)
{ {
$user = $this->profile; $user = $this->profile;
$monthHash = hash('sha1', date('Y').date('m')); $storagePath = MediaPathService::get($user, 2);
$userHash = hash('sha1', $user->id.(string) $user->created_at);
$storagePath = "public/m/{$monthHash}/{$userHash}";
try { try {
$info = pathinfo($url); $info = pathinfo($url);

View file

@ -2,9 +2,15 @@
namespace App\Jobs\SharePipeline; namespace App\Jobs\SharePipeline;
use Cache, Log; use App\Jobs\HomeFeedPipeline\FeedInsertPipeline;
use Illuminate\Support\Facades\Redis; use App\Notification;
use App\{Status, Notification}; use App\Services\ReblogService;
use App\Services\StatusService;
use App\Status;
use App\Transformer\ActivityPub\Verb\Announce;
use App\Util\ActivityPub\HttpSignature;
use GuzzleHttp\Client;
use GuzzleHttp\Pool;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
@ -12,141 +18,136 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use League\Fractal; use League\Fractal;
use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Serializer\ArraySerializer;
use App\Transformer\ActivityPub\Verb\Announce;
use GuzzleHttp\{Pool, Client, Promise};
use App\Util\ActivityPub\HttpSignature;
use App\Services\ReblogService;
use App\Services\StatusService;
use App\Jobs\HomeFeedPipeline\FeedInsertPipeline;
class SharePipeline implements ShouldQueue class SharePipeline implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status; protected $status;
/** /**
* Delete the job if its models no longer exist. * Delete the job if its models no longer exist.
* *
* @var bool * @var bool
*/ */
public $deleteWhenMissingModels = true; public $deleteWhenMissingModels = true;
/** /**
* Create a new job instance. * Create a new job instance.
* *
* @return void * @return void
*/ */
public function __construct(Status $status) public function __construct(Status $status)
{ {
$this->status = $status; $this->status = $status;
} }
/** /**
* Execute the job. * Execute the job.
* *
* @return void * @return void
*/ */
public function handle() public function handle()
{ {
$status = $this->status; $status = $this->status;
$parent = Status::find($this->status->reblog_of_id); $parent = Status::find($this->status->reblog_of_id);
if(!$parent) { if (! $parent) {
return; return;
} }
$actor = $status->profile; $actor = $status->profile;
$target = $parent->profile; $target = $parent->profile;
if ($status->uri !== null) { if ($status->uri !== null) {
// Ignore notifications to remote statuses // Ignore notifications to remote statuses
return; return;
} }
if($target->id === $status->profile_id) { if ($target->id === $status->profile_id) {
$this->remoteAnnounceDeliver(); $this->remoteAnnounceDeliver();
return true;
}
ReblogService::addPostReblog($parent->profile_id, $status->id); return true;
}
$parent->reblogs_count = $parent->reblogs_count + 1; ReblogService::addPostReblog($parent->profile_id, $status->id);
$parent->save();
StatusService::del($parent->id);
Notification::firstOrCreate( $parent->reblogs_count = $parent->reblogs_count + 1;
[ $parent->save();
'profile_id' => $target->id, StatusService::del($parent->id);
'actor_id' => $actor->id,
'action' => 'share',
'item_type' => 'App\Status',
'item_id' => $status->reblog_of_id ?? $status->id,
]
);
FeedInsertPipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); Notification::firstOrCreate(
[
'profile_id' => $target->id,
'actor_id' => $actor->id,
'action' => 'share',
'item_type' => 'App\Status',
'item_id' => $status->reblog_of_id ?? $status->id,
]
);
return $this->remoteAnnounceDeliver(); FeedInsertPipeline::dispatch($status->id, $status->profile_id)->onQueue('feed');
}
public function remoteAnnounceDeliver() return $this->remoteAnnounceDeliver();
{ }
if(config('app.env') !== 'production' || config_cache('federation.activitypub.enabled') == false) {
return true;
}
$status = $this->status;
$profile = $status->profile;
$fractal = new Fractal\Manager(); public function remoteAnnounceDeliver()
$fractal->setSerializer(new ArraySerializer()); {
$resource = new Fractal\Resource\Item($status, new Announce()); if (config('app.env') !== 'production' || (bool) config_cache('federation.activitypub.enabled') == false) {
$activity = $fractal->createData($resource)->toArray(); return true;
}
$status = $this->status;
$profile = $status->profile;
$audience = $status->profile->getAudienceInbox(); $fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($status, new Announce());
$activity = $fractal->createData($resource)->toArray();
if(empty($audience) || $status->scope != 'public') { $audience = $status->profile->getAudienceInbox();
// Return on profiles with no remote followers
return;
}
$payload = json_encode($activity); if (empty($audience) || $status->scope != 'public') {
// Return on profiles with no remote followers
return;
}
$client = new Client([ $payload = json_encode($activity);
'timeout' => config('federation.activitypub.delivery.timeout')
]);
$version = config('pixelfed.version'); $client = new Client([
$appUrl = config('app.url'); 'timeout' => config('federation.activitypub.delivery.timeout'),
$userAgent = "(Pixelfed/{$version}; +{$appUrl})"; ]);
$requests = function($audience) use ($client, $activity, $profile, $payload, $userAgent) { $version = config('pixelfed.version');
foreach($audience as $url) { $appUrl = config('app.url');
$headers = HttpSignature::sign($profile, $url, $activity, [ $userAgent = "(Pixelfed/{$version}; +{$appUrl})";
'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'User-Agent' => $userAgent,
]);
yield function() use ($client, $url, $headers, $payload) {
return $client->postAsync($url, [
'curl' => [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HEADER => true
]
]);
};
}
};
$pool = new Pool($client, $requests($audience), [ $requests = function ($audience) use ($client, $activity, $profile, $payload, $userAgent) {
'concurrency' => config('federation.activitypub.delivery.concurrency'), foreach ($audience as $url) {
'fulfilled' => function ($response, $index) { $headers = HttpSignature::sign($profile, $url, $activity, [
}, 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'rejected' => function ($reason, $index) { 'User-Agent' => $userAgent,
} ]);
]); yield function () use ($client, $url, $headers, $payload) {
return $client->postAsync($url, [
'curl' => [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HEADER => true,
],
]);
};
}
};
$promise = $pool->promise(); $pool = new Pool($client, $requests($audience), [
'concurrency' => config('federation.activitypub.delivery.concurrency'),
'fulfilled' => function ($response, $index) {
},
'rejected' => function ($reason, $index) {
},
]);
$promise->wait(); $promise = $pool->promise();
} $promise->wait();
}
} }

View file

@ -2,9 +2,15 @@
namespace App\Jobs\SharePipeline; namespace App\Jobs\SharePipeline;
use Cache, Log; use App\Jobs\HomeFeedPipeline\FeedRemovePipeline;
use Illuminate\Support\Facades\Redis; use App\Notification;
use App\{Status, Notification}; use App\Services\ReblogService;
use App\Services\StatusService;
use App\Status;
use App\Transformer\ActivityPub\Verb\UndoAnnounce;
use App\Util\ActivityPub\HttpSignature;
use GuzzleHttp\Client;
use GuzzleHttp\Pool;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
@ -12,128 +18,125 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use League\Fractal; use League\Fractal;
use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Serializer\ArraySerializer;
use App\Transformer\ActivityPub\Verb\UndoAnnounce;
use GuzzleHttp\{Pool, Client, Promise};
use App\Util\ActivityPub\HttpSignature;
use App\Services\ReblogService;
use App\Services\StatusService;
use App\Jobs\HomeFeedPipeline\FeedRemovePipeline;
class UndoSharePipeline implements ShouldQueue class UndoSharePipeline implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status;
public $deleteWhenMissingModels = true;
public function __construct(Status $status) protected $status;
{
$this->status = $status;
}
public function handle() public $deleteWhenMissingModels = true;
{
$status = $this->status;
$actor = $status->profile;
$parent = Status::find($status->reblog_of_id);
FeedRemovePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); public function __construct(Status $status)
{
$this->status = $status;
}
if($parent) { public function handle()
$target = $parent->profile_id; {
ReblogService::removePostReblog($parent->profile_id, $status->id); $status = $this->status;
$actor = $status->profile;
$parent = Status::find($status->reblog_of_id);
if($parent->reblogs_count > 0) { FeedRemovePipeline::dispatch($status->id, $status->profile_id)->onQueue('feed');
$parent->reblogs_count = $parent->reblogs_count - 1;
$parent->save();
StatusService::del($parent->id);
}
$notification = Notification::whereProfileId($target) if ($parent) {
->whereActorId($status->profile_id) $target = $parent->profile_id;
->whereAction('share') ReblogService::removePostReblog($parent->profile_id, $status->id);
->whereItemId($status->reblog_of_id)
->whereItemType('App\Status')
->first();
if($notification) { if ($parent->reblogs_count > 0) {
$notification->forceDelete(); $parent->reblogs_count = $parent->reblogs_count - 1;
} $parent->save();
} StatusService::del($parent->id);
}
if ($status->uri != null) { $notification = Notification::whereProfileId($target)
return; ->whereActorId($status->profile_id)
} ->whereAction('share')
->whereItemId($status->reblog_of_id)
->whereItemType('App\Status')
->first();
if(config('app.env') !== 'production' || config_cache('federation.activitypub.enabled') == false) { if ($notification) {
return $status->delete(); $notification->forceDelete();
} else { }
return $this->remoteAnnounceDeliver(); }
}
}
public function remoteAnnounceDeliver() if ($status->uri != null) {
{ return;
if(config('app.env') !== 'production' || config_cache('federation.activitypub.enabled') == false) { }
if (config('app.env') !== 'production' || (bool) config_cache('federation.activitypub.enabled') == false) {
return $status->delete();
} else {
return $this->remoteAnnounceDeliver();
}
}
public function remoteAnnounceDeliver()
{
if (config('app.env') !== 'production' || (bool) config_cache('federation.activitypub.enabled') == false) {
$status->delete(); $status->delete();
return 1;
}
$status = $this->status; return 1;
$profile = $status->profile; }
$fractal = new Fractal\Manager(); $status = $this->status;
$fractal->setSerializer(new ArraySerializer()); $profile = $status->profile;
$resource = new Fractal\Resource\Item($status, new UndoAnnounce());
$activity = $fractal->createData($resource)->toArray();
$audience = $status->profile->getAudienceInbox(); $fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($status, new UndoAnnounce());
$activity = $fractal->createData($resource)->toArray();
if(empty($audience) || $status->scope != 'public') { $audience = $status->profile->getAudienceInbox();
return 1;
}
$payload = json_encode($activity); if (empty($audience) || $status->scope != 'public') {
return 1;
}
$client = new Client([ $payload = json_encode($activity);
'timeout' => config('federation.activitypub.delivery.timeout')
]);
$version = config('pixelfed.version'); $client = new Client([
$appUrl = config('app.url'); 'timeout' => config('federation.activitypub.delivery.timeout'),
$userAgent = "(Pixelfed/{$version}; +{$appUrl})"; ]);
$requests = function($audience) use ($client, $activity, $profile, $payload, $userAgent) { $version = config('pixelfed.version');
foreach($audience as $url) { $appUrl = config('app.url');
$headers = HttpSignature::sign($profile, $url, $activity, [ $userAgent = "(Pixelfed/{$version}; +{$appUrl})";
'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'User-Agent' => $userAgent,
]);
yield function() use ($client, $url, $headers, $payload) {
return $client->postAsync($url, [
'curl' => [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HEADER => true
]
]);
};
}
};
$pool = new Pool($client, $requests($audience), [ $requests = function ($audience) use ($client, $activity, $profile, $payload, $userAgent) {
'concurrency' => config('federation.activitypub.delivery.concurrency'), foreach ($audience as $url) {
'fulfilled' => function ($response, $index) { $headers = HttpSignature::sign($profile, $url, $activity, [
}, 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'rejected' => function ($reason, $index) { 'User-Agent' => $userAgent,
} ]);
]); yield function () use ($client, $url, $headers, $payload) {
return $client->postAsync($url, [
'curl' => [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HEADER => true,
],
]);
};
}
};
$promise = $pool->promise(); $pool = new Pool($client, $requests($audience), [
'concurrency' => config('federation.activitypub.delivery.concurrency'),
'fulfilled' => function ($response, $index) {
},
'rejected' => function ($reason, $index) {
},
]);
$promise->wait(); $promise = $pool->promise();
$status->delete(); $promise->wait();
return 1; $status->delete();
}
return 1;
}
} }

View file

@ -2,126 +2,122 @@
namespace App\Jobs\StatusPipeline; namespace App\Jobs\StatusPipeline;
use DB, Cache, Storage; use App\AccountInterstitial;
use App\{ use App\Bookmark;
AccountInterstitial, use App\CollectionItem;
Bookmark, use App\DirectMessage;
CollectionItem, use App\Jobs\MediaPipeline\MediaDeletePipeline;
DirectMessage, use App\Like;
Like, use App\Media;
Media, use App\MediaTag;
MediaTag, use App\Mention;
Mention, use App\Notification;
Notification, use App\Report;
Report, use App\Services\CollectionService;
Status, use App\Services\NotificationService;
StatusArchived, use App\Services\StatusService;
StatusHashtag, use App\Status;
StatusView use App\StatusArchived;
}; use App\StatusHashtag;
use App\StatusView;
use App\Transformer\ActivityPub\Verb\DeleteNote;
use App\Util\ActivityPub\HttpSignature;
use Cache;
use GuzzleHttp\Client;
use GuzzleHttp\Pool;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use League\Fractal; use League\Fractal;
use Illuminate\Support\Str;
use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Serializer\ArraySerializer;
use App\Transformer\ActivityPub\Verb\DeleteNote;
use App\Util\ActivityPub\Helpers;
use GuzzleHttp\Pool;
use GuzzleHttp\Client;
use GuzzleHttp\Promise;
use App\Util\ActivityPub\HttpSignature;
use App\Services\CollectionService;
use App\Services\StatusService;
use App\Services\NotificationService;
use App\Jobs\MediaPipeline\MediaDeletePipeline;
class StatusDelete implements ShouldQueue class StatusDelete implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status; protected $status;
/** /**
* Delete the job if its models no longer exist. * Delete the job if its models no longer exist.
* *
* @var bool * @var bool
*/ */
public $deleteWhenMissingModels = true; public $deleteWhenMissingModels = true;
public $timeout = 900; public $timeout = 900;
public $tries = 2; public $tries = 2;
/** /**
* Create a new job instance. * Create a new job instance.
* *
* @return void * @return void
*/ */
public function __construct(Status $status) public function __construct(Status $status)
{ {
$this->status = $status; $this->status = $status;
} }
/** /**
* Execute the job. * Execute the job.
* *
* @return void * @return void
*/ */
public function handle() public function handle()
{ {
$status = $this->status; $status = $this->status;
$profile = $this->status->profile; $profile = $this->status->profile;
StatusService::del($status->id, true); StatusService::del($status->id, true);
if($profile) { if ($profile) {
if(in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) { if (in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) {
$profile->status_count = $profile->status_count - 1; $profile->status_count = $profile->status_count - 1;
$profile->save(); $profile->save();
} }
} }
Cache::forget('pf:atom:user-feed:by-id:' . $status->profile_id); Cache::forget('pf:atom:user-feed:by-id:'.$status->profile_id);
if(config_cache('federation.activitypub.enabled') == true) { if ((bool) config_cache('federation.activitypub.enabled') == true) {
return $this->fanoutDelete($status); return $this->fanoutDelete($status);
} else { } else {
return $this->unlinkRemoveMedia($status); return $this->unlinkRemoveMedia($status);
} }
} }
public function unlinkRemoveMedia($status) public function unlinkRemoveMedia($status)
{ {
Media::whereStatusId($status->id) Media::whereStatusId($status->id)
->get() ->get()
->each(function($media) { ->each(function ($media) {
MediaDeletePipeline::dispatch($media); MediaDeletePipeline::dispatch($media);
}); });
if($status->in_reply_to_id) { if ($status->in_reply_to_id) {
$parent = Status::findOrFail($status->in_reply_to_id); $parent = Status::findOrFail($status->in_reply_to_id);
--$parent->reply_count; $parent->reply_count--;
$parent->save(); $parent->save();
StatusService::del($parent->id); StatusService::del($parent->id);
} }
Bookmark::whereStatusId($status->id)->delete(); Bookmark::whereStatusId($status->id)->delete();
CollectionItem::whereObjectType('App\Status') CollectionItem::whereObjectType('App\Status')
->whereObjectId($status->id) ->whereObjectId($status->id)
->get() ->get()
->each(function($col) { ->each(function ($col) {
CollectionService::removeItem($col->collection_id, $col->object_id); CollectionService::removeItem($col->collection_id, $col->object_id);
$col->delete(); $col->delete();
}); });
$dms = DirectMessage::whereStatusId($status->id)->get(); $dms = DirectMessage::whereStatusId($status->id)->get();
foreach($dms as $dm) { foreach ($dms as $dm) {
$not = Notification::whereItemType('App\DirectMessage') $not = Notification::whereItemType('App\DirectMessage')
->whereItemId($dm->id) ->whereItemId($dm->id)
->first(); ->first();
if($not) { if ($not) {
NotificationService::del($not->profile_id, $not->id); NotificationService::del($not->profile_id, $not->id);
$not->forceDeleteQuietly(); $not->forceDeleteQuietly();
} }
@ -130,11 +126,11 @@ class StatusDelete implements ShouldQueue
Like::whereStatusId($status->id)->delete(); Like::whereStatusId($status->id)->delete();
$mediaTags = MediaTag::where('status_id', $status->id)->get(); $mediaTags = MediaTag::where('status_id', $status->id)->get();
foreach($mediaTags as $mtag) { foreach ($mediaTags as $mtag) {
$not = Notification::whereItemType('App\MediaTag') $not = Notification::whereItemType('App\MediaTag')
->whereItemId($mtag->id) ->whereItemId($mtag->id)
->first(); ->first();
if($not) { if ($not) {
NotificationService::del($not->profile_id, $not->id); NotificationService::del($not->profile_id, $not->id);
$not->forceDeleteQuietly(); $not->forceDeleteQuietly();
} }
@ -142,85 +138,85 @@ class StatusDelete implements ShouldQueue
} }
Mention::whereStatusId($status->id)->forceDelete(); Mention::whereStatusId($status->id)->forceDelete();
Notification::whereItemType('App\Status') Notification::whereItemType('App\Status')
->whereItemId($status->id) ->whereItemId($status->id)
->forceDelete(); ->forceDelete();
Report::whereObjectType('App\Status') Report::whereObjectType('App\Status')
->whereObjectId($status->id) ->whereObjectId($status->id)
->delete(); ->delete();
StatusArchived::whereStatusId($status->id)->delete(); StatusArchived::whereStatusId($status->id)->delete();
StatusHashtag::whereStatusId($status->id)->delete(); StatusHashtag::whereStatusId($status->id)->delete();
StatusView::whereStatusId($status->id)->delete(); StatusView::whereStatusId($status->id)->delete();
Status::whereInReplyToId($status->id)->update(['in_reply_to_id' => null]); Status::whereInReplyToId($status->id)->update(['in_reply_to_id' => null]);
AccountInterstitial::where('item_type', 'App\Status') AccountInterstitial::where('item_type', 'App\Status')
->where('item_id', $status->id) ->where('item_id', $status->id)
->delete(); ->delete();
$status->delete(); $status->delete();
return 1;
}
public function fanoutDelete($status)
{
$profile = $status->profile;
if(!$profile) {
return;
}
$audience = $status->profile->getAudienceInbox();
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($status, new DeleteNote());
$activity = $fractal->createData($resource)->toArray();
$this->unlinkRemoveMedia($status);
$payload = json_encode($activity);
$client = new Client([
'timeout' => config('federation.activitypub.delivery.timeout')
]);
$version = config('pixelfed.version');
$appUrl = config('app.url');
$userAgent = "(Pixelfed/{$version}; +{$appUrl})";
$requests = function($audience) use ($client, $activity, $profile, $payload, $userAgent) {
foreach($audience as $url) {
$headers = HttpSignature::sign($profile, $url, $activity, [
'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'User-Agent' => $userAgent,
]);
yield function() use ($client, $url, $headers, $payload) {
return $client->postAsync($url, [
'curl' => [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HEADER => true
]
]);
};
}
};
$pool = new Pool($client, $requests($audience), [
'concurrency' => config('federation.activitypub.delivery.concurrency'),
'fulfilled' => function ($response, $index) {
},
'rejected' => function ($reason, $index) {
}
]);
$promise = $pool->promise();
$promise->wait();
return 1; return 1;
} }
public function fanoutDelete($status)
{
$profile = $status->profile;
if (! $profile) {
return;
}
$audience = $status->profile->getAudienceInbox();
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($status, new DeleteNote());
$activity = $fractal->createData($resource)->toArray();
$this->unlinkRemoveMedia($status);
$payload = json_encode($activity);
$client = new Client([
'timeout' => config('federation.activitypub.delivery.timeout'),
]);
$version = config('pixelfed.version');
$appUrl = config('app.url');
$userAgent = "(Pixelfed/{$version}; +{$appUrl})";
$requests = function ($audience) use ($client, $activity, $profile, $payload, $userAgent) {
foreach ($audience as $url) {
$headers = HttpSignature::sign($profile, $url, $activity, [
'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'User-Agent' => $userAgent,
]);
yield function () use ($client, $url, $headers, $payload) {
return $client->postAsync($url, [
'curl' => [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HEADER => true,
],
]);
};
}
};
$pool = new Pool($client, $requests($audience), [
'concurrency' => config('federation.activitypub.delivery.concurrency'),
'fulfilled' => function ($response, $index) {
},
'rejected' => function ($reason, $index) {
},
]);
$promise = $pool->promise();
$promise->wait();
return 1;
}
} }

View file

@ -3,12 +3,16 @@
namespace App\Jobs\StatusPipeline; namespace App\Jobs\StatusPipeline;
use App\Hashtag; use App\Hashtag;
use App\Jobs\HomeFeedPipeline\FeedInsertPipeline;
use App\Jobs\MentionPipeline\MentionPipeline; use App\Jobs\MentionPipeline\MentionPipeline;
use App\Mention; use App\Mention;
use App\Profile; use App\Profile;
use App\Services\AdminShadowFilterService;
use App\Services\PublicTimelineService;
use App\Services\StatusService;
use App\Services\UserFilterService;
use App\Status; use App\Status;
use App\StatusHashtag; use App\StatusHashtag;
use App\Services\PublicTimelineService;
use App\Util\Lexer\Autolink; use App\Util\Lexer\Autolink;
use App\Util\Lexer\Extractor; use App\Util\Lexer\Extractor;
use App\Util\Sentiment\Bouncer; use App\Util\Sentiment\Bouncer;
@ -19,18 +23,15 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use App\Services\StatusService;
use App\Services\UserFilterService;
use App\Services\AdminShadowFilterService;
use App\Jobs\HomeFeedPipeline\FeedInsertPipeline;
use App\Jobs\HomeFeedPipeline\HashtagInsertFanoutPipeline;
class StatusEntityLexer implements ShouldQueue class StatusEntityLexer implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status; protected $status;
protected $entities; protected $entities;
protected $autolink; protected $autolink;
/** /**
@ -60,12 +61,12 @@ class StatusEntityLexer implements ShouldQueue
$profile = $this->status->profile; $profile = $this->status->profile;
$status = $this->status; $status = $this->status;
if(in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) { if (in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])) {
$profile->status_count = $profile->status_count + 1; $profile->status_count = $profile->status_count + 1;
$profile->save(); $profile->save();
} }
if($profile->no_autolink == false) { if ($profile->no_autolink == false) {
$this->parseEntities(); $this->parseEntities();
} }
} }
@ -103,16 +104,16 @@ class StatusEntityLexer implements ShouldQueue
$status = $this->status; $status = $this->status;
foreach ($tags as $tag) { foreach ($tags as $tag) {
if(mb_strlen($tag) > 124) { if (mb_strlen($tag) > 124) {
continue; continue;
} }
DB::transaction(function () use ($status, $tag) { DB::transaction(function () use ($status, $tag) {
$slug = str_slug($tag, '-', false); $slug = str_slug($tag, '-', false);
$hashtag = Hashtag::firstOrCreate([ $hashtag = Hashtag::firstOrCreate([
'slug' => $slug 'slug' => $slug,
], [ ], [
'name' => $tag 'name' => $tag,
]); ]);
StatusHashtag::firstOrCreate( StatusHashtag::firstOrCreate(
@ -136,11 +137,11 @@ class StatusEntityLexer implements ShouldQueue
foreach ($mentions as $mention) { foreach ($mentions as $mention) {
$mentioned = Profile::whereUsername($mention)->first(); $mentioned = Profile::whereUsername($mention)->first();
if (empty($mentioned) || !isset($mentioned->id)) { if (empty($mentioned) || ! isset($mentioned->id)) {
continue; continue;
} }
$blocks = UserFilterService::blocks($mentioned->id); $blocks = UserFilterService::blocks($mentioned->id);
if($blocks && in_array($status->profile_id, $blocks)) { if ($blocks && in_array($status->profile_id, $blocks)) {
continue; continue;
} }
@ -161,8 +162,8 @@ class StatusEntityLexer implements ShouldQueue
$status = $this->status; $status = $this->status;
StatusService::refresh($status->id); StatusService::refresh($status->id);
if(config('exp.cached_home_timeline')) { if (config('exp.cached_home_timeline')) {
if( $status->in_reply_to_id === null && if ($status->in_reply_to_id === null &&
in_array($status->scope, ['public', 'unlisted', 'private']) in_array($status->scope, ['public', 'unlisted', 'private'])
) { ) {
FeedInsertPipeline::dispatch($status->id, $status->profile_id)->onQueue('feed'); FeedInsertPipeline::dispatch($status->id, $status->profile_id)->onQueue('feed');
@ -179,28 +180,28 @@ class StatusEntityLexer implements ShouldQueue
'photo:album', 'photo:album',
'video', 'video',
'video:album', 'video:album',
'photo:video:album' 'photo:video:album',
]; ];
if(config_cache('pixelfed.bouncer.enabled')) { if ((bool) config_cache('pixelfed.bouncer.enabled')) {
Bouncer::get($status); Bouncer::get($status);
} }
Cache::forget('pf:atom:user-feed:by-id:' . $status->profile_id); Cache::forget('pf:atom:user-feed:by-id:'.$status->profile_id);
$hideNsfw = config('instance.hide_nsfw_on_public_feeds'); $hideNsfw = config('instance.hide_nsfw_on_public_feeds');
if( $status->uri == null && if ($status->uri == null &&
$status->scope == 'public' && $status->scope == 'public' &&
in_array($status->type, $types) && in_array($status->type, $types) &&
$status->in_reply_to_id === null && $status->in_reply_to_id === null &&
$status->reblog_of_id === null && $status->reblog_of_id === null &&
($hideNsfw ? $status->is_nsfw == false : true) ($hideNsfw ? $status->is_nsfw == false : true)
) { ) {
if(AdminShadowFilterService::canAddToPublicFeedByProfileId($status->profile_id)) { if (AdminShadowFilterService::canAddToPublicFeedByProfileId($status->profile_id)) {
PublicTimelineService::add($status->id); PublicTimelineService::add($status->id);
} }
} }
if(config_cache('federation.activitypub.enabled') == true && config('app.env') == 'production') { if ((bool) config_cache('federation.activitypub.enabled') == true && config('app.env') == 'production') {
StatusActivityPubDeliver::dispatch($status); StatusActivityPubDeliver::dispatch($status);
} }
} }

View file

@ -2,172 +2,171 @@
namespace App\Jobs\StatusPipeline; namespace App\Jobs\StatusPipeline;
use App\Media;
use App\Models\StatusEdit;
use App\ModLog;
use App\Profile;
use App\Services\StatusService;
use App\Status;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use App\Media;
use App\ModLog;
use App\Profile;
use App\Status;
use App\Models\StatusEdit;
use App\Services\StatusService;
use Purify;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Purify;
class StatusRemoteUpdatePipeline implements ShouldQueue class StatusRemoteUpdatePipeline implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $activity; public $activity;
/** /**
* Create a new job instance. * Create a new job instance.
*/ */
public function __construct($activity) public function __construct($activity)
{ {
$this->activity = $activity; $this->activity = $activity;
} }
/** /**
* Execute the job. * Execute the job.
*/ */
public function handle(): void public function handle(): void
{ {
$activity = $this->activity; $activity = $this->activity;
$status = Status::with('media')->whereObjectUrl($activity['id'])->first(); $status = Status::with('media')->whereObjectUrl($activity['id'])->first();
if(!$status) { if (! $status) {
return; return;
} }
$this->createPreviousEdit($status); $this->createPreviousEdit($status);
$this->updateMedia($status, $activity); $this->updateMedia($status, $activity);
$this->updateImmediateAttributes($status, $activity); $this->updateImmediateAttributes($status, $activity);
$this->createEdit($status, $activity); $this->createEdit($status, $activity);
} }
protected function createPreviousEdit($status) protected function createPreviousEdit($status)
{ {
if(!$status->edits()->count()) { if (! $status->edits()->count()) {
StatusEdit::create([ StatusEdit::create([
'status_id' => $status->id, 'status_id' => $status->id,
'profile_id' => $status->profile_id, 'profile_id' => $status->profile_id,
'caption' => $status->caption, 'caption' => $status->caption,
'spoiler_text' => $status->cw_summary, 'spoiler_text' => $status->cw_summary,
'is_nsfw' => $status->is_nsfw, 'is_nsfw' => $status->is_nsfw,
'ordered_media_attachment_ids' => $status->media()->orderBy('order')->pluck('id')->toArray(), 'ordered_media_attachment_ids' => $status->media()->orderBy('order')->pluck('id')->toArray(),
'created_at' => $status->created_at 'created_at' => $status->created_at,
]); ]);
} }
} }
protected function updateMedia($status, $activity) protected function updateMedia($status, $activity)
{ {
if(!isset($activity['attachment'])) { if (! isset($activity['attachment'])) {
return; return;
} }
$ogm = $status->media->count() ? $status->media()->orderBy('order')->get() : collect([]); $ogm = $status->media->count() ? $status->media()->orderBy('order')->get() : collect([]);
$nm = collect($activity['attachment'])->filter(function($nm) { $nm = collect($activity['attachment'])->filter(function ($nm) {
return isset( return isset(
$nm['type'], $nm['type'],
$nm['mediaType'], $nm['mediaType'],
$nm['url'] $nm['url']
) && ) &&
in_array($nm['type'], ['Document', 'Image', 'Video']) && in_array($nm['type'], ['Document', 'Image', 'Video']) &&
in_array($nm['mediaType'], explode(',', config('pixelfed.media_types'))); in_array($nm['mediaType'], explode(',', config_cache('pixelfed.media_types')));
}); });
// Skip when no media // Skip when no media
if(!$ogm->count() && !$nm->count()) { if (! $ogm->count() && ! $nm->count()) {
return; return;
} }
Media::whereProfileId($status->profile_id) Media::whereProfileId($status->profile_id)
->whereStatusId($status->id) ->whereStatusId($status->id)
->update([ ->update([
'status_id' => null 'status_id' => null,
]); ]);
$nm->each(function($n, $key) use($status) { $nm->each(function ($n, $key) use ($status) {
$res = Http::withOptions(['allow_redirects' => false])->retry(3, 100, throw: false)->head($n['url']); $res = Http::withOptions(['allow_redirects' => false])->retry(3, 100, throw: false)->head($n['url']);
if(!$res->successful()) { if (! $res->successful()) {
return; return;
} }
if(!in_array($res->header('content-type'), explode(',',config('pixelfed.media_types')))) { if (! in_array($res->header('content-type'), explode(',', config_cache('pixelfed.media_types')))) {
return; return;
} }
$m = new Media; $m = new Media;
$m->status_id = $status->id; $m->status_id = $status->id;
$m->profile_id = $status->profile_id; $m->profile_id = $status->profile_id;
$m->remote_media = true; $m->remote_media = true;
$m->media_path = $n['url']; $m->media_path = $n['url'];
$m->mime = $res->header('content-type'); $m->mime = $res->header('content-type');
$m->size = $res->hasHeader('content-length') ? $res->header('content-length') : null; $m->size = $res->hasHeader('content-length') ? $res->header('content-length') : null;
$m->caption = isset($n['name']) && !empty($n['name']) ? Purify::clean($n['name']) : null; $m->caption = isset($n['name']) && ! empty($n['name']) ? Purify::clean($n['name']) : null;
$m->remote_url = $n['url']; $m->remote_url = $n['url'];
$m->blurhash = isset($n['blurhash']) && (strlen($n['blurhash']) < 50) ? $n['blurhash'] : null; $m->blurhash = isset($n['blurhash']) && (strlen($n['blurhash']) < 50) ? $n['blurhash'] : null;
$m->width = isset($n['width']) && !empty($n['width']) ? $n['width'] : null; $m->width = isset($n['width']) && ! empty($n['width']) ? $n['width'] : null;
$m->height = isset($n['height']) && !empty($n['height']) ? $n['height'] : null; $m->height = isset($n['height']) && ! empty($n['height']) ? $n['height'] : null;
$m->skip_optimize = true; $m->skip_optimize = true;
$m->order = $key + 1; $m->order = $key + 1;
$m->save(); $m->save();
}); });
} }
protected function updateImmediateAttributes($status, $activity) protected function updateImmediateAttributes($status, $activity)
{ {
if(isset($activity['content'])) { if (isset($activity['content'])) {
$status->caption = strip_tags($activity['content']); $status->caption = strip_tags($activity['content']);
$status->rendered = Purify::clean($activity['content']); $status->rendered = Purify::clean($activity['content']);
} }
if(isset($activity['sensitive'])) { if (isset($activity['sensitive'])) {
if((bool) $activity['sensitive'] == false) { if ((bool) $activity['sensitive'] == false) {
$status->is_nsfw = false; $status->is_nsfw = false;
$exists = ModLog::whereObjectType('App\Status::class') $exists = ModLog::whereObjectType('App\Status::class')
->whereObjectId($status->id) ->whereObjectId($status->id)
->whereAction('admin.status.moderate') ->whereAction('admin.status.moderate')
->exists(); ->exists();
if($exists == true) { if ($exists == true) {
$status->is_nsfw = true; $status->is_nsfw = true;
} }
$profile = Profile::find($status->profile_id); $profile = Profile::find($status->profile_id);
if(!$profile || $profile->cw == true) { if (! $profile || $profile->cw == true) {
$status->is_nsfw = true; $status->is_nsfw = true;
} }
} else { } else {
$status->is_nsfw = true; $status->is_nsfw = true;
} }
} }
if(isset($activity['summary'])) { if (isset($activity['summary'])) {
$status->cw_summary = Purify::clean($activity['summary']); $status->cw_summary = Purify::clean($activity['summary']);
} else { } else {
$status->cw_summary = null; $status->cw_summary = null;
} }
$status->edited_at = now(); $status->edited_at = now();
$status->save(); $status->save();
StatusService::del($status->id); StatusService::del($status->id);
} }
protected function createEdit($status, $activity) protected function createEdit($status, $activity)
{ {
$cleaned = isset($activity['content']) ? Purify::clean($activity['content']) : null; $cleaned = isset($activity['content']) ? Purify::clean($activity['content']) : null;
$spoiler_text = isset($activity['summary']) ? Purify::clean($activity['summary']) : null; $spoiler_text = isset($activity['summary']) ? Purify::clean($activity['summary']) : null;
$sensitive = isset($activity['sensitive']) ? $activity['sensitive'] : null; $sensitive = isset($activity['sensitive']) ? $activity['sensitive'] : null;
$mids = $status->media()->count() ? $status->media()->orderBy('order')->pluck('id')->toArray() : null; $mids = $status->media()->count() ? $status->media()->orderBy('order')->pluck('id')->toArray() : null;
StatusEdit::create([ StatusEdit::create([
'status_id' => $status->id, 'status_id' => $status->id,
'profile_id' => $status->profile_id, 'profile_id' => $status->profile_id,
'caption' => $cleaned, 'caption' => $cleaned,
'spoiler_text' => $spoiler_text, 'spoiler_text' => $spoiler_text,
'is_nsfw' => $sensitive, 'is_nsfw' => $sensitive,
'ordered_media_attachment_ids' => $mids 'ordered_media_attachment_ids' => $mids,
]); ]);
} }
} }

View file

@ -18,7 +18,7 @@ class CustomEmoji extends Model
public static function scan($text, $activitypub = false) public static function scan($text, $activitypub = false)
{ {
if(config('federation.custom_emoji.enabled') == false) { if((bool) config_cache('federation.custom_emoji.enabled') == false) {
return []; return [];
} }

View file

@ -3,9 +3,9 @@
namespace App\Observers; namespace App\Observers;
use App\Avatar; use App\Avatar;
use App\Services\AccountService;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use App\Services\AccountService;
class AvatarObserver class AvatarObserver
{ {
@ -19,7 +19,6 @@ class AvatarObserver
/** /**
* Handle the avatar "created" event. * Handle the avatar "created" event.
* *
* @param \App\Avatar $avatar
* @return void * @return void
*/ */
public function created(Avatar $avatar) public function created(Avatar $avatar)
@ -30,7 +29,6 @@ class AvatarObserver
/** /**
* Handle the avatar "updated" event. * Handle the avatar "updated" event.
* *
* @param \App\Avatar $avatar
* @return void * @return void
*/ */
public function updated(Avatar $avatar) public function updated(Avatar $avatar)
@ -41,7 +39,6 @@ class AvatarObserver
/** /**
* Handle the avatar "deleted" event. * Handle the avatar "deleted" event.
* *
* @param \App\Avatar $avatar
* @return void * @return void
*/ */
public function deleted(Avatar $avatar) public function deleted(Avatar $avatar)
@ -52,23 +49,22 @@ class AvatarObserver
/** /**
* Handle the avatar "deleting" event. * Handle the avatar "deleting" event.
* *
* @param \App\Avatar $avatar
* @return void * @return void
*/ */
public function deleting(Avatar $avatar) public function deleting(Avatar $avatar)
{ {
$path = storage_path('app/'.$avatar->media_path); $path = storage_path('app/'.$avatar->media_path);
if( is_file($path) && if (is_file($path) &&
$avatar->media_path != 'public/avatars/default.png' && $avatar->media_path != 'public/avatars/default.png' &&
$avatar->media_path != 'public/avatars/default.jpg' $avatar->media_path != 'public/avatars/default.jpg'
) { ) {
@unlink($path); @unlink($path);
} }
if(config_cache('pixelfed.cloud_storage')) { if ((bool) config_cache('pixelfed.cloud_storage')) {
$disk = Storage::disk(config('filesystems.cloud')); $disk = Storage::disk(config('filesystems.cloud'));
$base = Str::startsWith($avatar->media_path, 'cache/avatars/'); $base = Str::startsWith($avatar->media_path, 'cache/avatars/');
if($base && $disk->exists($avatar->media_path)) { if ($base && $disk->exists($avatar->media_path)) {
$disk->delete($avatar->media_path); $disk->delete($avatar->media_path);
} }
} }
@ -78,7 +74,6 @@ class AvatarObserver
/** /**
* Handle the avatar "restored" event. * Handle the avatar "restored" event.
* *
* @param \App\Avatar $avatar
* @return void * @return void
*/ */
public function restored(Avatar $avatar) public function restored(Avatar $avatar)
@ -89,7 +84,6 @@ class AvatarObserver
/** /**
* Handle the avatar "force deleted" event. * Handle the avatar "force deleted" event.
* *
* @param \App\Avatar $avatar
* @return void * @return void
*/ */
public function forceDeleted(Avatar $avatar) public function forceDeleted(Avatar $avatar)

View file

@ -107,7 +107,7 @@ class UserObserver
CreateAvatar::dispatch($profile); CreateAvatar::dispatch($profile);
}); });
if(config_cache('account.autofollow') == true) { if((bool) config_cache('account.autofollow') == true) {
$names = config_cache('account.autofollow_usernames'); $names = config_cache('account.autofollow_usernames');
$names = explode(',', $names); $names = explode(',', $names);

View file

@ -2,9 +2,9 @@
namespace App\Providers; namespace App\Providers;
use Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Laravel\Passport\Passport; use Laravel\Passport\Passport;
use Gate;
class AuthServiceProvider extends ServiceProvider class AuthServiceProvider extends ServiceProvider
{ {
@ -24,11 +24,11 @@ class AuthServiceProvider extends ServiceProvider
*/ */
public function boot() public function boot()
{ {
if(config('app.env') === 'production' && config('pixelfed.oauth_enabled') == true) { if (config('app.env') === 'production' && (bool) config_cache('pixelfed.oauth_enabled') == true) {
Passport::tokensExpireIn(now()->addDays(config('instance.oauth.token_expiration', 356))); Passport::tokensExpireIn(now()->addDays(config('instance.oauth.token_expiration', 356)));
Passport::refreshTokensExpireIn(now()->addDays(config('instance.oauth.refresh_expiration', 400))); Passport::refreshTokensExpireIn(now()->addDays(config('instance.oauth.refresh_expiration', 400)));
Passport::enableImplicitGrant(); Passport::enableImplicitGrant();
if(config('instance.oauth.pat.enabled')) { if (config('instance.oauth.pat.enabled')) {
Passport::personalAccessClientId(config('instance.oauth.pat.id')); Passport::personalAccessClientId(config('instance.oauth.pat.id'));
} }
@ -38,7 +38,7 @@ class AuthServiceProvider extends ServiceProvider
'follow' => 'Ability to follow other profiles', 'follow' => 'Ability to follow other profiles',
'admin:read' => 'Read all data on the server', 'admin:read' => 'Read all data on the server',
'admin:write' => 'Modify all data on the server', 'admin:write' => 'Modify all data on the server',
'push' => 'Receive your push notifications' 'push' => 'Receive your push notifications',
]); ]);
Passport::setDefaultScope([ Passport::setDefaultScope([

View file

@ -0,0 +1,166 @@
<?php
namespace App\Services;
use App\Services\Internal\BeagleService;
use App\User;
use Illuminate\Support\Str;
class AdminSettingsService
{
public static function getAll()
{
return [
'features' => self::getFeatures(),
'landing' => self::getLanding(),
'branding' => self::getBranding(),
'media' => self::getMedia(),
'rules' => self::getRules(),
'suggested_rules' => self::getSuggestedRules(),
'users' => self::getUsers(),
'posts' => self::getPosts(),
'platform' => self::getPlatform(),
'storage' => self::getStorage(),
];
}
public static function getFeatures()
{
$cloud_storage = (bool) config_cache('pixelfed.cloud_storage');
$cloud_disk = config('filesystems.cloud');
$cloud_ready = ! empty(config('filesystems.disks.'.$cloud_disk.'.key')) && ! empty(config('filesystems.disks.'.$cloud_disk.'.secret'));
$openReg = (bool) config_cache('pixelfed.open_registration');
$curOnboarding = (bool) config_cache('instance.curated_registration.enabled');
$regState = $openReg ? 'open' : ($curOnboarding ? 'filtered' : 'closed');
return [
'registration_status' => $regState,
'cloud_storage' => $cloud_ready && $cloud_storage,
'activitypub_enabled' => (bool) config_cache('federation.activitypub.enabled'),
'account_migration' => (bool) config_cache('federation.migration'),
'mobile_apis' => (bool) config_cache('pixelfed.oauth_enabled'),
'stories' => (bool) config_cache('instance.stories.enabled'),
'instagram_import' => (bool) config_cache('pixelfed.import.instagram.enabled'),
'autospam_enabled' => (bool) config_cache('pixelfed.bouncer.enabled'),
];
}
public static function getLanding()
{
$availableAdmins = User::whereIsAdmin(true)->get();
$currentAdmin = config_cache('instance.admin.pid');
return [
'admins' => $availableAdmins,
'current_admin' => $currentAdmin,
'show_directory' => (bool) config_cache('instance.landing.show_directory'),
'show_explore' => (bool) config_cache('instance.landing.show_explore'),
];
}
public static function getBranding()
{
return [
'name' => config_cache('app.name'),
'short_description' => config_cache('app.short_description'),
'long_description' => config_cache('app.description'),
];
}
public static function getMedia()
{
return [
'max_photo_size' => config_cache('pixelfed.max_photo_size'),
'max_album_length' => config_cache('pixelfed.max_album_length'),
'image_quality' => config_cache('pixelfed.image_quality'),
'media_types' => config_cache('pixelfed.media_types'),
'optimize_image' => (bool) config_cache('pixelfed.optimize_image'),
'optimize_video' => (bool) config_cache('pixelfed.optimize_video'),
];
}
public static function getRules()
{
return config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : [];
}
public static function getSuggestedRules()
{
return BeagleService::getDefaultRules();
}
public static function getUsers()
{
$autoFollow = config_cache('account.autofollow_usernames');
if (strlen($autoFollow) >= 2) {
$autoFollow = explode(',', $autoFollow);
} else {
$autoFollow = [];
}
return [
'require_email_verification' => (bool) config_cache('pixelfed.enforce_email_verification'),
'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit'),
'max_account_size' => config_cache('pixelfed.max_account_size'),
'admin_autofollow' => (bool) config_cache('account.autofollow'),
'admin_autofollow_accounts' => $autoFollow,
'max_user_blocks' => (int) config_cache('instance.user_filters.max_user_blocks'),
'max_user_mutes' => (int) config_cache('instance.user_filters.max_user_mutes'),
'max_domain_blocks' => (int) config_cache('instance.user_filters.max_domain_blocks'),
];
}
public static function getPosts()
{
return [
'max_caption_length' => config_cache('pixelfed.max_caption_length'),
'max_altext_length' => config_cache('pixelfed.max_altext_length'),
];
}
public static function getPlatform()
{
return [
'allow_app_registration' => (bool) config_cache('pixelfed.allow_app_registration'),
'app_registration_rate_limit_attempts' => config_cache('pixelfed.app_registration_rate_limit_attempts'),
'app_registration_rate_limit_decay' => config_cache('pixelfed.app_registration_rate_limit_decay'),
'app_registration_confirm_rate_limit_attempts' => config_cache('pixelfed.app_registration_confirm_rate_limit_attempts'),
'app_registration_confirm_rate_limit_decay' => config_cache('pixelfed.app_registration_confirm_rate_limit_decay'),
'allow_post_embeds' => (bool) config_cache('instance.embed.post'),
'allow_profile_embeds' => (bool) config_cache('instance.embed.profile'),
'captcha_enabled' => (bool) config_cache('captcha.enabled'),
'captcha_on_login' => (bool) config_cache('captcha.active.login'),
'captcha_on_register' => (bool) config_cache('captcha.active.register'),
'captcha_secret' => Str::of(config_cache('captcha.secret'))->mask('*', 4, -4),
'captcha_sitekey' => Str::of(config_cache('captcha.sitekey'))->mask('*', 4, -4),
'custom_emoji_enabled' => (bool) config_cache('federation.custom_emoji.enabled'),
];
}
public static function getStorage()
{
$cloud_storage = (bool) config_cache('pixelfed.cloud_storage');
$cloud_disk = config('filesystems.cloud');
$cloud_ready = ! empty(config('filesystems.disks.'.$cloud_disk.'.key')) && ! empty(config('filesystems.disks.'.$cloud_disk.'.secret'));
$primaryDisk = (bool) $cloud_ready && $cloud_storage;
$pkey = 'filesystems.disks.'.$cloud_disk.'.';
$disk = [
'driver' => $cloud_disk,
'key' => Str::of(config_cache($pkey.'key'))->mask('*', 0, -2),
'secret' => Str::of(config_cache($pkey.'secret'))->mask('*', 0, -2),
'region' => config_cache($pkey.'region'),
'bucket' => config_cache($pkey.'bucket'),
'visibility' => config_cache($pkey.'visibility'),
'endpoint' => config_cache($pkey.'endpoint'),
'url' => config_cache($pkey.'url'),
'use_path_style_endpoint' => config_cache($pkey.'use_path_style_endpoint'),
];
return [
'primary_disk' => $primaryDisk ? 'cloud' : 'local',
'cloud_ready' => (bool) $cloud_ready,
'cloud_disk' => $cloud_disk,
'disk_config' => $disk,
];
}
}

View file

@ -2,77 +2,82 @@
namespace App\Services; namespace App\Services;
use App\Util\Lexer\Classifier;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use App\Util\Lexer\Classifier;
class AutospamService class AutospamService
{ {
const CHCKD_CACHE_KEY = 'pf:services:autospam:nlp:checked'; const CHCKD_CACHE_KEY = 'pf:services:autospam:nlp:checked';
const MODEL_CACHE_KEY = 'pf:services:autospam:nlp:model-cache';
const MODEL_FILE_PATH = 'nlp/active-training-data.json';
const MODEL_SPAM_PATH = 'nlp/spam.json';
const MODEL_HAM_PATH = 'nlp/ham.json';
public static function check($text) const MODEL_CACHE_KEY = 'pf:services:autospam:nlp:model-cache';
{
if(!$text || strlen($text) == 0) {
false;
}
if(!self::active()) {
return null;
}
$model = self::getCachedModel();
$classifier = new Classifier;
$classifier->import($model['documents'], $model['words']);
return $classifier->most($text) === 'spam';
}
public static function eligible() const MODEL_FILE_PATH = 'nlp/active-training-data.json';
{
return Cache::remember(self::CHCKD_CACHE_KEY, 86400, function() {
if(!config_cache('pixelfed.bouncer.enabled') || !config('autospam.enabled')) {
return false;
}
if(!Storage::exists(self::MODEL_SPAM_PATH)) { const MODEL_SPAM_PATH = 'nlp/spam.json';
return false;
}
if(!Storage::exists(self::MODEL_HAM_PATH)) { const MODEL_HAM_PATH = 'nlp/ham.json';
return false;
}
if(!Storage::exists(self::MODEL_FILE_PATH)) { public static function check($text)
return false; {
} else { if (! $text || strlen($text) == 0) {
if(Storage::size(self::MODEL_FILE_PATH) < 1000) {
return false;
}
}
return true; }
}); if (! self::active()) {
} return null;
}
$model = self::getCachedModel();
$classifier = new Classifier;
$classifier->import($model['documents'], $model['words']);
public static function active() return $classifier->most($text) === 'spam';
{ }
return config_cache('autospam.nlp.enabled') && self::eligible();
}
public static function getCachedModel() public static function eligible()
{ {
if(!self::active()) { return Cache::remember(self::CHCKD_CACHE_KEY, 86400, function () {
return null; if (! (bool) config_cache('pixelfed.bouncer.enabled') || ! (bool) config_cache('autospam.enabled')) {
} return false;
}
return Cache::remember(self::MODEL_CACHE_KEY, 86400, function() { if (! Storage::exists(self::MODEL_SPAM_PATH)) {
$res = Storage::get(self::MODEL_FILE_PATH); return false;
if(!$res || empty($res)) { }
return null;
}
return json_decode($res, true); if (! Storage::exists(self::MODEL_HAM_PATH)) {
}); return false;
} }
if (! Storage::exists(self::MODEL_FILE_PATH)) {
return false;
} else {
if (Storage::size(self::MODEL_FILE_PATH) < 1000) {
return false;
}
}
return true;
});
}
public static function active()
{
return config_cache('autospam.nlp.enabled') && self::eligible();
}
public static function getCachedModel()
{
if (! self::active()) {
return null;
}
return Cache::remember(self::MODEL_CACHE_KEY, 86400, function () {
$res = Storage::get(self::MODEL_FILE_PATH);
if (! $res || empty($res)) {
return null;
}
return json_decode($res, true);
});
}
} }

View file

@ -8,6 +8,14 @@ use Cache;
class ConfigCacheService class ConfigCacheService
{ {
const CACHE_KEY = 'config_cache:_v0-key:'; const CACHE_KEY = 'config_cache:_v0-key:';
const PROTECTED_KEYS = [
'filesystems.disks.s3.key',
'filesystems.disks.s3.secret',
'filesystems.disks.spaces.key',
'filesystems.disks.spaces.secret',
'captcha.secret',
'captcha.sitekey',
];
public static function get($key) public static function get($key)
{ {
@ -89,6 +97,41 @@ class ConfigCacheService
'pixelfed.app_registration_confirm_rate_limit_decay', 'pixelfed.app_registration_confirm_rate_limit_decay',
'instance.embed.profile', 'instance.embed.profile',
'instance.embed.post', 'instance.embed.post',
'captcha.enabled',
'captcha.secret',
'captcha.sitekey',
'captcha.active.login',
'captcha.active.register',
'captcha.triggers.login.enabled',
'captcha.triggers.login.attempts',
'federation.custom_emoji.enabled',
'pixelfed.optimize_image',
'pixelfed.optimize_video',
'pixelfed.max_collection_length',
'media.delete_local_after_cloud',
'instance.user_filters.max_user_blocks',
'instance.user_filters.max_user_mutes',
'instance.user_filters.max_domain_blocks',
'filesystems.disks.s3.key',
'filesystems.disks.s3.secret',
'filesystems.disks.s3.region',
'filesystems.disks.s3.bucket',
'filesystems.disks.s3.visibility',
'filesystems.disks.s3.url',
'filesystems.disks.s3.endpoint',
'filesystems.disks.s3.use_path_style_endpoint',
'filesystems.disks.spaces.key',
'filesystems.disks.spaces.secret',
'filesystems.disks.spaces.region',
'filesystems.disks.spaces.bucket',
'filesystems.disks.spaces.visibility',
'filesystems.disks.spaces.url',
'filesystems.disks.spaces.endpoint',
'filesystems.disks.spaces.use_path_style_endpoint',
// 'system.user_mode' // 'system.user_mode'
]; ];
@ -100,20 +143,34 @@ class ConfigCacheService
return config($key); return config($key);
} }
$protect = false;
$protected = null;
if(in_array($key, self::PROTECTED_KEYS)) {
$protect = true;
}
$v = config($key); $v = config($key);
$c = ConfigCacheModel::where('k', $key)->first(); $c = ConfigCacheModel::where('k', $key)->first();
if ($c) { if ($c) {
return $c->v ?? config($key); if($protect) {
return decrypt($c->v) ?? config($key);
} else {
return $c->v ?? config($key);
}
} }
if (! $v) { if (! $v) {
return; return;
} }
if($protect && $v) {
$protected = encrypt($v);
}
$cc = new ConfigCacheModel; $cc = new ConfigCacheModel;
$cc->k = $key; $cc->k = $key;
$cc->v = $v; $cc->v = $protect ? $protected : $v;
$cc->save(); $cc->save();
return $v; return $v;
@ -124,8 +181,15 @@ class ConfigCacheService
{ {
$exists = ConfigCacheModel::whereK($key)->first(); $exists = ConfigCacheModel::whereK($key)->first();
$protect = false;
$protected = null;
if(in_array($key, self::PROTECTED_KEYS)) {
$protect = true;
$protected = encrypt($val);
}
if ($exists) { if ($exists) {
$exists->v = $val; $exists->v = $protect ? $protected : $val;
$exists->save(); $exists->save();
Cache::put(self::CACHE_KEY.$key, $val, now()->addHours(12)); Cache::put(self::CACHE_KEY.$key, $val, now()->addHours(12));
@ -134,7 +198,7 @@ class ConfigCacheService
$cc = new ConfigCacheModel; $cc = new ConfigCacheModel;
$cc->k = $key; $cc->k = $key;
$cc->v = $val; $cc->v = $protect ? $protected : $val;
$cc->save(); $cc->save();
Cache::put(self::CACHE_KEY.$key, $val, now()->addHours(12)); Cache::put(self::CACHE_KEY.$key, $val, now()->addHours(12));

View file

@ -13,7 +13,7 @@ class CustomEmojiService
{ {
public static function get($shortcode) public static function get($shortcode)
{ {
if(config('federation.custom_emoji.enabled') == false) { if((bool) config_cache('federation.custom_emoji.enabled') == false) {
return; return;
} }
@ -22,7 +22,7 @@ class CustomEmojiService
public static function import($url, $id = false) public static function import($url, $id = false)
{ {
if(config('federation.custom_emoji.enabled') == false) { if((bool) config_cache('federation.custom_emoji.enabled') == false) {
return; return;
} }

View file

@ -0,0 +1,82 @@
<?php
namespace App\Services;
use Aws\S3\S3Client;
use Aws\S3\Exception\S3Exception;
use GuzzleHttp\Exception\ConnectException;
use League\Flysystem\AwsS3V3\AwsS3V3Adapter;
use League\Flysystem\Filesystem;
use League\Flysystem\UnableToRetrieveMetadata;
use League\Flysystem\FilesystemException;
use League\Flysystem\UnableToListContents;
use League\Flysystem\FileAttributes;
use League\Flysystem\UnableToWriteFile;
class FilesystemService
{
const VERIFY_FILE_NAME = 'cfstest.txt';
public static function getVerifyCredentials($key, $secret, $region, $bucket, $endpoint)
{
$client = new S3Client([
'version' => 'latest',
'region' => $region,
'endpoint' => $endpoint,
'credentials' => [
'key' => $key,
'secret' => $secret,
]
]);
$adapter = new AwsS3V3Adapter(
$client,
$bucket,
);
$throw = false;
$filesystem = new Filesystem($adapter);
$writable = false;
try {
$filesystem->write(self::VERIFY_FILE_NAME, 'ok', []);
$writable = true;
} catch (FilesystemException | UnableToWriteFile $exception) {
$writable = false;
}
if(!$writable) {
return false;
}
try {
$response = $filesystem->read(self::VERIFY_FILE_NAME);
if($response === 'ok') {
$writable = true;
$res[] = self::VERIFY_FILE_NAME;
} else {
$writable = false;
}
} catch (FilesystemException | UnableToReadFile $exception) {
$writable = false;
}
if(in_array(self::VERIFY_FILE_NAME, $res)) {
try {
$filesystem->delete(self::VERIFY_FILE_NAME);
} catch (FilesystemException | UnableToDeleteFile $exception) {
$writable = false;
}
}
if(!$writable) {
return false;
}
if(in_array(self::VERIFY_FILE_NAME, $res)) {
return true;
}
return false;
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace App\Services\Internal;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
class BeagleService
{
const DEFAULT_RULES_CACHE_KEY = 'pf:services:beagle:default_rules:v1';
public static function getDefaultRules()
{
return Cache::remember(self::DEFAULT_RULES_CACHE_KEY, now()->addDays(7), function() {
try {
$res = Http::withOptions(['allow_redirects' => false])
->timeout(5)
->connectTimeout(5)
->retry(2, 500)
->get('https://beagle.pixelfed.net/api/v1/common/suggestions/rules');
} catch (RequestException $e) {
return;
} catch (ConnectionException $e) {
return;
} catch (Exception $e) {
return;
}
if(!$res->ok()) {
return;
}
$json = $res->json();
if(!isset($json['rule_suggestions']) || !count($json['rule_suggestions'])) {
return [];
}
return $json['rule_suggestions'];
});
}
}

View file

@ -53,8 +53,8 @@ class LandingService
'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' => (bool) config_cache('instance.landing.show_directory'),
'show_explore_feed' => config_cache('instance.landing.show_explore'), 'show_explore_feed' => (bool) config_cache('instance.landing.show_explore'),
'open_registration' => (bool) $openReg, 'open_registration' => (bool) $openReg,
'curated_onboarding' => (bool) config_cache('instance.curated_registration.enabled'), 'curated_onboarding' => (bool) config_cache('instance.curated_registration.enabled'),
'version' => config('pixelfed.version'), 'version' => config('pixelfed.version'),
@ -85,7 +85,7 @@ class LandingService
'media_types' => config_cache('pixelfed.media_types'), 'media_types' => config_cache('pixelfed.media_types'),
], ],
'features' => [ 'features' => [
'federation' => config_cache('federation.activitypub.enabled'), 'federation' => (bool) config_cache('federation.activitypub.enabled'),
'timelines' => [ 'timelines' => [
'local' => true, 'local' => true,
'network' => (bool) config_cache('federation.network_timeline'), 'network' => (bool) config_cache('federation.network_timeline'),

View file

@ -2,44 +2,38 @@
namespace App\Services; namespace App\Services;
use App\Jobs\AvatarPipeline\AvatarStorageCleanup;
use App\Jobs\MediaPipeline\MediaDeletePipeline;
use App\Media;
use App\Util\ActivityPub\Helpers; use App\Util\ActivityPub\Helpers;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Http\File; use Illuminate\Http\File;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use App\Media;
use App\Profile;
use App\User;
use GuzzleHttp\Client;
use App\Services\AccountService;
use App\Http\Controllers\AvatarController;
use GuzzleHttp\Exception\RequestException;
use App\Jobs\MediaPipeline\MediaDeletePipeline;
use Illuminate\Support\Arr;
use App\Jobs\AvatarPipeline\AvatarStorageCleanup;
class MediaStorageService {
class MediaStorageService
{
public static function store(Media $media) public static function store(Media $media)
{ {
if(config_cache('pixelfed.cloud_storage') == true) { if ((bool) config_cache('pixelfed.cloud_storage') == true) {
(new self())->cloudStore($media); (new self())->cloudStore($media);
} }
return;
} }
public static function move(Media $media) public static function move(Media $media)
{ {
if($media->remote_media) { if ($media->remote_media) {
return; return;
} }
if(config_cache('pixelfed.cloud_storage') == true) { if ((bool) config_cache('pixelfed.cloud_storage') == true) {
return (new self())->cloudMove($media); return (new self())->cloudMove($media);
} }
return;
} }
public static function avatar($avatar, $local = false, $skipRecentCheck = false) public static function avatar($avatar, $local = false, $skipRecentCheck = false)
@ -56,31 +50,31 @@ class MediaStorageService {
return false; return false;
} }
$h = Arr::mapWithKeys($r->getHeaders(), function($item, $key) { $h = Arr::mapWithKeys($r->getHeaders(), function ($item, $key) {
return [strtolower($key) => last($item)]; return [strtolower($key) => last($item)];
}); });
if(!isset($h['content-length'], $h['content-type'])) { if (! isset($h['content-length'], $h['content-type'])) {
return false; return false;
} }
$len = (int) $h['content-length']; $len = (int) $h['content-length'];
$mime = $h['content-type']; $mime = $h['content-type'];
if($len < 10 || $len > ((config_cache('pixelfed.max_photo_size') * 1000))) { if ($len < 10 || $len > ((config_cache('pixelfed.max_photo_size') * 1000))) {
return false; return false;
} }
return [ return [
'length' => $len, 'length' => $len,
'mime' => $mime 'mime' => $mime,
]; ];
} }
protected function cloudStore($media) protected function cloudStore($media)
{ {
if($media->remote_media == true) { if ($media->remote_media == true) {
if(config('media.storage.remote.cloud')) { if (config('media.storage.remote.cloud')) {
(new self())->remoteToCloud($media); (new self())->remoteToCloud($media);
} }
} else { } else {
@ -100,7 +94,7 @@ class MediaStorageService {
$storagePath = implode('/', $p); $storagePath = implode('/', $p);
$url = ResilientMediaStorageService::store($storagePath, $path, $name); $url = ResilientMediaStorageService::store($storagePath, $path, $name);
if($thumb) { if ($thumb) {
$thumbUrl = ResilientMediaStorageService::store($storagePath, $thumb, $thumbname); $thumbUrl = ResilientMediaStorageService::store($storagePath, $thumb, $thumbname);
$media->thumbnail_url = $thumbUrl; $media->thumbnail_url = $thumbUrl;
} }
@ -108,8 +102,8 @@ class MediaStorageService {
$media->optimized_url = $url; $media->optimized_url = $url;
$media->replicated_at = now(); $media->replicated_at = now();
$media->save(); $media->save();
if($media->status_id) { if ($media->status_id) {
Cache::forget('status:transformer:media:attachments:' . $media->status_id); Cache::forget('status:transformer:media:attachments:'.$media->status_id);
MediaService::del($media->status_id); MediaService::del($media->status_id);
StatusService::del($media->status_id, false); StatusService::del($media->status_id, false);
} }
@ -119,20 +113,20 @@ class MediaStorageService {
{ {
$url = $media->remote_url; $url = $media->remote_url;
if(!Helpers::validateUrl($url)) { if (! Helpers::validateUrl($url)) {
return; return;
} }
$head = $this->head($media->remote_url); $head = $this->head($media->remote_url);
if(!$head) { if (! $head) {
return; return;
} }
$mimes = [ $mimes = [
'image/jpeg', 'image/jpeg',
'image/png', 'image/png',
'video/mp4' 'video/mp4',
]; ];
$mime = $head['mime']; $mime = $head['mime'];
@ -141,11 +135,11 @@ class MediaStorageService {
$media->remote_media = true; $media->remote_media = true;
$media->save(); $media->save();
if(!in_array($mime, $mimes)) { if (! in_array($mime, $mimes)) {
return; return;
} }
if($head['length'] >= $max_size) { if ($head['length'] >= $max_size) {
return; return;
} }
@ -168,10 +162,10 @@ class MediaStorageService {
} }
$base = MediaPathService::get($media->profile); $base = MediaPathService::get($media->profile);
$path = Str::random(40) . $ext; $path = Str::random(40).$ext;
$tmpBase = storage_path('app/remcache/'); $tmpBase = storage_path('app/remcache/');
$tmpPath = $media->profile_id . '-' . $path; $tmpPath = $media->profile_id.'-'.$path;
$tmpName = $tmpBase . $tmpPath; $tmpName = $tmpBase.$tmpPath;
$data = file_get_contents($url, false, null, 0, $head['length']); $data = file_get_contents($url, false, null, 0, $head['length']);
file_put_contents($tmpName, $data); file_put_contents($tmpName, $data);
$hash = hash_file('sha256', $tmpName); $hash = hash_file('sha256', $tmpName);
@ -186,8 +180,8 @@ class MediaStorageService {
$media->replicated_at = now(); $media->replicated_at = now();
$media->save(); $media->save();
if($media->status_id) { if ($media->status_id) {
Cache::forget('status:transformer:media:attachments:' . $media->status_id); Cache::forget('status:transformer:media:attachments:'.$media->status_id);
} }
unlink($tmpName); unlink($tmpName);
@ -199,13 +193,13 @@ class MediaStorageService {
$url = $avatar->remote_url; $url = $avatar->remote_url;
$driver = $local ? 'local' : config('filesystems.cloud'); $driver = $local ? 'local' : config('filesystems.cloud');
if(empty($url) || Helpers::validateUrl($url) == false) { if (empty($url) || Helpers::validateUrl($url) == false) {
return; return;
} }
$head = $this->head($url); $head = $this->head($url);
if($head == false) { if ($head == false) {
return; return;
} }
@ -218,46 +212,47 @@ class MediaStorageService {
$mime = $head['mime']; $mime = $head['mime'];
$max_size = (int) config('pixelfed.max_avatar_size') * 1000; $max_size = (int) config('pixelfed.max_avatar_size') * 1000;
if(!$skipRecentCheck) { if (! $skipRecentCheck) {
if($avatar->last_fetched_at && $avatar->last_fetched_at->gt(now()->subMonths(3))) { if ($avatar->last_fetched_at && $avatar->last_fetched_at->gt(now()->subMonths(3))) {
return; return;
} }
} }
Cache::forget('avatar:' . $avatar->profile_id); Cache::forget('avatar:'.$avatar->profile_id);
AccountService::del($avatar->profile_id); AccountService::del($avatar->profile_id);
// handle pleroma edge case // handle pleroma edge case
if(Str::endsWith($mime, '; charset=utf-8')) { if (Str::endsWith($mime, '; charset=utf-8')) {
$mime = str_replace('; charset=utf-8', '', $mime); $mime = str_replace('; charset=utf-8', '', $mime);
} }
if(!in_array($mime, $mimes)) { if (! in_array($mime, $mimes)) {
return; return;
} }
if($head['length'] >= $max_size) { if ($head['length'] >= $max_size) {
return; return;
} }
$base = ($local ? 'public/cache/' : 'cache/') . 'avatars/' . $avatar->profile_id; $base = ($local ? 'public/cache/' : 'cache/').'avatars/'.$avatar->profile_id;
$ext = $head['mime'] == 'image/jpeg' ? 'jpg' : 'png'; $ext = $head['mime'] == 'image/jpeg' ? 'jpg' : 'png';
$path = 'avatar_' . strtolower(Str::random(random_int(3,6))) . '.' . $ext; $path = 'avatar_'.strtolower(Str::random(random_int(3, 6))).'.'.$ext;
$tmpBase = storage_path('app/remcache/'); $tmpBase = storage_path('app/remcache/');
$tmpPath = 'avatar_' . $avatar->profile_id . '-' . $path; $tmpPath = 'avatar_'.$avatar->profile_id.'-'.$path;
$tmpName = $tmpBase . $tmpPath; $tmpName = $tmpBase.$tmpPath;
$data = @file_get_contents($url, false, null, 0, $head['length']); $data = @file_get_contents($url, false, null, 0, $head['length']);
if(!$data) { if (! $data) {
return; return;
} }
file_put_contents($tmpName, $data); file_put_contents($tmpName, $data);
$mimeCheck = Storage::mimeType('remcache/' . $tmpPath); $mimeCheck = Storage::mimeType('remcache/'.$tmpPath);
if(!$mimeCheck || !in_array($mimeCheck, ['image/png', 'image/jpeg'])) { if (! $mimeCheck || ! in_array($mimeCheck, ['image/png', 'image/jpeg'])) {
$avatar->last_fetched_at = now(); $avatar->last_fetched_at = now();
$avatar->save(); $avatar->save();
unlink($tmpName); unlink($tmpName);
return; return;
} }
@ -265,15 +260,15 @@ class MediaStorageService {
$file = $disk->putFileAs($base, new File($tmpName), $path, 'public'); $file = $disk->putFileAs($base, new File($tmpName), $path, 'public');
$permalink = $disk->url($file); $permalink = $disk->url($file);
$avatar->media_path = $base . '/' . $path; $avatar->media_path = $base.'/'.$path;
$avatar->is_remote = true; $avatar->is_remote = true;
$avatar->cdn_url = $local ? config('app.url') . $permalink : $permalink; $avatar->cdn_url = $local ? config('app.url').$permalink : $permalink;
$avatar->size = $head['length']; $avatar->size = $head['length'];
$avatar->change_count = $avatar->change_count + 1; $avatar->change_count = $avatar->change_count + 1;
$avatar->last_fetched_at = now(); $avatar->last_fetched_at = now();
$avatar->save(); $avatar->save();
Cache::forget('avatar:' . $avatar->profile_id); Cache::forget('avatar:'.$avatar->profile_id);
AccountService::del($avatar->profile_id); AccountService::del($avatar->profile_id);
AvatarStorageCleanup::dispatch($avatar)->onQueue($queue)->delay(now()->addMinutes(random_int(3, 15))); AvatarStorageCleanup::dispatch($avatar)->onQueue($queue)->delay(now()->addMinutes(random_int(3, 15)));
@ -282,7 +277,7 @@ class MediaStorageService {
public static function delete(Media $media, $confirm = false) public static function delete(Media $media, $confirm = false)
{ {
if(!$confirm) { if (! $confirm) {
return; return;
} }
MediaDeletePipeline::dispatch($media)->onQueue('mmo'); MediaDeletePipeline::dispatch($media)->onQueue('mmo');
@ -290,13 +285,13 @@ class MediaStorageService {
protected function cloudMove($media) protected function cloudMove($media)
{ {
if(!Storage::exists($media->media_path)) { if (! Storage::exists($media->media_path)) {
return 'invalid file'; return 'invalid file';
} }
$path = storage_path('app/'.$media->media_path); $path = storage_path('app/'.$media->media_path);
$thumb = false; $thumb = false;
if($media->thumbnail_path) { if ($media->thumbnail_path) {
$thumb = storage_path('app/'.$media->thumbnail_path); $thumb = storage_path('app/'.$media->thumbnail_path);
$pt = explode('/', $media->thumbnail_path); $pt = explode('/', $media->thumbnail_path);
$thumbname = array_pop($pt); $thumbname = array_pop($pt);
@ -307,7 +302,7 @@ class MediaStorageService {
$storagePath = implode('/', $p); $storagePath = implode('/', $p);
$url = ResilientMediaStorageService::store($storagePath, $path, $name); $url = ResilientMediaStorageService::store($storagePath, $path, $name);
if($thumb) { if ($thumb) {
$thumbUrl = ResilientMediaStorageService::store($storagePath, $thumb, $thumbname); $thumbUrl = ResilientMediaStorageService::store($storagePath, $thumb, $thumbname);
$media->thumbnail_url = $thumbUrl; $media->thumbnail_url = $thumbUrl;
} }
@ -316,8 +311,8 @@ class MediaStorageService {
$media->replicated_at = now(); $media->replicated_at = now();
$media->save(); $media->save();
if($media->status_id) { if ($media->status_id) {
Cache::forget('status:transformer:media:attachments:' . $media->status_id); Cache::forget('status:transformer:media:attachments:'.$media->status_id);
MediaService::del($media->status_id); MediaService::del($media->status_id);
StatusService::del($media->status_id, false); StatusService::del($media->status_id, false);
} }

View file

@ -2,49 +2,34 @@
namespace App\Util\ActivityPub; namespace App\Util\ActivityPub;
use DB, Cache, Purify, Storage, Request, Validator; use App\Instance;
use App\{
Activity,
Follower,
Instance,
Like,
Media,
Notification,
Profile,
Status
};
use Zttp\Zttp;
use Carbon\Carbon;
use GuzzleHttp\Client;
use Illuminate\Http\File;
use Illuminate\Validation\Rule;
use App\Jobs\AvatarPipeline\CreateAvatar;
use App\Jobs\RemoteFollowPipeline\RemoteFollowImportRecent;
use App\Jobs\ImageOptimizePipeline\{ImageOptimize,ImageThumbnail};
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Jobs\StatusPipeline\StatusReplyPipeline;
use App\Jobs\StatusPipeline\StatusTagsPipeline;
use App\Util\ActivityPub\HttpSignature;
use Illuminate\Support\Str;
use App\Services\ActivityPubFetchService;
use App\Services\ActivityPubDeliveryService;
use App\Services\CustomEmojiService;
use App\Services\InstanceService;
use App\Services\MediaPathService;
use App\Services\MediaStorageService;
use App\Services\NetworkTimelineService;
use App\Jobs\MediaPipeline\MediaStoragePipeline;
use App\Jobs\AvatarPipeline\RemoteAvatarFetch; use App\Jobs\AvatarPipeline\RemoteAvatarFetch;
use App\Jobs\HomeFeedPipeline\FeedInsertRemotePipeline; use App\Jobs\HomeFeedPipeline\FeedInsertRemotePipeline;
use App\Util\Media\License; use App\Jobs\MediaPipeline\MediaStoragePipeline;
use App\Jobs\StatusPipeline\StatusReplyPipeline;
use App\Jobs\StatusPipeline\StatusTagsPipeline;
use App\Media;
use App\Models\Poll; use App\Models\Poll;
use Illuminate\Contracts\Cache\LockTimeoutException; use App\Profile;
use App\Services\DomainService;
use App\Services\UserFilterService;
use App\Services\Account\AccountStatService; use App\Services\Account\AccountStatService;
use App\Services\ActivityPubDeliveryService;
use App\Services\ActivityPubFetchService;
use App\Services\DomainService;
use App\Services\InstanceService;
use App\Services\MediaPathService;
use App\Services\NetworkTimelineService;
use App\Services\UserFilterService;
use App\Status;
use App\Util\Media\License;
use Cache;
use Carbon\Carbon;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Purify;
use Validator;
class Helpers { class Helpers
{
public static function validateObject($data) public static function validateObject($data)
{ {
$verbs = ['Create', 'Announce', 'Like', 'Follow', 'Delete', 'Accept', 'Reject', 'Undo', 'Tombstone']; $verbs = ['Create', 'Announce', 'Like', 'Follow', 'Delete', 'Accept', 'Reject', 'Undo', 'Tombstone'];
@ -53,14 +38,14 @@ class Helpers {
'type' => [ 'type' => [
'required', 'required',
'string', 'string',
Rule::in($verbs) Rule::in($verbs),
], ],
'id' => 'required|string', 'id' => 'required|string',
'actor' => 'required|string|url', 'actor' => 'required|string|url',
'object' => 'required', 'object' => 'required',
'object.type' => 'required_if:type,Create', 'object.type' => 'required_if:type,Create',
'object.attributedTo' => 'required_if:type,Create|url', 'object.attributedTo' => 'required_if:type,Create|url',
'published' => 'required_if:type,Create|date' 'published' => 'required_if:type,Create|date',
])->passes(); ])->passes();
return $valid; return $valid;
@ -68,8 +53,8 @@ class Helpers {
public static function verifyAttachments($data) public static function verifyAttachments($data)
{ {
if(!isset($data['object']) || empty($data['object'])) { if (! isset($data['object']) || empty($data['object'])) {
$data = ['object'=>$data]; $data = ['object' => $data];
} }
$activity = $data['object']; $activity = $data['object'];
@ -80,7 +65,7 @@ class Helpers {
// Peertube // Peertube
// $mediaTypes = in_array('video/mp4', $mimeTypes) ? ['Document', 'Image', 'Video', 'Link'] : ['Document', 'Image']; // $mediaTypes = in_array('video/mp4', $mimeTypes) ? ['Document', 'Image', 'Video', 'Link'] : ['Document', 'Image'];
if(!isset($activity['attachment']) || empty($activity['attachment'])) { if (! isset($activity['attachment']) || empty($activity['attachment'])) {
return false; return false;
} }
@ -100,13 +85,13 @@ class Helpers {
'*.type' => [ '*.type' => [
'required', 'required',
'string', 'string',
Rule::in($mediaTypes) Rule::in($mediaTypes),
], ],
'*.url' => 'required|url', '*.url' => 'required|url',
'*.mediaType' => [ '*.mediaType' => [
'required', 'required',
'string', 'string',
Rule::in($mimeTypes) Rule::in($mimeTypes),
], ],
'*.name' => 'sometimes|nullable|string', '*.name' => 'sometimes|nullable|string',
'*.blurhash' => 'sometimes|nullable|string|min:6|max:164', '*.blurhash' => 'sometimes|nullable|string|min:6|max:164',
@ -119,7 +104,7 @@ class Helpers {
public static function normalizeAudience($data, $localOnly = true) public static function normalizeAudience($data, $localOnly = true)
{ {
if(!isset($data['to'])) { if (! isset($data['to'])) {
return; return;
} }
@ -128,32 +113,35 @@ class Helpers {
$audience['cc'] = []; $audience['cc'] = [];
$scope = 'private'; $scope = 'private';
if(is_array($data['to']) && !empty($data['to'])) { if (is_array($data['to']) && ! empty($data['to'])) {
foreach ($data['to'] as $to) { foreach ($data['to'] as $to) {
if($to == 'https://www.w3.org/ns/activitystreams#Public') { if ($to == 'https://www.w3.org/ns/activitystreams#Public') {
$scope = 'public'; $scope = 'public';
continue; continue;
} }
$url = $localOnly ? self::validateLocalUrl($to) : self::validateUrl($to); $url = $localOnly ? self::validateLocalUrl($to) : self::validateUrl($to);
if($url != false) { if ($url != false) {
array_push($audience['to'], $url); array_push($audience['to'], $url);
} }
} }
} }
if(is_array($data['cc']) && !empty($data['cc'])) { if (is_array($data['cc']) && ! empty($data['cc'])) {
foreach ($data['cc'] as $cc) { foreach ($data['cc'] as $cc) {
if($cc == 'https://www.w3.org/ns/activitystreams#Public') { if ($cc == 'https://www.w3.org/ns/activitystreams#Public') {
$scope = 'unlisted'; $scope = 'unlisted';
continue; continue;
} }
$url = $localOnly ? self::validateLocalUrl($cc) : self::validateUrl($cc); $url = $localOnly ? self::validateLocalUrl($cc) : self::validateUrl($cc);
if($url != false) { if ($url != false) {
array_push($audience['cc'], $url); array_push($audience['cc'], $url);
} }
} }
} }
$audience['scope'] = $scope; $audience['scope'] = $scope;
return $audience; return $audience;
} }
@ -161,56 +149,57 @@ class Helpers {
{ {
$audience = self::normalizeAudience($data); $audience = self::normalizeAudience($data);
$url = $profile->permalink(); $url = $profile->permalink();
return in_array($url, $audience['to']) || in_array($url, $audience['cc']); return in_array($url, $audience['to']) || in_array($url, $audience['cc']);
} }
public static function validateUrl($url) public static function validateUrl($url)
{ {
if(is_array($url)) { if (is_array($url)) {
$url = $url[0]; $url = $url[0];
} }
$hash = hash('sha256', $url); $hash = hash('sha256', $url);
$key = "helpers:url:valid:sha256-{$hash}"; $key = "helpers:url:valid:sha256-{$hash}";
$valid = Cache::remember($key, 900, function() use($url) { $valid = Cache::remember($key, 900, function () use ($url) {
$localhosts = [ $localhosts = [
'127.0.0.1', 'localhost', '::1' '127.0.0.1', 'localhost', '::1',
]; ];
if(strtolower(mb_substr($url, 0, 8)) !== 'https://') { if (strtolower(mb_substr($url, 0, 8)) !== 'https://') {
return false; return false;
} }
if(substr_count($url, '://') !== 1) { if (substr_count($url, '://') !== 1) {
return false; return false;
} }
if(mb_substr($url, 0, 8) !== 'https://') { if (mb_substr($url, 0, 8) !== 'https://') {
$url = 'https://' . substr($url, 8); $url = 'https://'.substr($url, 8);
} }
$valid = filter_var($url, FILTER_VALIDATE_URL); $valid = filter_var($url, FILTER_VALIDATE_URL);
if(!$valid) { if (! $valid) {
return false; return false;
} }
$host = parse_url($valid, PHP_URL_HOST); $host = parse_url($valid, PHP_URL_HOST);
if(in_array($host, $localhosts)) { if (in_array($host, $localhosts)) {
return false; return false;
} }
if(config('security.url.verify_dns')) { if (config('security.url.verify_dns')) {
if(DomainService::hasValidDns($host) === false) { if (DomainService::hasValidDns($host) === false) {
return false; return false;
} }
} }
if(app()->environment() === 'production') { if (app()->environment() === 'production') {
$bannedInstances = InstanceService::getBannedDomains(); $bannedInstances = InstanceService::getBannedDomains();
if(in_array($host, $bannedInstances)) { if (in_array($host, $bannedInstances)) {
return false; return false;
} }
} }
@ -224,12 +213,14 @@ class Helpers {
public static function validateLocalUrl($url) public static function validateLocalUrl($url)
{ {
$url = self::validateUrl($url); $url = self::validateUrl($url);
if($url == true) { if ($url == true) {
$domain = config('pixelfed.domain.app'); $domain = config('pixelfed.domain.app');
$host = parse_url($url, PHP_URL_HOST); $host = parse_url($url, PHP_URL_HOST);
$url = strtolower($domain) === strtolower($host) ? $url : false; $url = strtolower($domain) === strtolower($host) ? $url : false;
return $url; return $url;
} }
return false; return false;
} }
@ -237,15 +228,16 @@ class Helpers {
{ {
$version = config('pixelfed.version'); $version = config('pixelfed.version');
$url = config('app.url'); $url = config('app.url');
return [ return [
'Accept' => 'application/activity+json', 'Accept' => 'application/activity+json',
'User-Agent' => "(Pixelfed/{$version}; +{$url})", 'User-Agent' => "(Pixelfed/{$version}; +{$url})",
]; ];
} }
public static function fetchFromUrl($url = false) public static function fetchFromUrl($url = false)
{ {
if(self::validateUrl($url) == false) { if (self::validateUrl($url) == false) {
return; return;
} }
@ -253,13 +245,13 @@ class Helpers {
$key = "helpers:url:fetcher:sha256-{$hash}"; $key = "helpers:url:fetcher:sha256-{$hash}";
$ttl = now()->addMinutes(15); $ttl = now()->addMinutes(15);
return Cache::remember($key, $ttl, function() use($url) { return Cache::remember($key, $ttl, function () use ($url) {
$res = ActivityPubFetchService::get($url); $res = ActivityPubFetchService::get($url);
if(!$res || empty($res)) { if (! $res || empty($res)) {
return false; return false;
} }
$res = json_decode($res, true, 8); $res = json_decode($res, true, 8);
if(json_last_error() == JSON_ERROR_NONE) { if (json_last_error() == JSON_ERROR_NONE) {
return $res; return $res;
} else { } else {
return false; return false;
@ -274,12 +266,12 @@ class Helpers {
public static function pluckval($val) public static function pluckval($val)
{ {
if(is_string($val)) { if (is_string($val)) {
return $val; return $val;
} }
if(is_array($val)) { if (is_array($val)) {
return !empty($val) ? head($val) : null; return ! empty($val) ? head($val) : null;
} }
return null; return null;
@ -288,51 +280,52 @@ class Helpers {
public static function statusFirstOrFetch($url, $replyTo = false) public static function statusFirstOrFetch($url, $replyTo = false)
{ {
$url = self::validateUrl($url); $url = self::validateUrl($url);
if($url == false) { if ($url == false) {
return; return;
} }
$host = parse_url($url, PHP_URL_HOST); $host = parse_url($url, PHP_URL_HOST);
$local = config('pixelfed.domain.app') == $host ? true : false; $local = config('pixelfed.domain.app') == $host ? true : false;
if($local) { if ($local) {
$id = (int) last(explode('/', $url)); $id = (int) last(explode('/', $url));
return Status::whereNotIn('scope', ['draft','archived'])->findOrFail($id);
return Status::whereNotIn('scope', ['draft', 'archived'])->findOrFail($id);
} }
$cached = Status::whereNotIn('scope', ['draft','archived']) $cached = Status::whereNotIn('scope', ['draft', 'archived'])
->whereUri($url) ->whereUri($url)
->orWhere('object_url', $url) ->orWhere('object_url', $url)
->first(); ->first();
if($cached) { if ($cached) {
return $cached; return $cached;
} }
$res = self::fetchFromUrl($url); $res = self::fetchFromUrl($url);
if(!$res || empty($res) || isset($res['error']) || !isset($res['@context']) || !isset($res['published']) ) { if (! $res || empty($res) || isset($res['error']) || ! isset($res['@context']) || ! isset($res['published'])) {
return; return;
} }
if(config('autospam.live_filters.enabled')) { if (config('autospam.live_filters.enabled')) {
$filters = config('autospam.live_filters.filters'); $filters = config('autospam.live_filters.filters');
if(!empty($filters) && isset($res['content']) && !empty($res['content']) && strlen($filters) > 3) { if (! empty($filters) && isset($res['content']) && ! empty($res['content']) && strlen($filters) > 3) {
$filters = array_map('trim', explode(',', $filters)); $filters = array_map('trim', explode(',', $filters));
$content = $res['content']; $content = $res['content'];
foreach($filters as $filter) { foreach ($filters as $filter) {
$filter = trim(strtolower($filter)); $filter = trim(strtolower($filter));
if(!$filter || !strlen($filter)) { if (! $filter || ! strlen($filter)) {
continue; continue;
} }
if(str_contains(strtolower($content), $filter)) { if (str_contains(strtolower($content), $filter)) {
return; return;
} }
} }
} }
} }
if(isset($res['object'])) { if (isset($res['object'])) {
$activity = $res; $activity = $res;
} else { } else {
$activity = ['object' => $res]; $activity = ['object' => $res];
@ -342,37 +335,37 @@ class Helpers {
$cw = isset($res['sensitive']) ? (bool) $res['sensitive'] : false; $cw = isset($res['sensitive']) ? (bool) $res['sensitive'] : false;
if(isset($res['to']) == true) { if (isset($res['to']) == true) {
if(is_array($res['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['to'])) { if (is_array($res['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['to'])) {
$scope = 'public'; $scope = 'public';
} }
if(is_string($res['to']) && 'https://www.w3.org/ns/activitystreams#Public' == $res['to']) { if (is_string($res['to']) && $res['to'] == 'https://www.w3.org/ns/activitystreams#Public') {
$scope = 'public'; $scope = 'public';
} }
} }
if(isset($res['cc']) == true) { if (isset($res['cc']) == true) {
if(is_array($res['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['cc'])) { if (is_array($res['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $res['cc'])) {
$scope = 'unlisted'; $scope = 'unlisted';
} }
if(is_string($res['cc']) && 'https://www.w3.org/ns/activitystreams#Public' == $res['cc']) { if (is_string($res['cc']) && $res['cc'] == 'https://www.w3.org/ns/activitystreams#Public') {
$scope = 'unlisted'; $scope = 'unlisted';
} }
} }
if(config('costar.enabled') == true) { if (config('costar.enabled') == true) {
$blockedKeywords = config('costar.keyword.block'); $blockedKeywords = config('costar.keyword.block');
if($blockedKeywords !== null) { if ($blockedKeywords !== null) {
$keywords = config('costar.keyword.block'); $keywords = config('costar.keyword.block');
foreach($keywords as $kw) { foreach ($keywords as $kw) {
if(Str::contains($res['content'], $kw) == true) { if (Str::contains($res['content'], $kw) == true) {
return; return;
} }
} }
} }
$unlisted = config('costar.domain.unlisted'); $unlisted = config('costar.domain.unlisted');
if(in_array(parse_url($url, PHP_URL_HOST), $unlisted) == true) { if (in_array(parse_url($url, PHP_URL_HOST), $unlisted) == true) {
$unlisted = true; $unlisted = true;
$scope = 'unlisted'; $scope = 'unlisted';
} else { } else {
@ -380,7 +373,7 @@ class Helpers {
} }
$cwDomains = config('costar.domain.cw'); $cwDomains = config('costar.domain.cw');
if(in_array(parse_url($url, PHP_URL_HOST), $cwDomains) == true) { if (in_array(parse_url($url, PHP_URL_HOST), $cwDomains) == true) {
$cw = true; $cw = true;
} }
} }
@ -389,15 +382,15 @@ class Helpers {
$idDomain = parse_url($id, PHP_URL_HOST); $idDomain = parse_url($id, PHP_URL_HOST);
$urlDomain = parse_url($url, PHP_URL_HOST); $urlDomain = parse_url($url, PHP_URL_HOST);
if($idDomain && $urlDomain && strtolower($idDomain) !== strtolower($urlDomain)) { if ($idDomain && $urlDomain && strtolower($idDomain) !== strtolower($urlDomain)) {
return; return;
} }
if(!self::validateUrl($id)) { if (! self::validateUrl($id)) {
return; return;
} }
if(!isset($activity['object']['attributedTo'])) { if (! isset($activity['object']['attributedTo'])) {
return; return;
} }
@ -405,39 +398,38 @@ class Helpers {
$activity['object']['attributedTo'] : $activity['object']['attributedTo'] :
(is_array($activity['object']['attributedTo']) ? (is_array($activity['object']['attributedTo']) ?
collect($activity['object']['attributedTo']) collect($activity['object']['attributedTo'])
->filter(function($o) { ->filter(function ($o) {
return $o && isset($o['type']) && $o['type'] == 'Person'; return $o && isset($o['type']) && $o['type'] == 'Person';
}) })
->pluck('id') ->pluck('id')
->first() : null ->first() : null
); );
if($attributedTo) { if ($attributedTo) {
$actorDomain = parse_url($attributedTo, PHP_URL_HOST); $actorDomain = parse_url($attributedTo, PHP_URL_HOST);
if(!self::validateUrl($attributedTo) || if (! self::validateUrl($attributedTo) ||
$idDomain !== $actorDomain || $idDomain !== $actorDomain ||
$actorDomain !== $urlDomain $actorDomain !== $urlDomain
) ) {
{
return; return;
} }
} }
if($idDomain !== $urlDomain) { if ($idDomain !== $urlDomain) {
return; return;
} }
$profile = self::profileFirstOrNew($attributedTo); $profile = self::profileFirstOrNew($attributedTo);
if(!$profile) { if (! $profile) {
return; return;
} }
if(isset($activity['object']['inReplyTo']) && !empty($activity['object']['inReplyTo']) || $replyTo == true) { if (isset($activity['object']['inReplyTo']) && ! empty($activity['object']['inReplyTo']) || $replyTo == true) {
$reply_to = self::statusFirstOrFetch(self::pluckval($activity['object']['inReplyTo']), false); $reply_to = self::statusFirstOrFetch(self::pluckval($activity['object']['inReplyTo']), false);
if($reply_to) { if ($reply_to) {
$blocks = UserFilterService::blocks($reply_to->profile_id); $blocks = UserFilterService::blocks($reply_to->profile_id);
if(in_array($profile->id, $blocks)) { if (in_array($profile->id, $blocks)) {
return; return;
} }
} }
@ -447,15 +439,15 @@ class Helpers {
} }
$ts = self::pluckval($res['published']); $ts = self::pluckval($res['published']);
if($scope == 'public' && in_array($urlDomain, InstanceService::getUnlistedDomains())) { if ($scope == 'public' && in_array($urlDomain, InstanceService::getUnlistedDomains())) {
$scope = 'unlisted'; $scope = 'unlisted';
} }
if(in_array($urlDomain, InstanceService::getNsfwDomains())) { if (in_array($urlDomain, InstanceService::getNsfwDomains())) {
$cw = true; $cw = true;
} }
if($res['type'] === 'Question') { if ($res['type'] === 'Question') {
$status = self::storePoll( $status = self::storePoll(
$profile, $profile,
$res, $res,
@ -466,6 +458,7 @@ class Helpers {
$scope, $scope,
$id $id
); );
return $status; return $status;
} else { } else {
$status = self::storeStatus($url, $profile, $res); $status = self::storeStatus($url, $profile, $res);
@ -482,12 +475,12 @@ class Helpers {
$idDomain = parse_url($id, PHP_URL_HOST); $idDomain = parse_url($id, PHP_URL_HOST);
$urlDomain = parse_url($url, PHP_URL_HOST); $urlDomain = parse_url($url, PHP_URL_HOST);
$originalUrlDomain = parse_url($originalUrl, PHP_URL_HOST); $originalUrlDomain = parse_url($originalUrl, PHP_URL_HOST);
if(!self::validateUrl($id) || !self::validateUrl($url)) { if (! self::validateUrl($id) || ! self::validateUrl($url)) {
return; return;
} }
if( strtolower($originalUrlDomain) !== strtolower($idDomain) || if (strtolower($originalUrlDomain) !== strtolower($idDomain) ||
strtolower($originalUrlDomain) !== strtolower($urlDomain) ) { strtolower($originalUrlDomain) !== strtolower($urlDomain)) {
return; return;
} }
@ -498,21 +491,21 @@ class Helpers {
$cw = self::getSensitive($activity, $url); $cw = self::getSensitive($activity, $url);
$pid = is_object($profile) ? $profile->id : (is_array($profile) ? $profile['id'] : null); $pid = is_object($profile) ? $profile->id : (is_array($profile) ? $profile['id'] : null);
$isUnlisted = is_object($profile) ? $profile->unlisted : (is_array($profile) ? $profile['unlisted'] : false); $isUnlisted = is_object($profile) ? $profile->unlisted : (is_array($profile) ? $profile['unlisted'] : false);
$commentsDisabled = isset($activity['commentsEnabled']) ? !boolval($activity['commentsEnabled']) : false; $commentsDisabled = isset($activity['commentsEnabled']) ? ! boolval($activity['commentsEnabled']) : false;
if(!$pid) { if (! $pid) {
return; return;
} }
if($scope == 'public') { if ($scope == 'public') {
if($isUnlisted == true) { if ($isUnlisted == true) {
$scope = 'unlisted'; $scope = 'unlisted';
} }
} }
$status = Status::updateOrCreate( $status = Status::updateOrCreate(
[ [
'uri' => $url 'uri' => $url,
], [ ], [
'profile_id' => $pid, 'profile_id' => $pid,
'url' => $url, 'url' => $url,
@ -527,24 +520,24 @@ class Helpers {
'visibility' => $scope, 'visibility' => $scope,
'cw_summary' => ($cw == true && isset($activity['summary']) ? 'cw_summary' => ($cw == true && isset($activity['summary']) ?
Purify::clean(strip_tags($activity['summary'])) : null), Purify::clean(strip_tags($activity['summary'])) : null),
'comments_disabled' => $commentsDisabled 'comments_disabled' => $commentsDisabled,
] ]
); );
if($reply_to == null) { if ($reply_to == null) {
self::importNoteAttachment($activity, $status); self::importNoteAttachment($activity, $status);
} else { } else {
if(isset($activity['attachment']) && !empty($activity['attachment'])) { if (isset($activity['attachment']) && ! empty($activity['attachment'])) {
self::importNoteAttachment($activity, $status); self::importNoteAttachment($activity, $status);
} }
StatusReplyPipeline::dispatch($status); StatusReplyPipeline::dispatch($status);
} }
if(isset($activity['tag']) && is_array($activity['tag']) && !empty($activity['tag'])) { if (isset($activity['tag']) && is_array($activity['tag']) && ! empty($activity['tag'])) {
StatusTagsPipeline::dispatch($activity, $status); StatusTagsPipeline::dispatch($activity, $status);
} }
if( config('instance.timeline.network.cached') && if (config('instance.timeline.network.cached') &&
$status->in_reply_to_id === null && $status->in_reply_to_id === null &&
$status->reblog_of_id === null && $status->reblog_of_id === null &&
in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) && in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) &&
@ -556,8 +549,8 @@ class Helpers {
->unique() ->unique()
->values() ->values()
->toArray(); ->toArray();
if(!in_array($urlDomain, $filteredDomains)) { if (! in_array($urlDomain, $filteredDomains)) {
if(!$isUnlisted) { if (! $isUnlisted) {
NetworkTimelineService::add($status->id); NetworkTimelineService::add($status->id);
} }
} }
@ -565,7 +558,7 @@ class Helpers {
AccountStatService::incrementPostCount($pid); AccountStatService::incrementPostCount($pid);
if( $status->in_reply_to_id === null && if ($status->in_reply_to_id === null &&
in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
) { ) {
FeedInsertRemotePipeline::dispatch($status->id, $pid)->onQueue('feed'); FeedInsertRemotePipeline::dispatch($status->id, $pid)->onQueue('feed');
@ -576,14 +569,14 @@ class Helpers {
public static function getSensitive($activity, $url) public static function getSensitive($activity, $url)
{ {
if(!$url || !strlen($url)) { if (! $url || ! strlen($url)) {
return true; return true;
} }
$urlDomain = parse_url($url, PHP_URL_HOST); $urlDomain = parse_url($url, PHP_URL_HOST);
$cw = isset($activity['sensitive']) ? (bool) $activity['sensitive'] : false; $cw = isset($activity['sensitive']) ? (bool) $activity['sensitive'] : false;
if(in_array($urlDomain, InstanceService::getNsfwDomains())) { if (in_array($urlDomain, InstanceService::getNsfwDomains())) {
$cw = true; $cw = true;
} }
@ -593,13 +586,13 @@ class Helpers {
public static function getReplyTo($activity) public static function getReplyTo($activity)
{ {
$reply_to = null; $reply_to = null;
$inReplyTo = isset($activity['inReplyTo']) && !empty($activity['inReplyTo']) ? $inReplyTo = isset($activity['inReplyTo']) && ! empty($activity['inReplyTo']) ?
self::pluckval($activity['inReplyTo']) : self::pluckval($activity['inReplyTo']) :
false; false;
if($inReplyTo) { if ($inReplyTo) {
$reply_to = self::statusFirstOrFetch($inReplyTo); $reply_to = self::statusFirstOrFetch($inReplyTo);
if($reply_to) { if ($reply_to) {
$reply_to = optional($reply_to)->id; $reply_to = optional($reply_to)->id;
} }
} else { } else {
@ -616,25 +609,25 @@ class Helpers {
$urlDomain = parse_url(self::pluckval($url), PHP_URL_HOST); $urlDomain = parse_url(self::pluckval($url), PHP_URL_HOST);
$scope = 'private'; $scope = 'private';
if(isset($activity['to']) == true) { if (isset($activity['to']) == true) {
if(is_array($activity['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $activity['to'])) { if (is_array($activity['to']) && in_array('https://www.w3.org/ns/activitystreams#Public', $activity['to'])) {
$scope = 'public'; $scope = 'public';
} }
if(is_string($activity['to']) && 'https://www.w3.org/ns/activitystreams#Public' == $activity['to']) { if (is_string($activity['to']) && $activity['to'] == 'https://www.w3.org/ns/activitystreams#Public') {
$scope = 'public'; $scope = 'public';
} }
} }
if(isset($activity['cc']) == true) { if (isset($activity['cc']) == true) {
if(is_array($activity['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $activity['cc'])) { if (is_array($activity['cc']) && in_array('https://www.w3.org/ns/activitystreams#Public', $activity['cc'])) {
$scope = 'unlisted'; $scope = 'unlisted';
} }
if(is_string($activity['cc']) && 'https://www.w3.org/ns/activitystreams#Public' == $activity['cc']) { if (is_string($activity['cc']) && $activity['cc'] == 'https://www.w3.org/ns/activitystreams#Public') {
$scope = 'unlisted'; $scope = 'unlisted';
} }
} }
if($scope == 'public' && in_array($urlDomain, InstanceService::getUnlistedDomains())) { if ($scope == 'public' && in_array($urlDomain, InstanceService::getUnlistedDomains())) {
$scope = 'unlisted'; $scope = 'unlisted';
} }
@ -643,15 +636,15 @@ class Helpers {
private static function storePoll($profile, $res, $url, $ts, $reply_to, $cw, $scope, $id) private static function storePoll($profile, $res, $url, $ts, $reply_to, $cw, $scope, $id)
{ {
if(!isset($res['endTime']) || !isset($res['oneOf']) || !is_array($res['oneOf']) || count($res['oneOf']) > 4) { if (! isset($res['endTime']) || ! isset($res['oneOf']) || ! is_array($res['oneOf']) || count($res['oneOf']) > 4) {
return; return;
} }
$options = collect($res['oneOf'])->map(function($option) { $options = collect($res['oneOf'])->map(function ($option) {
return $option['name']; return $option['name'];
})->toArray(); })->toArray();
$cachedTallies = collect($res['oneOf'])->map(function($option) { $cachedTallies = collect($res['oneOf'])->map(function ($option) {
return $option['replies']['totalItems'] ?? 0; return $option['replies']['totalItems'] ?? 0;
})->toArray(); })->toArray();
@ -697,9 +690,10 @@ class Helpers {
public static function importNoteAttachment($data, Status $status) public static function importNoteAttachment($data, Status $status)
{ {
if(self::verifyAttachments($data) == false) { if (self::verifyAttachments($data) == false) {
// \Log::info('importNoteAttachment::failedVerification.', [$data['id']]); // \Log::info('importNoteAttachment::failedVerification.', [$data['id']]);
$status->viewType(); $status->viewType();
return; return;
} }
$attachments = isset($data['object']) ? $data['object']['attachment'] : $data['attachment']; $attachments = isset($data['object']) ? $data['object']['attachment'] : $data['attachment'];
@ -712,11 +706,11 @@ class Helpers {
$storagePath = MediaPathService::get($user, 2); $storagePath = MediaPathService::get($user, 2);
$allowed = explode(',', config_cache('pixelfed.media_types')); $allowed = explode(',', config_cache('pixelfed.media_types'));
foreach($attachments as $key => $media) { foreach ($attachments as $key => $media) {
$type = $media['mediaType']; $type = $media['mediaType'];
$url = $media['url']; $url = $media['url'];
$valid = self::validateUrl($url); $valid = self::validateUrl($url);
if(in_array($type, $allowed) == false || $valid == false) { if (in_array($type, $allowed) == false || $valid == false) {
continue; continue;
} }
$blurhash = isset($media['blurhash']) ? $media['blurhash'] : null; $blurhash = isset($media['blurhash']) ? $media['blurhash'] : null;
@ -735,50 +729,52 @@ class Helpers {
$media->remote_url = $url; $media->remote_url = $url;
$media->caption = $caption; $media->caption = $caption;
$media->order = $key + 1; $media->order = $key + 1;
if($width) { if ($width) {
$media->width = $width; $media->width = $width;
} }
if($height) { if ($height) {
$media->height = $height; $media->height = $height;
} }
if($license) { if ($license) {
$media->license = $license; $media->license = $license;
} }
$media->mime = $type; $media->mime = $type;
$media->version = 3; $media->version = 3;
$media->save(); $media->save();
if(config_cache('pixelfed.cloud_storage') == true) { if ((bool) config_cache('pixelfed.cloud_storage') == true) {
MediaStoragePipeline::dispatch($media); MediaStoragePipeline::dispatch($media);
} }
} }
$status->viewType(); $status->viewType();
return;
} }
public static function profileFirstOrNew($url) public static function profileFirstOrNew($url)
{ {
$url = self::validateUrl($url); $url = self::validateUrl($url);
if($url == false) { if ($url == false) {
return; return;
} }
$host = parse_url($url, PHP_URL_HOST); $host = parse_url($url, PHP_URL_HOST);
$local = config('pixelfed.domain.app') == $host ? true : false; $local = config('pixelfed.domain.app') == $host ? true : false;
if($local == true) { if ($local == true) {
$id = last(explode('/', $url)); $id = last(explode('/', $url));
return Profile::whereNull('status') return Profile::whereNull('status')
->whereNull('domain') ->whereNull('domain')
->whereUsername($id) ->whereUsername($id)
->firstOrFail(); ->firstOrFail();
} }
if($profile = Profile::whereRemoteUrl($url)->first()) { if ($profile = Profile::whereRemoteUrl($url)->first()) {
if($profile->last_fetched_at && $profile->last_fetched_at->lt(now()->subHours(24))) { if ($profile->last_fetched_at && $profile->last_fetched_at->lt(now()->subHours(24))) {
return self::profileUpdateOrCreate($url); return self::profileUpdateOrCreate($url);
} }
return $profile; return $profile;
} }
@ -788,42 +784,42 @@ class Helpers {
public static function profileUpdateOrCreate($url) public static function profileUpdateOrCreate($url)
{ {
$res = self::fetchProfileFromUrl($url); $res = self::fetchProfileFromUrl($url);
if(!$res || isset($res['id']) == false) { if (! $res || isset($res['id']) == false) {
return; return;
} }
$urlDomain = parse_url($url, PHP_URL_HOST); $urlDomain = parse_url($url, PHP_URL_HOST);
$domain = parse_url($res['id'], PHP_URL_HOST); $domain = parse_url($res['id'], PHP_URL_HOST);
if(strtolower($urlDomain) !== strtolower($domain)) { if (strtolower($urlDomain) !== strtolower($domain)) {
return; return;
} }
if(!isset($res['preferredUsername']) && !isset($res['nickname'])) { if (! isset($res['preferredUsername']) && ! isset($res['nickname'])) {
return; return;
} }
// skip invalid usernames // skip invalid usernames
if(!ctype_alnum($res['preferredUsername'])) { if (! ctype_alnum($res['preferredUsername'])) {
$tmpUsername = str_replace(['_', '.', '-'], '', $res['preferredUsername']); $tmpUsername = str_replace(['_', '.', '-'], '', $res['preferredUsername']);
if(!ctype_alnum($tmpUsername)) { if (! ctype_alnum($tmpUsername)) {
return; return;
} }
} }
$username = (string) Purify::clean($res['preferredUsername'] ?? $res['nickname']); $username = (string) Purify::clean($res['preferredUsername'] ?? $res['nickname']);
if(empty($username)) { if (empty($username)) {
return; return;
} }
$remoteUsername = $username; $remoteUsername = $username;
$webfinger = "@{$username}@{$domain}"; $webfinger = "@{$username}@{$domain}";
if(!self::validateUrl($res['inbox'])) { if (! self::validateUrl($res['inbox'])) {
return; return;
} }
if(!self::validateUrl($res['id'])) { if (! self::validateUrl($res['id'])) {
return; return;
} }
$instance = Instance::updateOrCreate([ $instance = Instance::updateOrCreate([
'domain' => $domain 'domain' => $domain,
]); ]);
if($instance->wasRecentlyCreated == true) { if ($instance->wasRecentlyCreated == true) {
\App\Jobs\InstancePipeline\FetchNodeinfoPipeline::dispatch($instance)->onQueue('low'); \App\Jobs\InstancePipeline\FetchNodeinfoPipeline::dispatch($instance)->onQueue('low');
} }
@ -846,13 +842,14 @@ class Helpers {
] ]
); );
if( $profile->last_fetched_at == null || if ($profile->last_fetched_at == null ||
$profile->last_fetched_at->lt(now()->subMonths(3)) $profile->last_fetched_at->lt(now()->subMonths(3))
) { ) {
RemoteAvatarFetch::dispatch($profile); RemoteAvatarFetch::dispatch($profile);
} }
$profile->last_fetched_at = now(); $profile->last_fetched_at = now();
$profile->save(); $profile->save();
return $profile; return $profile;
} }
@ -863,7 +860,7 @@ class Helpers {
public static function sendSignedObject($profile, $url, $body) public static function sendSignedObject($profile, $url, $body)
{ {
if(app()->environment() !== 'production') { if (app()->environment() !== 'production') {
return; return;
} }
ActivityPubDeliveryService::queue() ActivityPubDeliveryService::queue()

View file

@ -2,34 +2,32 @@
namespace App\Util\ActivityPub; namespace App\Util\ActivityPub;
use App\Profile;
use App\Status;
use League\Fractal;
use App\Http\Controllers\ProfileController; use App\Http\Controllers\ProfileController;
use App\Transformer\ActivityPub\ProfileOutbox; use App\Status;
use App\Transformer\ActivityPub\Verb\CreateNote; use App\Transformer\ActivityPub\Verb\CreateNote;
use League\Fractal;
class Outbox { class Outbox
{
public static function get($profile)
{
abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
abort_if(! config('federation.activitypub.outbox'), 404);
public static function get($profile) if ($profile->status != null) {
{
abort_if(!config_cache('federation.activitypub.enabled'), 404);
abort_if(!config('federation.activitypub.outbox'), 404);
if($profile->status != null) {
return ProfileController::accountCheck($profile); return ProfileController::accountCheck($profile);
} }
if($profile->is_private) { if ($profile->is_private) {
return ['error'=>'403', 'msg' => 'private profile']; return ['error' => '403', 'msg' => 'private profile'];
} }
$timeline = $profile $timeline = $profile
->statuses() ->statuses()
->whereScope('public') ->whereScope('public')
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')
->take(10) ->take(10)
->get(); ->get();
$count = Status::whereProfileId($profile->id)->count(); $count = Status::whereProfileId($profile->id)->count();
@ -38,14 +36,14 @@ class Outbox {
$res = $fractal->createData($resource)->toArray(); $res = $fractal->createData($resource)->toArray();
$outbox = [ $outbox = [
'@context' => 'https://www.w3.org/ns/activitystreams', '@context' => 'https://www.w3.org/ns/activitystreams',
'_debug' => 'Outbox only supports latest 10 objects, pagination is not supported', '_debug' => 'Outbox only supports latest 10 objects, pagination is not supported',
'id' => $profile->permalink('/outbox'), 'id' => $profile->permalink('/outbox'),
'type' => 'OrderedCollection', 'type' => 'OrderedCollection',
'totalItems' => $count, 'totalItems' => $count,
'orderedItems' => $res['data'] 'orderedItems' => $res['data'],
]; ];
return $outbox;
}
return $outbox;
}
} }

View file

@ -30,16 +30,16 @@ class Config
'version' => config('pixelfed.version'), 'version' => config('pixelfed.version'),
'open_registration' => (bool) config_cache('pixelfed.open_registration'), 'open_registration' => (bool) config_cache('pixelfed.open_registration'),
'uploader' => [ 'uploader' => [
'max_photo_size' => (int) config('pixelfed.max_photo_size'), 'max_photo_size' => (int) config_cache('pixelfed.max_photo_size'),
'max_caption_length' => (int) config_cache('pixelfed.max_caption_length'), 'max_caption_length' => (int) config_cache('pixelfed.max_caption_length'),
'max_altext_length' => (int) config_cache('pixelfed.max_altext_length', 150), 'max_altext_length' => (int) config_cache('pixelfed.max_altext_length', 150),
'album_limit' => (int) config_cache('pixelfed.max_album_length'), 'album_limit' => (int) config_cache('pixelfed.max_album_length'),
'image_quality' => (int) config_cache('pixelfed.image_quality'), 'image_quality' => (int) config_cache('pixelfed.image_quality'),
'max_collection_length' => (int) config('pixelfed.max_collection_length', 18), 'max_collection_length' => (int) config_cache('pixelfed.max_collection_length', 18),
'optimize_image' => (bool) config('pixelfed.optimize_image'), 'optimize_image' => (bool) config_cache('pixelfed.optimize_image'),
'optimize_video' => (bool) config('pixelfed.optimize_video'), 'optimize_video' => (bool) config_cache('pixelfed.optimize_video'),
'media_types' => config_cache('pixelfed.media_types'), 'media_types' => config_cache('pixelfed.media_types'),
'mime_types' => config_cache('pixelfed.media_types') ? explode(',', config_cache('pixelfed.media_types')) : [], 'mime_types' => config_cache('pixelfed.media_types') ? explode(',', config_cache('pixelfed.media_types')) : [],

View file

@ -19,7 +19,7 @@ return new class extends Migration
public function up() public function up()
{ {
ini_set('memory_limit', '-1'); ini_set('memory_limit', '-1');
if(config_cache('pixelfed.cloud_storage') == false) { if((bool) config_cache('pixelfed.cloud_storage') == false) {
return; return;
} }

BIN
public/css/admin.css vendored

Binary file not shown.

BIN
public/css/spa.css vendored

Binary file not shown.

BIN
public/js/admin.js vendored

Binary file not shown.

BIN
public/js/manifest.js vendored

Binary file not shown.

Binary file not shown.

View file

@ -202,16 +202,12 @@
if(res.data && res.data.length) { if(res.data && res.data.length) {
this.feed = res.data; this.feed = res.data;
this.maxId = res.data[res.data.length - 1].id; this.maxId = res.data[res.data.length - 1].id;
return true; this.canLoadMore = true;
} else { } else {
this.feedLoaded = true; this.feedLoaded = true;
this.isLoaded = true; this.isLoaded = true;
return false;
} }
}) })
.then(res => {
this.canLoadMore = res;
})
.finally(() => { .finally(() => {
this.feedLoaded = true; this.feedLoaded = true;
this.isLoaded = true; this.isLoaded = true;
@ -242,14 +238,11 @@
if(res.data && res.data.length) { if(res.data && res.data.length) {
this.feed.push(...res.data); this.feed.push(...res.data);
this.maxId = res.data[res.data.length - 1].id; this.maxId = res.data[res.data.length - 1].id;
return true; this.canLoadMore = true;
} else { } else {
return false; this.canLoadMore = false;
} }
}) })
.then(res => {
this.canLoadMore = res;
})
.finally(() => { .finally(() => {
this.isIntersecting = false; this.isIntersecting = false;
}) })

View file

@ -251,10 +251,11 @@
</div> </div>
<div> <div>
<b-button <b-button
variant="secondary" variant="link-dark"
@click="showInstanceModal = false" size="sm"
@click="onViewMoreInstance"
> >
Close View More
</b-button> </b-button>
<b-button <b-button
variant="primary" variant="primary"
@ -885,8 +886,12 @@
}; };
} }
} }
} },
onViewMoreInstance() {
this.showInstanceModal = false;
window.location.href = '/i/admin/instances/show/' + this.instanceModal.id
}
} }
} }
</script> </script>

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<div class="mb-0" :style="{ 'font-size':`${fontSize}px` }">{{ contentText }}</div> <div class="mb-0" :style="{ 'font-size':`${fontSize}px` }">{{ contentText }}</div>
<p class="mb-0"><a v-if="canStepExpand || (canExpand && !expanded)" class="font-weight-bold small" href="#" @click="expand()">Read more</a></p> <p class="mb-0"><a v-if="canStepExpand || (canExpand && !expanded)" class="font-weight-bold small" href="#" @click.prevent="expand()">Read more</a></p>
</div> </div>
</template> </template>

View file

@ -0,0 +1,42 @@
<template>
<div class="card shadow-none border card-body">
<div class="form-group mb-0">
<div class="custom-control custom-checkbox">
<input type="checkbox" :name="elementId" class="custom-control-input" :id="elementId" :checked="value" @change="$emit('change', !value)">
<label class="custom-control-label font-weight-bold" :for="elementId">{{ name }}</label>
</div>
<p class="mt-1 mb-0 small text-muted" v-html="description"></p>
</div>
</div>
</template>
<script>
export default {
props: {
name: {
type: String
},
value: {
type: Boolean
},
description: {
type: String
}
},
computed: {
elementId: {
get() {
let name = this.name;
name = name.toLowerCase();
name = name.replace(/[^a-z0-9 -]/g, ' ');
name = name.replace(/\s+/g, '-');
name = name.replace(/^-+|-+$/g, '');
return 'fec_' + name;
}
}
}
}
</script>

View file

@ -0,0 +1,74 @@
<template>
<div :class="[ isCard ? 'card shadow-none border card-body' : '' ]">
<div
class="form-group"
:class="[ isInline ? 'd-flex align-items-center gap-1' : 'mb-1' ]">
<label :for="elementId" class="font-weight-bold mb-0">{{ name }}</label>
<input
:id="elementId"
class="form-control form-control-muted mb-0"
:placeholder="placeholder"
:value="value"
@input="$emit('change', $event.target.value)"
:disabled="isDisabled" />
</div>
<p v-if="description && description.length" class="help-text small text-muted mb-0" v-html="description">
</p>
</div>
</template>
<script>
export default {
props: {
name: {
type: String
},
value: {
type: String
},
placeholder: {
type: String
},
description: {
type: String
},
isCard: {
type: Boolean,
default: true
},
isInline: {
type: Boolean,
default: false
},
isDisabled: {
type: Boolean,
default: false
}
},
computed: {
elementId: {
get() {
let name = this.name;
name = name.toLowerCase();
name = name.replace(/[^a-z0-9 -]/g, ' ');
name = name.replace(/\s+/g, '-');
name = name.replace(/^-+|-+$/g, '');
return 'fec_' + name;
}
}
}
}
</script>
<style lang="scss" scoped="true">
.gap-1 {
gap: 1rem;
}
</style>

View file

@ -0,0 +1,63 @@
<template>
<div>
<div class="d-flex justify-content-between align-items-center">
<div style="width:100px;"></div>
<div>
<h2 class="display-4 mb-0" style="font-weight: 800;">{{ title }}</h2>
</div>
<div>
<button
class="btn btn-primary rounded-pill font-weight-bold px-5"
:disabled="isSaving || saved"
@click.prevent="save">
<template v-if="isSaving === true"><b-spinner small class="mx-2" /></template>
<template v-else>{{ buttonLabel }}</template>
</button>
</div>
</div>
<hr class="mt-3">
</div>
</template>
<script>
export default {
props: {
title: {
type: String
},
saving: {
type: Boolean
},
saved: {
type: Boolean
}
},
computed: {
buttonLabel: {
get() {
if(this.saved) {
return 'Saved';
}
if(this.saving) {
return 'Saving';
}
return 'Save';
}
},
isSaving: {
get() {
return this.saving;
}
}
},
methods: {
save($event) {
$event.currentTarget?.blur();
this.$emit('save');
}
}
}
</script>

View file

@ -36,11 +36,21 @@ Vue.component(
require('./../components/admin/AdminReports.vue').default require('./../components/admin/AdminReports.vue').default
); );
Vue.component(
'admin-settings',
require('./../components/admin/AdminSettings.vue').default
);
Vue.component( Vue.component(
'instances-component', 'instances-component',
require('./../components/admin/AdminInstances.vue').default require('./../components/admin/AdminInstances.vue').default
); );
// Vue.component(
// 'instance-details-component',
// require('./../components/admin/AdminInstanceDetails.vue').default
// );
Vue.component( Vue.component(
'hashtag-component', 'hashtag-component',
require('./../components/admin/AdminHashtags.vue').default require('./../components/admin/AdminHashtags.vue').default

View file

@ -10,6 +10,7 @@ License - nucleoapp.com/license/
src: url('/fonts/nucleo-icons.eot') format('embedded-opentype'), url('/fonts/nucleo-icons.woff2') format('woff2'), url('/fonts/nucleo-icons.woff') format('woff'), url('/fonts/nucleo-icons.ttf') format('truetype'), url('/fonts/nucleo-icons.svg') format('svg'); src: url('/fonts/nucleo-icons.eot') format('embedded-opentype'), url('/fonts/nucleo-icons.woff2') format('woff2'), url('/fonts/nucleo-icons.woff') format('woff'), url('/fonts/nucleo-icons.ttf') format('truetype'), url('/fonts/nucleo-icons.svg') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-display: swap;
} }
/*------------------------ /*------------------------
base class definition base class definition

View file

@ -392,6 +392,10 @@ span.twitter-typeahead .tt-suggestion:focus {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
} }
.timestamp-overlay-badge {
color: var(--dark);
}
.timeline-status-component { .timeline-status-component {
.username { .username {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

View file

@ -69,7 +69,7 @@
<div class="col-12 col-md-6 offset-md-3 my-3"> <div class="col-12 col-md-6 offset-md-3 my-3">
<div class="border rounded p-3 border-primary"> <div class="border rounded p-3 border-primary">
<p class="h4 font-weight-bold pt-2 text-primary">Review the Community Guidelines</p> <p class="h4 font-weight-bold pt-2 text-primary">Review the Community Guidelines</p>
<p class="lead pt-4 text-primary">We want to keep {{config('app.name')}} a safe place for everyone, and we created these <a class="font-weight-bold text-primary" href="{{route('help.community-guidelines')}}">Community Guidelines</a> to support and protect our community.</p> <p class="lead pt-4 text-primary">We want to keep {{config_cache('app.name')}} a safe place for everyone, and we created these <a class="font-weight-bold text-primary" href="{{route('help.community-guidelines')}}">Community Guidelines</a> to support and protect our community.</p>
</div> </div>
</div> </div>
@ -100,4 +100,4 @@
ctx.putImageData(imageData, 0, 0); ctx.putImageData(imageData, 0, 0);
</script> </script>
@endif @endif
@endpush @endpush

View file

@ -70,7 +70,7 @@
<div class="col-12 col-md-6 offset-md-3 my-3"> <div class="col-12 col-md-6 offset-md-3 my-3">
<div class="border rounded p-3 border-primary"> <div class="border rounded p-3 border-primary">
<p class="h4 font-weight-bold pt-2 text-primary">Review the Community Guidelines</p> <p class="h4 font-weight-bold pt-2 text-primary">Review the Community Guidelines</p>
<p class="lead pt-4 text-primary">We want to keep {{config('app.name')}} a safe place for everyone, and we created these <a class="font-weight-bold text-primary" href="{{route('help.community-guidelines')}}">Community Guidelines</a> to support and protect our community.</p> <p class="lead pt-4 text-primary">We want to keep {{config_cache('app.name')}} a safe place for everyone, and we created these <a class="font-weight-bold text-primary" href="{{route('help.community-guidelines')}}">Community Guidelines</a> to support and protect our community.</p>
</div> </div>
</div> </div>
@ -127,4 +127,4 @@
ctx.putImageData(imageData, 0, 0); ctx.putImageData(imageData, 0, 0);
</script> </script>
@endif @endif
@endpush @endpush

View file

@ -62,7 +62,7 @@
<div class="col-12 col-md-6 offset-md-3 my-3"> <div class="col-12 col-md-6 offset-md-3 my-3">
<div class="border rounded p-3 border-primary"> <div class="border rounded p-3 border-primary">
<p class="h4 font-weight-bold pt-2 text-primary">Review the Community Guidelines</p> <p class="h4 font-weight-bold pt-2 text-primary">Review the Community Guidelines</p>
<p class="lead pt-4 text-primary">We want to keep {{config('app.name')}} a safe place for everyone, and we created these <a class="font-weight-bold text-primary" href="{{route('help.community-guidelines')}}">Community Guidelines</a> to support and protect our community.</p> <p class="lead pt-4 text-primary">We want to keep {{config_cache('app.name')}} a safe place for everyone, and we created these <a class="font-weight-bold text-primary" href="{{route('help.community-guidelines')}}">Community Guidelines</a> to support and protect our community.</p>
</div> </div>
</div> </div>
<div class="col-12 col-md-6 offset-md-3 mt-4 mb-5"> <div class="col-12 col-md-6 offset-md-3 mt-4 mb-5">
@ -96,4 +96,4 @@
ctx.putImageData(imageData, 0, 0); ctx.putImageData(imageData, 0, 0);
</script> </script>
@endif @endif
@endpush @endpush

View file

@ -69,7 +69,7 @@
<div class="col-12 col-md-6 offset-md-3 my-3"> <div class="col-12 col-md-6 offset-md-3 my-3">
<div class="border rounded p-3 border-primary"> <div class="border rounded p-3 border-primary">
<p class="h4 font-weight-bold pt-2 text-primary">Review the Community Guidelines</p> <p class="h4 font-weight-bold pt-2 text-primary">Review the Community Guidelines</p>
<p class="lead pt-4 text-primary">We want to keep {{config('app.name')}} a safe place for everyone, and we created these <a class="font-weight-bold text-primary" href="{{route('help.community-guidelines')}}">Community Guidelines</a> to support and protect our community.</p> <p class="lead pt-4 text-primary">We want to keep {{config_cache('app.name')}} a safe place for everyone, and we created these <a class="font-weight-bold text-primary" href="{{route('help.community-guidelines')}}">Community Guidelines</a> to support and protect our community.</p>
</div> </div>
</div> </div>
@ -125,4 +125,4 @@
ctx.putImageData(imageData, 0, 0); ctx.putImageData(imageData, 0, 0);
</script> </script>
@endif @endif
@endpush @endpush

View file

@ -66,7 +66,7 @@
</li> </li>
<li> <li>
<strong><span class="badge badge-primary">OAUTH</span> enabled: </strong> <strong><span class="badge badge-primary">OAUTH</span> enabled: </strong>
<span>{{ config_cache('pixelfed.oauth_enabled') ? '✅ true' : '❌ false' }}</span> <span>{{ (bool) config_cache('pixelfed.oauth_enabled') ? '✅ true' : '❌ false' }}</span>
</li> </li>
<li> <li>
<strong><span class="badge badge-primary">OAUTH</span> token_expiration</strong> <strong><span class="badge badge-primary">OAUTH</span> token_expiration</strong>
@ -298,7 +298,7 @@
<tr> <tr>
<td><span class="badge badge-primary">FEDERATION</span></td> <td><span class="badge badge-primary">FEDERATION</span></td>
<td><strong>ACTIVITY_PUB</strong></td> <td><strong>ACTIVITY_PUB</strong></td>
<td><span>{{config_cache('federation.activitypub.enabled') ? '✅ true' : '❌ false' }}</span></td> <td><span>{{(bool) config_cache('federation.activitypub.enabled') ? '✅ true' : '❌ false' }}</span></td>
</tr> </tr>
<tr> <tr>
<td><span class="badge badge-primary">FEDERATION</span></td> <td><span class="badge badge-primary">FEDERATION</span></td>
@ -358,7 +358,7 @@
<tr> <tr>
<td><span class="badge badge-primary">FEDERATION</span></td> <td><span class="badge badge-primary">FEDERATION</span></td>
<td><strong>PF_NETWORK_TIMELINE</strong></td> <td><strong>PF_NETWORK_TIMELINE</strong></td>
<td><span>{{config_cache('federation.network_timeline') ? '✅ true' : '❌ false' }}</span></td> <td><span>{{(bool) config_cache('federation.network_timeline') ? '✅ true' : '❌ false' }}</span></td>
</tr> </tr>
<tr> <tr>
<td><span class="badge badge-primary">FEDERATION</span></td> <td><span class="badge badge-primary">FEDERATION</span></td>
@ -368,7 +368,7 @@
<tr> <tr>
<td><span class="badge badge-primary">FEDERATION</span></td> <td><span class="badge badge-primary">FEDERATION</span></td>
<td><strong>CUSTOM_EMOJI</strong></td> <td><strong>CUSTOM_EMOJI</strong></td>
<td><span>{{config_cache('federation.custom_emoji.enabled') ? '✅ true' : '❌ false' }}</span></td> <td><span>{{(bool) config_cache('federation.custom_emoji.enabled') ? '✅ true' : '❌ false' }}</span></td>
</tr> </tr>
<tr> <tr>
<td><span class="badge badge-primary">FEDERATION</span></td> <td><span class="badge badge-primary">FEDERATION</span></td>
@ -545,7 +545,7 @@
<tr> <tr>
<td><span class="badge badge-primary">INSTANCE</span></td> <td><span class="badge badge-primary">INSTANCE</span></td>
<td><strong>STORIES_ENABLED</strong></td> <td><strong>STORIES_ENABLED</strong></td>
<td><span>{{config_cache('instance.stories.enabled') ? '✅ true' : '❌ false' }}</span></td> <td><span>{{(bool) config_cache('instance.stories.enabled') ? '✅ true' : '❌ false' }}</span></td>
</tr> </tr>
<tr> <tr>
<td><span class="badge badge-primary">INSTANCE</span></td> <td><span class="badge badge-primary">INSTANCE</span></td>
@ -740,7 +740,7 @@
<tr> <tr>
<td><span class="badge badge-primary">PIXELFED</span></td> <td><span class="badge badge-primary">PIXELFED</span></td>
<td><strong>PF_ENABLE_CLOUD</strong></td> <td><strong>PF_ENABLE_CLOUD</strong></td>
<td><span>{{config_cache('pixelfed.cloud_storage') ? '✅ true' : '❌ false' }}</span></td> <td><span>{{(bool) config_cache('pixelfed.cloud_storage') ? '✅ true' : '❌ false' }}</span></td>
</tr> </tr>
<tr> <tr>
<td><span class="badge badge-primary">PIXELFED</span></td> <td><span class="badge badge-primary">PIXELFED</span></td>
@ -750,12 +750,12 @@
<tr> <tr>
<td><span class="badge badge-primary">PIXELFED</span></td> <td><span class="badge badge-primary">PIXELFED</span></td>
<td><strong>PF_OPTIMIZE_IMAGES</strong></td> <td><strong>PF_OPTIMIZE_IMAGES</strong></td>
<td><span>{{config_cache('pixelfed.optimize_image') ? '✅ true' : '❌ false' }}</span></td> <td><span>{{(bool) config_cache('pixelfed.optimize_image') ? '✅ true' : '❌ false' }}</span></td>
</tr> </tr>
<tr> <tr>
<td><span class="badge badge-primary">PIXELFED</span></td> <td><span class="badge badge-primary">PIXELFED</span></td>
<td><strong>PF_OPTIMIZE_VIDEOS</strong></td> <td><strong>PF_OPTIMIZE_VIDEOS</strong></td>
<td><span>{{config_cache('pixelfed.optimize_video') ? '✅ true' : '❌ false' }}</span></td> <td><span>{{(bool) config_cache('pixelfed.optimize_video') ? '✅ true' : '❌ false' }}</span></td>
</tr> </tr>
<tr> <tr>
<td><span class="badge badge-primary">PIXELFED</span></td> <td><span class="badge badge-primary">PIXELFED</span></td>
@ -810,12 +810,12 @@
<tr> <tr>
<td><span class="badge badge-primary">PIXELFED</span></td> <td><span class="badge badge-primary">PIXELFED</span></td>
<td><strong>OAUTH_ENABLED</strong></td> <td><strong>OAUTH_ENABLED</strong></td>
<td><span>{{config_cache('pixelfed.oauth_enabled') ? '✅ true' : '❌ false' }}</span></td> <td><span>{{ (bool) config_cache('pixelfed.oauth_enabled') ? '✅ true' : '❌ false' }}</span></td>
</tr> </tr>
<tr> <tr>
<td><span class="badge badge-primary">PIXELFED</span></td> <td><span class="badge badge-primary">PIXELFED</span></td>
<td><strong>PF_BOUNCER_ENABLED</strong></td> <td><strong>PF_BOUNCER_ENABLED</strong></td>
<td><span>{{config_cache('pixelfed.bouncer.enabled') ? '✅ true' : '❌ false' }}</span></td> <td><span>{{(bool) config_cache('pixelfed.bouncer.enabled') ? '✅ true' : '❌ false' }}</span></td>
</tr> </tr>
<tr> <tr>
<td><span class="badge badge-primary">PIXELFED</span></td> <td><span class="badge badge-primary">PIXELFED</span></td>

View file

@ -1,421 +1,12 @@
@extends('admin.partial.template-full') @extends('admin.partial.template-full')
@section('section') @section('section')
<div class="title mb-4">
<h3 class="font-weight-bold">Settings</h3>
@if(config('instance.enable_cc'))
<p class="lead mb-0">Manage instance settings</p>
</div> </div>
<form method="post"> <admin-settings />
@csrf
<ul class="nav nav-tabs nav-fill border-bottom-0" id="myTab" role="tablist">
<li class="nav-item">
<a class="nav-link font-weight-bold active" id="home-tab" data-toggle="tab" href="#home" role="tab" aria-controls="home" aria-selected="true"><i class="fas fa-home"></i></a>
</li>
<li class="nav-item border-none">
<a class="nav-link font-weight-bold px-4" id="landing-tab" data-toggle="tab" href="#landing" role="tab" aria-controls="landing">Landing</a>
</li>
<li class="nav-item border-none">
<a class="nav-link font-weight-bold px-4" id="brand-tab" data-toggle="tab" href="#brand" role="tab" aria-controls="brand">Brand</a>
</li>
{{-- <li class="nav-item border-none">
<a class="nav-link font-weight-bold px-4" id="media-tab" data-toggle="tab" href="#media" role="tab" aria-controls="media">Mail</a>
</li> --}}
<li class="nav-item border-none">
<a class="nav-link font-weight-bold px-4" id="media-tab" data-toggle="tab" href="#media" role="tab" aria-controls="media">Media</a>
</li>
<li class="nav-item border-none">
<a class="nav-link font-weight-bold px-4" id="rules-tab" data-toggle="tab" href="#rules" role="tab" aria-controls="rules">Rules</a>
</li>
<li class="nav-item border-none">
<a class="nav-link font-weight-bold px-4" id="users-tab" data-toggle="tab" href="#users" role="tab" aria-controls="users">Users</a>
</li>
<li class="nav-item">
<a class="nav-link font-weight-bold px-4" id="advanced-tab" data-toggle="tab" href="#advanced" role="tab" aria-controls="advanced">Advanced</a>
</li>
</ul>
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">
{{-- <div class="ml-n4 mr-n2 p-3 border-top border-bottom">
<label class="font-weight-bold text-muted">System Configuration</label>
<ul class="list-unstyled">
<li>
<span class="text-muted">Max Upload Size: </span>
<span class="font-weight-bold">{{$system['max_upload_size']}}</span>
</li>
<li>
<span class="text-muted">Image Driver: </span>
<span class="font-weight-bold">{{$system['image_driver']}}</span>
</li>
<li>
<span class="text-muted">Image Driver Loaded: </span>
<span class="font-weight-bold">
@if($system['image_driver_loaded'])
<i class="fas fa-check text-success"></i>
@else
<i class="fas fa-times text-danger"></i>
@endif
</span>
</li>
<li>
<span class="text-muted">File Permissions: </span>
<span class="font-weight-bold">
@if($system['permissions'])
<i class="fas fa-check text-success"></i>
@else
<i class="fas fa-times text-danger"></i>
@endif
</span>
</li>
<li>
<span class="text-muted"></span>
<span class="font-weight-bold"></span>
</li>
</ul>
</div> --}}
<div class="form-group mb-0">
<div class="ml-n4 mr-n2 p-3 border-top border-bottom">
<label class="font-weight-bold text-muted">Features</label>
<div class="form-group row mb-5">
<label for="staticEmail" class="col-sm-12 col-form-label font-weight-bold">Registration Status</label>
<div class="col-sm-4">
<select class="custom-select" name="regs">
<option value="open" {{ $regState === 'open' ? 'selected' : '' }}>Open - Anyone can register</option>
<option value="filtered" {{ $regState === 'filtered' ? 'selected' : '' }}>Filtered - Anyone can apply (Curated Onboarding)</option>
<option value="closed" {{ $regState === 'closed' ? 'selected' : '' }}>Closed - Nobody can register</option>
</select>
</div>
</div>
@if($cloud_ready)
<div class="custom-control custom-checkbox mt-2">
<input type="checkbox" name="cloud_storage" class="custom-control-input" id="cls1" {{config_cache('pixelfed.cloud_storage') ? 'checked' : ''}}>
<label class="custom-control-label font-weight-bold" for="cls1">Cloud Storage</label>
</div>
<p class="mb-4 small">Store photos &amp; videos on S3 compatible object storage providers.</p>
@endif
<div class="custom-control custom-checkbox mt-2">
<input type="checkbox" name="activitypub" class="custom-control-input" id="ap" {{config_cache('federation.activitypub.enabled') ? 'checked' : ''}}>
<label class="custom-control-label font-weight-bold" for="ap">ActivityPub</label>
</div>
<p class="mb-4 small">ActivityPub federation, compatible with Pixelfed, Mastodon and other projects.</p>
<div class="custom-control custom-checkbox mt-2">
<input type="checkbox" name="account_migration" class="custom-control-input" id="ap_mig" {{(bool)config_cache('federation.migration') ? 'checked' : ''}} {{(bool) config_cache('federation.activitypub.enabled') ? '' : 'disabled="disabled"'}}>
<label class="custom-control-label font-weight-bold" for="ap_mig">Account Migration</label>
</div>
@if((bool) config_cache('federation.activitypub.enabled'))
<p class="mb-4 small">Allow local accounts to migrate to other local or remote accounts.</p>
@else
<p class="mb-4 small text-muted"><strong>ActivityPub Required</strong> Allow local accounts to migrate to other local or remote accounts.</p>
@endif
{{-- <div class="custom-control custom-checkbox mt-2">
<input type="checkbox" name="open_registration" class="custom-control-input" id="openReg" {{config_cache('pixelfed.open_registration') ? 'checked' : ''}}>
<label class="custom-control-label font-weight-bold" for="openReg">Open Registrations</label>
</div>
<p class="mb-4 small">Allow new user registrations.</p> --}}
{{-- <div class="custom-control custom-checkbox mt-2">
<input type="checkbox" name="registration_approvals" class="custom-control-input" id="openRegApproval" {{config_cache('pixelfed.registration_approvals') ? 'checked' : ''}}>
<label class="custom-control-label font-weight-bold" for="openRegApproval">Registration Approval Mode</label>
</div>
<p class="mb-4 small">Manually review new account registration applications.</p> --}}
<div class="custom-control custom-checkbox mt-2">
<input type="checkbox" name="mobile_apis" class="custom-control-input" id="cf2" {{config_cache('pixelfed.oauth_enabled') ? 'checked' : ''}}>
<label class="custom-control-label font-weight-bold" for="cf2">Mobile APIs</label>
</div>
<p class="mb-4 small">Enable apis required for mobile app support.</p>
<div class="custom-control custom-checkbox mt-2">
<input type="checkbox" name="stories" class="custom-control-input" id="cf3" {{config_cache('instance.stories.enabled') ? 'checked' : ''}}>
<label class="custom-control-label font-weight-bold" for="cf3">Stories</label>
</div>
<p class="mb-4 small">Allow users to share ephemeral Stories.</p>
<div class="custom-control custom-checkbox mt-2">
<input type="checkbox" name="ig_import" class="custom-control-input" id="cf4" {{config_cache('pixelfed.import.instagram.enabled') ? 'checked' : ''}}>
<label class="custom-control-label font-weight-bold" for="cf4">Instagram Import</label>
</div>
<p class="mb-4 small">Allow <span class="font-weight-bold">experimental</span> Instagram Import support.</p>
<div class="custom-control custom-checkbox mt-2">
<input type="checkbox" name="spam_detection" class="custom-control-input" id="cf5" {{config_cache('pixelfed.bouncer.enabled') ? 'checked' : ''}}>
<label class="custom-control-label font-weight-bold" for="cf5">Spam detection</label>
</div>
<p class="mb-4 small">Detect and remove spam from timelines.</p>
</div>
</div>
{{-- <div class="form-group mb-0">
<div class="ml-n4 mr-n2 p-3 border-top border-bottom">
<label class="font-weight-bold text-muted">Name</label>
<input class="form-control col-8" name="name" placeholder="Pixelfed" value="{{config_cache('app.name')}}">
<p class="help-text small text-muted mt-3 mb-0">The instance name used in titles, metadata and apis.</p>
</div>
</div>
<div class="form-group mb-0">
<div class="ml-n4 mr-n2 p-3 border-bottom">
<label class="font-weight-bold text-muted">Short Description</label>
<textarea class="form-control" rows="3" name="short_description">{{config_cache('app.short_description')}}</textarea>
<p class="help-text small text-muted mt-3 mb-0">Short description of instance used on various pages and apis.</p>
</div>
</div>
<div class="form-group mb-0">
<div class="ml-n4 mr-n2 p-3 border-bottom">
<label class="font-weight-bold text-muted">Long Description</label>
<textarea class="form-control" rows="3" name="long_description">{{config_cache('app.description')}}</textarea>
<p class="help-text small text-muted mt-3 mb-0">Longer description of instance used on about page.</p>
</div>
</div> --}}
</div>
<div class="tab-pane" id="landing" role="tabpanel" aria-labelledby="landing-tab">
<div class="form-group mb-0">
<div class="ml-n4 mr-n2 p-3 border-top border-bottom">
<p class="mb-0 small">Configure your landing page</p>
</div>
</div>
<div class="form-group mb-0">
<div class="ml-n4 mr-n2 p-3 border-bottom">
<p class="font-weight-bold text-muted">Discovery</p>
<div class="my-3">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="show_directory" name="show_directory" {{ config_cache('instance.landing.show_directory') ? 'checked' : ''}}>
<label class="custom-control-label font-weight-bold" for="show_directory">Show Directory</label>
</div>
</div>
<div class="my-3">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="show_explore_feed" name="show_explore_feed" {{ config_cache('instance.landing.show_explore') ? 'checked' : ''}}>
<label class="custom-control-label font-weight-bold" for="show_explore_feed">Show Explore Feed</label>
</div>
</div>
</div>
</div>
<div class="form-group mb-0">
<div class="ml-n4 mr-n2 p-3 border-bottom">
<p class="font-weight-bold text-muted">Admin Account</p>
<div class="my-3">
<select class="custom-select" name="admin_account_id" style="max-width: 300px;">
<option selected disabled>Select an admin account</option>
@foreach($availableAdmins as $acct)
<option
value="{{ $acct->profile_id }}" {!! $currentAdmin && $currentAdmin['id'] == $acct->profile_id ? 'selected' : null !!}
>
<span class="font-weight-bold">&commat;{{ $acct->username }}</span>
</option>
@endforeach
</select>
</div>
</div>
</div>
</div>
<div class="tab-pane" id="brand" role="tabpanel" aria-labelledby="brand-tab">
<div class="form-group mb-0">
<div class="ml-n4 mr-n2 p-3 border-top border-bottom">
<label class="font-weight-bold text-muted">Name</label>
<input class="form-control col-8" name="name" placeholder="Pixelfed" value="{{config_cache('app.name')}}">
<p class="help-text small text-muted mt-3 mb-0">The instance name used in titles, metadata and apis.</p>
</div>
</div>
<div class="form-group mb-0">
<div class="ml-n4 mr-n2 p-3 border-bottom">
<label class="font-weight-bold text-muted">Short Description</label>
<textarea class="form-control" rows="3" name="short_description">{{config_cache('app.short_description')}}</textarea>
<p class="help-text small text-muted mt-3 mb-0">Short description of instance used on various pages and apis.</p>
</div>
</div>
<div class="form-group mb-0">
<div class="ml-n4 mr-n2 p-3 border-bottom">
<label class="font-weight-bold text-muted">Long Description</label>
<textarea class="form-control" rows="3" name="long_description">{{config_cache('app.description')}}</textarea>
<p class="help-text small text-muted mt-3 mb-0">Longer description of instance used on about page.</p>
</div>
</div>
<div class="form-group mb-0">
<div class="ml-n4 mr-n2 p-3 border-top border-bottom">
<label class="font-weight-bold text-muted">About Title</label>
<input class="form-control col-8" name="about_title" placeholder="Photo Sharing. For Everyone" value="{{config_cache('about.title')}}">
<p class="help-text small text-muted mt-3 mb-0">The header title used on the <a href="/site/about">about page</a>.</p>
</div>
</div>
</div>
<div class="tab-pane" id="users" role="tabpanel" aria-labelledby="users-tab">
<div class="form-group mb-0">
<div class="ml-n4 mr-n2 p-3 border-top">
<div class="custom-control custom-checkbox mt-2">
<input type="checkbox" name="require_email_verification" class="custom-control-input" id="mailVerification" {{config_cache('pixelfed.enforce_email_verification') ? 'checked' : ''}}>
<label class="custom-control-label font-weight-bold" for="mailVerification">Require Email Verification</label>
</div>
</div>
</div>
<div class="form-group">
<div class="ml-n4 mr-n2 p-3 border-top">
<div class="custom-control custom-checkbox my-2">
<input type="checkbox" name="enforce_account_limit" class="custom-control-input" id="userEnforceLimit" {{config_cache('pixelfed.enforce_account_limit') ? 'checked' : ''}}>
<label class="custom-control-label font-weight-bold" for="userEnforceLimit">Enable account storage limit</label>
<p class="help-text small text-muted">Set a storage limit per user account.</p>
</div>
<label class="font-weight-bold text-muted">Account Limit</label>
<input class="form-control" name="account_limit" placeholder="Pixelfed" value="{{config_cache('pixelfed.max_account_size')}}">
<p class="help-text small text-muted mt-3 mb-0">Account limit size in KB.</p>
<p class="help-text small text-muted mb-0">{{config_cache('pixelfed.max_account_size')}} KB = {{floor(config_cache('pixelfed.max_account_size') / 1024)}} MB</p>
</div>
</div>
<div class="form-group">
<div class="ml-n4 mr-n2 p-3 border-top">
<div class="custom-control custom-checkbox my-2">
<input type="checkbox" name="account_autofollow" class="custom-control-input" id="userAccountAutofollow" {{config_cache('account.autofollow') ? 'checked' : ''}}>
<label class="custom-control-label font-weight-bold" for="userAccountAutofollow">Auto Follow Accounts</label>
<p class="help-text small text-muted">Enable auto follow accounts, new accounts will follow accounts you set.</p>
</div>
<label class="font-weight-bold text-muted">Accounts</label>
<textarea class="form-control" name="account_autofollow_usernames" placeholder="Add account usernames to follow separated by commas">{{config_cache('account.autofollow_usernames')}}</textarea>
<p class="help-text small text-muted mt-3 mb-0">Add account usernames to follow separated by commas.</p>
</div>
</div>
</div>
<div class="tab-pane" id="media" role="tabpanel" aria-labelledby="media-tab">
<div class="form-group mb-0">
<div class="ml-n4 mr-n2 p-3 border-top">
<label class="font-weight-bold text-muted">Max Size</label>
<input class="form-control" name="max_photo_size" value="{{config_cache('pixelfed.max_photo_size')}}">
<p class="help-text small text-muted mt-3 mb-0">Maximum file upload size in KB</p>
<p class="help-text small text-muted mb-0">{{config_cache('pixelfed.max_photo_size')}} KB = {{number_format(config_cache('pixelfed.max_photo_size') / 1024)}} MB</p>
</div>
</div>
<div class="form-group mb-0">
<div class="ml-n4 mr-n2 p-3 border-top">
<label class="font-weight-bold text-muted">Photo Album Limit</label>
<input class="form-control" name="max_album_length" value="{{config_cache('pixelfed.max_album_length')}}">
<p class="help-text small text-muted mt-3 mb-0">The maximum number of photos or videos per album</p>
</div>
</div>
<div class="form-group mb-0">
<div class="ml-n4 mr-n2 p-3 border-top">
<label class="font-weight-bold text-muted">Image Quality</label>
<input class="form-control" name="image_quality" value="{{config_cache('pixelfed.image_quality')}}">
<p class="help-text small text-muted mt-3 mb-0">Image optimization quality from 0-100%. Set to 0 to disable image optimization.</p>
</div>
</div>
<div class="form-group">
<div class="ml-n4 mr-n2 p-3 border-top border-bottom">
<label class="font-weight-bold text-muted">Media Types</label>
<div class="custom-control custom-checkbox mt-2">
<input type="checkbox" name="type_jpeg" class="custom-control-input" id="mediaType1" {{$jpeg ? 'checked' : ''}}>
<label class="custom-control-label" for="mediaType1"><span class="border border-dark px-1 rounded font-weight-bold">JPEG</span></label>
</div>
<div class="custom-control custom-checkbox mt-2">
<input type="checkbox" name="type_png" class="custom-control-input" id="mediaType2" {{$png ? 'checked' : ''}}>
<label class="custom-control-label" for="mediaType2"><span class="border border-dark px-1 rounded font-weight-bold">PNG</span></label>
</div>
<div class="custom-control custom-checkbox mt-2">
<input type="checkbox" name="type_gif" class="custom-control-input" id="mediaType3" {{$gif ? 'checked' : ''}}>
<label class="custom-control-label" for="mediaType3"><span class="border border-dark px-1 rounded font-weight-bold">GIF</span></label>
</div>
<div class="custom-control custom-checkbox mt-2">
<input type="checkbox" name="type_webp" class="custom-control-input" id="mediaType4" {{$webp ? 'checked' : ''}}>
<label class="custom-control-label" for="mediaType4"><span class="border border-dark px-1 rounded font-weight-bold">WebP</span></label>
</div>
<div class="custom-control custom-checkbox mt-2">
<input type="checkbox" name="type_mp4" class="custom-control-input" id="mediaType5" {{$mp4 ? 'checked' : ''}}>
<label class="custom-control-label" for="mediaType5"><span class="border border-dark px-1 rounded font-weight-bold">MP4</span></label>
</div>
<p class="help-text small text-muted mt-3 mb-0">Allowed media types.</p>
</div>
</div>
</div>
<div class="tab-pane" id="rules" role="tabpanel" aria-labelledby="rules-tab">
<div class="border-top">
<p class="lead mt-3 py-3 text-center">Add rules that explain what is acceptable use.</p>
</div>
<div class="ml-n4 mr-n2 p-3 border-top border-bottom">
<p class="font-weight-bold text-muted">Active Rules</p>
<ol class="font-weight-bold">
@if($rules)
@foreach($rules as $rule)
<li class="mb-4">
<p class="mb-0">
{{$rule}}
</p>
<p>
<button type="button" class="btn btn-outline-danger btn-sm py-0 rule-delete" data-index="{{$loop->index}}">Delete</button>
</p>
</li>
@endforeach
@endif
</ol>
</div>
<div class="form-group mb-0">
<div class="ml-n4 mr-n2 p-3 border-top border-bottom">
<label class="font-weight-bold text-muted">Add Rule</label>
<input class="form-control" name="new_rule" placeholder="Add a new rule, we recommend being descriptive but keeping it short"/>
</div>
</div>
</div>
<div class="tab-pane" id="advanced" role="tabpanel" aria-labelledby="advanced-tab">
<div class="form-group mb-0">
<div class="ml-n4 mr-n2 p-3 border-top border-bottom">
<label class="font-weight-bold text-muted">Custom CSS</label>
<div class="custom-control custom-checkbox my-2">
<input type="checkbox" name="show_custom_css" class="custom-control-input" id="showCustomCss" {{config_cache('uikit.show_custom.css') ? 'checked' : ''}}>
<label class="custom-control-label font-weight-bold" for="showCustomCss">Enable custom CSS</label>
</div>
<textarea class="form-control" name="custom_css" rows="3">{{config_cache('uikit.custom.css')}}</textarea>
<p class="help-text small text-muted mt-3 mb-0">Add custom CSS, will be used on all pages</p>
</div>
</div>
</div>
</div>
<div class="form-group row mb-0 mt-4">
<div class="col-12 text-right">
<button type="submit" class="btn btn-primary font-weight-bold px-5">Save</button>
</div>
</div>
</form>
@else
</div>
<div class="py-5">
<p class="lead text-center font-weight-bold">Not enabled</p>
<p class="text-center">Add <code>ENABLE_CONFIG_CACHE=true</code> in your <span class="font-weight-bold">.env</span> file <br /> and run <span class="font-weight-bold">php artisan config:cache</span></p>
</div>
@endif
@endsection @endsection
@push('scripts') @push('scripts')
<script type="text/javascript"> <script type="text/javascript">
$('.rule-delete').on('click', function(e) { new Vue({ el: '#panel'});
if(window.confirm('Are you sure you want to delete this rule?')) {
let idx = e.target.dataset.index;
axios.post(window.location.href, {
'rule_delete': idx
}).then(res => {
$('.rule-delete[data-index="'+idx+'"]').parents().eq(1).remove();
});
}
});
$(document).ready(function() {
setTimeout(() => {
$('.alert-success').fadeOut();
}, 1000);
});
</script> </script>
@endpush @endpush

View file

@ -65,7 +65,7 @@
</div> </div>
</div> </div>
@if(config('captcha.enabled') || config('captcha.active.login') || config('captcha.active.register')) @if((bool) config_cache('captcha.enabled'))
<label class="font-weight-bold small text-muted">Captcha</label> <label class="font-weight-bold small text-muted">Captcha</label>
<div class="d-flex flex-grow-1"> <div class="d-flex flex-grow-1">
{!! Captcha::display(['data-theme' => 'dark']) !!} {!! Captcha::display(['data-theme' => 'dark']) !!}

View file

@ -76,10 +76,10 @@
</div> </div>
@if( @if(
config('captcha.enabled') || (bool) config_cache('captcha.enabled') &&
config('captcha.active.login') || (bool) config_cache('captcha.active.login') ||
( (
config('captcha.triggers.login.enabled') && (bool) config_cache('captcha.triggers.login.enabled') &&
request()->session()->has('login_attempts') && request()->session()->has('login_attempts') &&
request()->session()->get('login_attempts') >= config('captcha.triggers.login.attempts') request()->session()->get('login_attempts') >= config('captcha.triggers.login.attempts')
) )

View file

@ -54,7 +54,7 @@
</div> </div>
</div> </div>
@if(config('captcha.enabled')) @if((bool) config_cache('captcha.enabled'))
<label class="font-weight-bold small text-muted">Captcha</label> <label class="font-weight-bold small text-muted">Captcha</label>
<div class="d-flex flex-grow-1"> <div class="d-flex flex-grow-1">
{!! Captcha::display(['data-theme' => 'dark']) !!} {!! Captcha::display(['data-theme' => 'dark']) !!}

View file

@ -109,7 +109,7 @@
</div> </div>
</div> </div>
@if(config('captcha.enabled')) @if((bool) config_cache('captcha.enabled'))
<label class="font-weight-bold small pt-3 text-muted">Captcha</label> <label class="font-weight-bold small pt-3 text-muted">Captcha</label>
<div class="d-flex flex-grow-1"> <div class="d-flex flex-grow-1">
{!! Captcha::display(['data-theme' => 'dark']) !!} {!! Captcha::display(['data-theme' => 'dark']) !!}

View file

@ -81,7 +81,7 @@
</div> </div>
</div> </div>
@if(config('captcha.enabled') || config('captcha.active.register')) @if((bool) config_cache('captcha.enabled') && (bool) config_cache('captcha.active.register'))
<div class="d-flex justify-content-center my-3"> <div class="d-flex justify-content-center my-3">
{!! Captcha::display() !!} {!! Captcha::display() !!}
</div> </div>

View file

@ -1,4 +1,4 @@
@extends('layouts.app',['title' => 'Welcome to ' . config('app.name')]) @extends('layouts.app',['title' => 'Welcome to ' . config_cache('app.name')])
@section('content') @section('content')
<div class="container mt-4"> <div class="container mt-4">
@ -14,7 +14,7 @@
</div> </div>
@endif @endif
<p class="lead mb-0">Welcome to {{config('app.name')}}!</p> <p class="lead mb-0">Welcome to {{config_cache('app.name')}}!</p>
</div> </div>
</div> </div>

View file

@ -5,11 +5,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<title>{{ $title ?? config('app.name', 'Pixelfed') }}</title> <title>{{ $title ?? config_cache('app.name', 'Pixelfed') }}</title>
<link rel="manifest" href="{{url('/manifest.json')}}"> <link rel="manifest" href="{{url('/manifest.json')}}">
<meta property="og:site_name" content="{{ config('app.name', 'pixelfed') }}"> <meta property="og:site_name" content="{{ config_cache('app.name', 'pixelfed') }}">
<meta property="og:title" content="{{ $title ?? config('app.name', 'pixelfed') }}"> <meta property="og:title" content="{{ $title ?? config_cache('app.name', 'pixelfed') }}">
<meta property="og:type" content="article"> <meta property="og:type" content="article">
<meta property="og:url" content="{{url(request()->url())}}"> <meta property="og:url" content="{{url(request()->url())}}">
@stack('meta') @stack('meta')

View file

@ -70,11 +70,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<title>{{ $title ?? config('app.name', 'Pixelfed') }}</title> <title>{{ $title ?? config_cache('app.name', 'Pixelfed') }}</title>
<link rel="manifest" href="/manifest.json"> <link rel="manifest" href="/manifest.json">
<meta property="og:site_name" content="Pixelfed"> <meta property="og:site_name" content="Pixelfed">
<meta property="og:title" content="{{ $ogTitle ?? $title ?? config('app.name', 'pixelfed') }}"> <meta property="og:title" content="{{ $ogTitle ?? $title ?? config_cache('app.name', 'pixelfed') }}">
<meta property="og:type" content="{{ $ogType ?? 'article' }}"> <meta property="og:type" content="{{ $ogType ?? 'article' }}">
<meta property="og:url" content="{{url(request()->url())}}"> <meta property="og:url" content="{{url(request()->url())}}">
@stack('meta') @stack('meta')

View file

@ -11,8 +11,8 @@
<title>{{ $title ?? config_cache('app.name') }}</title> <title>{{ $title ?? config_cache('app.name') }}</title>
<meta property="og:site_name" content="{{ config('app.name', 'pixelfed') }}"> <meta property="og:site_name" content="{{ config_cache('app.name', 'pixelfed') }}">
<meta property="og:title" content="{{ $title ?? config('app.name', 'pixelfed') }}"> <meta property="og:title" content="{{ $title ?? config_cache('app.name', 'pixelfed') }}">
<meta property="og:type" content="article"> <meta property="og:type" content="article">
<meta property="og:url" content="{{request()->url()}}"> <meta property="og:url" content="{{request()->url()}}">
@stack('meta') @stack('meta')

View file

@ -9,11 +9,11 @@
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<title>{{ $title ?? config('app.name', 'Laravel') }}</title> <title>{{ $title ?? config_cache('app.name', 'Pixelfed') }}</title>
<link rel="manifest" href="/manifest.json"> <link rel="manifest" href="/manifest.json">
<meta property="og:site_name" content="{{ config('app.name', 'pixelfed') }}"> <meta property="og:site_name" content="{{ config_cache('app.name', 'pixelfed') }}">
<meta property="og:title" content="{{ $title ?? config('app.name', 'pixelfed') }}"> <meta property="og:title" content="{{ $title ?? config_cache('app.name', 'pixelfed') }}">
<meta property="og:type" content="article"> <meta property="og:type" content="article">
<meta property="og:url" content="{{request()->url()}}"> <meta property="og:url" content="{{request()->url()}}">
@stack('meta') @stack('meta')

View file

@ -105,7 +105,7 @@
{{__('navmenu.discover')}} {{__('navmenu.discover')}}
</a> </a>
@if(config_cache('instance.stories.enabled')) @if((bool) config_cache('instance.stories.enabled'))
<a class="dropdown-item lead" href="/i/stories/new"> <a class="dropdown-item lead" href="/i/stories/new">
<span style="width: 50px;margin-right:14px;"> <span style="width: 50px;margin-right:14px;">
<span class="fal fa-history text-lighter fa-lg"></span> <span class="fal fa-history text-lighter fa-lg"></span>

View file

@ -2,7 +2,7 @@
<div class="container"> <div class="container">
<a class="navbar-brand d-flex align-items-center" href="{{ url('/') }}"> <a class="navbar-brand d-flex align-items-center" href="{{ url('/') }}">
<img src="/img/pixelfed-icon-color.svg" height="30px" class="px-2" alt="Pixelfed logo"> <img src="/img/pixelfed-icon-color.svg" height="30px" class="px-2" alt="Pixelfed logo">
<span class="font-weight-bold mb-0" style="font-size:20px;">{{ config('app.name', 'Laravel') }}</span> <span class="font-weight-bold mb-0" style="font-size:20px;">{{ config_cache('app.name', 'Pixelfed') }}</span>
</a> </a>
</div> </div>
</nav> </nav>

View file

@ -11,8 +11,8 @@
<title>{!! $title ?? config_cache('app.name') !!}</title> <title>{!! $title ?? config_cache('app.name') !!}</title>
<meta property="og:site_name" content="{{ config('app.name', 'pixelfed') }}"> <meta property="og:site_name" content="{{ config_cache('app.name', 'pixelfed') }}">
<meta property="og:title" content="{{ $title ?? config('app.name', 'pixelfed') }}"> <meta property="og:title" content="{{ $title ?? config_cache('app.name', 'pixelfed') }}">
<meta property="og:type" content="article"> <meta property="og:type" content="article">
<meta property="og:url" content="{{request()->url()}}"> <meta property="og:url" content="{{request()->url()}}">
@stack('meta') @stack('meta')

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