diff --git a/CHANGELOG.md b/CHANGELOG.md index 69ef83f3c..12c35cd56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### New Features - Portfolios ([#3705](https://github.com/pixelfed/pixelfed/pull/3705)) +- Server Directory ([#3762](https://github.com/pixelfed/pixelfed/pull/3762)) +- Manually verify email address (php artisan user:verifyemail) ([682f5f0f](https://github.com/pixelfed/pixelfed/commit/682f5f0f)) ### Updates - Update ApiV1Controller, include self likes in favourited_by endpoint ([58b331d2](https://github.com/pixelfed/pixelfed/commit/58b331d2)) @@ -11,6 +13,20 @@ - Update status deletion, fix database lock issues and side effects ([04e8c96a](https://github.com/pixelfed/pixelfed/commit/04e8c96a)) - Fix remote profile avatar urls when storing locally ([b0422d4f](https://github.com/pixelfed/pixelfed/commit/b0422d4f)) - Enable network timeline caching by default ([c990ac2a](https://github.com/pixelfed/pixelfed/commit/c990ac2a)) +- Redirect /home to / ([97032997](https://github.com/pixelfed/pixelfed/commit/97032997)) +- Fix 2FA backup code bug ([a231b3c5](https://github.com/pixelfed/pixelfed/commit/a231b3c5)) +- Update federation config, enable remote follows by default ([59702d40](https://github.com/pixelfed/pixelfed/commit/59702d40)) +- Update ApiV1Controller, fix followAccountById with firstOrCreate() ([1d52ad0b](https://github.com/pixelfed/pixelfed/commit/1d52ad0b)) +- Update AccountService, fix delete status ([8b7121f9](https://github.com/pixelfed/pixelfed/commit/8b7121f9)) +- Update ap helpers, fix duplicate entry bug ([85cfa1ba](https://github.com/pixelfed/pixelfed/commit/85cfa1ba)) +- Update Inbox, fix handleUndoActivity ([d660e46b](https://github.com/pixelfed/pixelfed/commit/d660e46b)) +- Update HomeSettings controller, bail earlier when attempting to update email that already exists ([399bf5f8](https://github.com/pixelfed/pixelfed/commit/399bf5f8)) +- Update ProfileController, cache actor object and atom feed ([8665eab1](https://github.com/pixelfed/pixelfed/commit/8665eab1)) +- Update NotificationTransformer, fix mediaTag and modLog types ([b6c06c4b](https://github.com/pixelfed/pixelfed/commit/b6c06c4b)) +- Update landing view, add `app.name` and `app.short_description` for better customizability ([bda9d16b](https://github.com/pixelfed/pixelfed/commit/bda9d16b)) +- Update Profile, fix avatarUrl paths. Fixes #3559 #3634 ([989e4249](https://github.com/pixelfed/pixelfed/commit/989e4249)) +- Update InboxPipeline, bump request timeout from 5s to 60s ([bb120019](https://github.com/pixelfed/pixelfed/commit/bb120019)) +- Update web routes, fix missing hom route ([a9f4ddfc](https://github.com/pixelfed/pixelfed/commit/a9f4ddfc)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.4 (2022-10-04)](https://github.com/pixelfed/pixelfed/compare/v0.11.3...v0.11.4) diff --git a/app/Console/Commands/SendUpdateActor.php b/app/Console/Commands/SendUpdateActor.php index f0c753760..369d582a4 100644 --- a/app/Console/Commands/SendUpdateActor.php +++ b/app/Console/Commands/SendUpdateActor.php @@ -3,6 +3,7 @@ namespace App\Console\Commands; use Illuminate\Console\Command; +use Storage; use App\Profile; use App\User; use App\Instance; @@ -64,6 +65,7 @@ class SendUpdateActor extends Command return; } } + $this->touchStorageCache($domain); $this->line(' '); $this->error('Keep this window open during this process or it will not complete!'); $sharedInbox = Profile::whereDomain($domain)->whereNotNull('sharedInbox')->first(); @@ -76,8 +78,14 @@ class SendUpdateActor extends Command $this->info('Found sharedInbox: ' . $url); $bar = $this->output->createProgressBar($totalUserCount); $bar->start(); - User::whereNull('status')->chunk(50, function($users) use($bar, $url) { + + $startCache = $this->getStorageCache($domain); + User::whereNull('status')->when($startCache, function($query, $startCache) use($bar) { + $bar->advance($startCache); + return $query->where('id', '>', $startCache); + })->chunk(50, function($users) use($bar, $url, $domain) { foreach($users as $user) { + $this->updateStorageCache($domain, $user->id); $profile = Profile::find($user->profile_id); if(!$profile) { continue; @@ -120,6 +128,26 @@ class SendUpdateActor extends Command ]; } + protected function touchStorageCache($domain) + { + $path = 'actor-update-cache/' . $domain; + if(!Storage::exists($path)) { + Storage::put($path, ""); + } + } + + protected function getStorageCache($domain) + { + $path = 'actor-update-cache/' . $domain; + return Storage::get($path); + } + + protected function updateStorageCache($domain, $value) + { + $path = 'actor-update-cache/' . $domain; + Storage::put($path, $value); + } + protected function actorObject($profile) { $permalink = $profile->permalink(); diff --git a/app/Console/Commands/UserVerifyEmail.php b/app/Console/Commands/UserVerifyEmail.php new file mode 100644 index 000000000..3b3cac5ef --- /dev/null +++ b/app/Console/Commands/UserVerifyEmail.php @@ -0,0 +1,53 @@ +argument('username'))->first(); + + if(!$user) { + $this->error('Username not found'); + return; + } + + $user->email_verified_at = now(); + $user->save(); + $this->info('Successfully verified email address for ' . $user->username); + } +} diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 0d3177b9d..89f46e046 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -513,26 +513,25 @@ class AccountController extends Controller } } - protected function twoFactorBackupCheck($request, $code, User $user) - { - $backupCodes = $user->{'2fa_backup_codes'}; - if($backupCodes) { - $codes = json_decode($backupCodes, true); - foreach ($codes as $c) { - if(hash_equals($c, $code)) { - $codes = array_flatten(array_diff($codes, [$code])); - $user->{'2fa_backup_codes'} = json_encode($codes); - $user->save(); - $request->session()->push('2fa.session.active', true); - return true; - } else { - return false; - } - } - } else { - return false; - } - } + protected function twoFactorBackupCheck($request, $code, User $user) + { + $backupCodes = $user->{'2fa_backup_codes'}; + if($backupCodes) { + $codes = json_decode($backupCodes, true); + foreach ($codes as $c) { + if(hash_equals($c, $code)) { + $codes = array_flatten(array_diff($codes, [$code])); + $user->{'2fa_backup_codes'} = json_encode($codes); + $user->save(); + $request->session()->push('2fa.session.active', true); + return true; + } + } + return false; + } else { + return false; + } + } public function accountRestored(Request $request) { diff --git a/app/Http/Controllers/Admin/AdminDirectoryController.php b/app/Http/Controllers/Admin/AdminDirectoryController.php new file mode 100644 index 000000000..1e4db7d2d --- /dev/null +++ b/app/Http/Controllers/Admin/AdminDirectoryController.php @@ -0,0 +1,453 @@ +all())->pluck('name'); + $res['admins'] = User::whereIsAdmin(true) + ->where('2fa_enabled', true) + ->get()->map(function($user) { + return [ + 'uid' => (string) $user->id, + 'pid' => (string) $user->profile_id, + 'username' => $user->username, + 'created_at' => $user->created_at + ]; + }); + $config = ConfigCache::whereK('pixelfed.directory')->first(); + if($config) { + $data = $config->v ? json_decode($config->v, true) : []; + $res = array_merge($res, $data); + } + + if(empty($res['summary'])) { + $summary = ConfigCache::whereK('app.short_description')->pluck('v'); + $res['summary'] = $summary ? $summary[0] : null; + } + + if(isset($res['banner_image']) && !empty($res['banner_image'])) { + $res['banner_image'] = url(Storage::url($res['banner_image'])); + } + + if(isset($res['favourite_posts'])) { + $res['favourite_posts'] = collect($res['favourite_posts'])->map(function($id) { + return StatusService::get($id); + }) + ->filter(function($post) { + return $post && isset($post['account']); + }) + ->values(); + } + + $res['community_guidelines'] = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : []; + $res['open_registration'] = (bool) config_cache('pixelfed.open_registration'); + $res['oauth_enabled'] = (bool) config_cache('pixelfed.oauth_enabled') && file_exists(storage_path('oauth-public.key')) && file_exists(storage_path('oauth-private.key')); + + $res['activitypub_enabled'] = (bool) config_cache('federation.activitypub.enabled'); + + $res['feature_config'] = [ + 'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','), + 'image_quality' => config_cache('pixelfed.image_quality'), + 'optimize_image' => config_cache('pixelfed.optimize_image'), + 'max_photo_size' => config_cache('pixelfed.max_photo_size'), + 'max_caption_length' => config_cache('pixelfed.max_caption_length'), + 'max_altext_length' => config_cache('pixelfed.max_altext_length'), + 'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit'), + 'max_account_size' => config_cache('pixelfed.max_account_size'), + 'max_album_length' => config_cache('pixelfed.max_album_length'), + 'account_deletion' => config_cache('pixelfed.account_deletion'), + ]; + + if(config_cache('pixelfed.directory.testimonials')) { + $testimonials = collect(json_decode(config_cache('pixelfed.directory.testimonials'),true)) + ->map(function($t) { + return [ + 'profile' => AccountService::get($t['profile_id']), + 'body' => $t['body'] + ]; + }); + $res['testimonials'] = $testimonials; + } + + $validator = Validator::make($res['feature_config'], [ + 'media_types' => [ + 'required', + function ($attribute, $value, $fail) { + if (!in_array('image/jpeg', $value->toArray()) || !in_array('image/png', $value->toArray())) { + $fail('You must enable image/jpeg and image/png support.'); + } + }, + ], + 'image_quality' => 'required_if:optimize_image,true|integer|min:75|max:100', + 'max_altext_length' => 'required|integer|min:1000|max:5000', + 'max_photo_size' => 'required|integer|min:15000|max:100000', + 'max_account_size' => 'required_if:enforce_account_limit,true|integer|min:1000000', + 'max_album_length' => 'required|integer|min:4|max:20', + 'account_deletion' => 'required|accepted', + 'max_caption_length' => 'required|integer|min:500|max:10000' + ]); + + $res['requirements_validator'] = $validator->errors(); + + $res['is_eligible'] = $res['open_registration'] && + $res['oauth_enabled'] && + $res['activitypub_enabled'] && + count($res['requirements_validator']) === 0 && + $this->validVal($res, 'admin') && + $this->validVal($res, 'summary', null, 10) && + $this->validVal($res, 'favourite_posts', 3) && + $this->validVal($res, 'contact_email') && + $this->validVal($res, 'privacy_pledge') && + $this->validVal($res, 'location'); + + $res['has_submitted'] = config_cache('pixelfed.directory.has_submitted') ?? false; + $res['synced'] = config_cache('pixelfed.directory.is_synced') ?? false; + $res['latest_response'] = config_cache('pixelfed.directory.latest_response') ?? null; + + $path = base_path('resources/lang'); + $langs = collect([]); + + foreach (new \DirectoryIterator($path) as $io) { + $name = $io->getFilename(); + $skip = ['vendor']; + if($io->isDot() || in_array($name, $skip)) { + continue; + } + + if($io->isDir()) { + $langs->push(['code' => $name, 'name' => locale_get_display_name($name)]); + } + } + + $res['available_languages'] = $langs->sortBy('name')->values(); + $res['primary_locale'] = config('app.locale'); + + $submissionState = Http::withoutVerifying() + ->post('https://pixelfed.org/api/v1/directory/check-submission', [ + 'domain' => config('pixelfed.domain.app') + ]); + + $res['submission_state'] = $submissionState->json(); + return $res; + } + + protected function validVal($res, $val, $count = false, $minLen = false) + { + if(!isset($res[$val])) { + return false; + } + + if($count) { + return count($res[$val]) >= $count; + } + + if($minLen) { + return strlen($res[$val]) >= $minLen; + } + + return $res[$val]; + } + + public function directoryStore(Request $request) + { + $this->validate($request, [ + 'location' => 'string|min:1|max:53', + 'summary' => 'string|nullable|max:140', + 'admin_uid' => 'sometimes|nullable', + 'contact_email' => 'sometimes|nullable|email:rfc,dns', + 'favourite_posts' => 'array|max:12', + 'favourite_posts.*' => 'distinct', + 'privacy_pledge' => 'sometimes', + 'banner_image' => 'sometimes|mimes:jpg,png|dimensions:width=1920,height:1080|max:5000' + ]); + + $config = ConfigCache::firstOrNew([ + 'k' => 'pixelfed.directory' + ]); + + $res = $config->v ? json_decode($config->v, true) : []; + $res['summary'] = strip_tags($request->input('summary')); + $res['favourite_posts'] = $request->input('favourite_posts'); + $res['admin'] = (string) $request->input('admin_uid'); + $res['contact_email'] = $request->input('contact_email'); + $res['privacy_pledge'] = (bool) $request->input('privacy_pledge'); + + if($request->filled('location')) { + $exists = (new ISO3166)->name($request->location); + if($exists) { + $res['location'] = $request->input('location'); + } + } + + if($request->hasFile('banner_image')) { + collect(Storage::files('public/headers')) + ->filter(function($name) { + $protected = [ + 'public/headers/.gitignore', + 'public/headers/default.jpg', + 'public/headers/missing.png' + ]; + return !in_array($name, $protected); + }) + ->each(function($name) { + Storage::delete($name); + }); + $path = $request->file('banner_image')->store('public/headers'); + $res['banner_image'] = $path; + ConfigCacheService::put('app.banner_image', url(Storage::url($path))); + + Cache::forget('api:v1:instance-data-response-v1'); + } + + $config->v = json_encode($res); + $config->save(); + + ConfigCacheService::put('pixelfed.directory', $config->v); + $updated = json_decode($config->v, true); + if(isset($updated['banner_image'])) { + $updated['banner_image'] = url(Storage::url($updated['banner_image'])); + } + return $updated; + } + + public function directoryHandleServerSubmission(Request $request) + { + $reqs = []; + $reqs['feature_config'] = [ + 'open_registration' => config_cache('pixelfed.open_registration'), + 'activitypub_enabled' => config_cache('federation.activitypub.enabled'), + 'oauth_enabled' => config_cache('pixelfed.oauth_enabled'), + 'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','), + 'image_quality' => config_cache('pixelfed.image_quality'), + 'optimize_image' => config_cache('pixelfed.optimize_image'), + 'max_photo_size' => config_cache('pixelfed.max_photo_size'), + 'max_caption_length' => config_cache('pixelfed.max_caption_length'), + 'max_altext_length' => config_cache('pixelfed.max_altext_length'), + 'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit'), + 'max_account_size' => config_cache('pixelfed.max_account_size'), + 'max_album_length' => config_cache('pixelfed.max_album_length'), + 'account_deletion' => config_cache('pixelfed.account_deletion'), + ]; + + $validator = Validator::make($reqs['feature_config'], [ + 'open_registration' => 'required|accepted', + 'activitypub_enabled' => 'required|accepted', + 'oauth_enabled' => 'required|accepted', + 'media_types' => [ + 'required', + function ($attribute, $value, $fail) { + if (!in_array('image/jpeg', $value->toArray()) || !in_array('image/png', $value->toArray())) { + $fail('You must enable image/jpeg and image/png support.'); + } + }, + ], + 'image_quality' => 'required_if:optimize_image,true|integer|min:75|max:100', + 'max_altext_length' => 'required|integer|min:1000|max:5000', + 'max_photo_size' => 'required|integer|min:15000|max:100000', + 'max_account_size' => 'required_if:enforce_account_limit,true|integer|min:1000000', + 'max_album_length' => 'required|integer|min:4|max:20', + 'account_deletion' => 'required|accepted', + 'max_caption_length' => 'required|integer|min:500|max:10000' + ]); + + if(!$validator->validate()) { + return response()->json($validator->errors(), 422); + } + + ConfigCacheService::put('pixelfed.directory.submission-key', Str::random(random_int(40, 69))); + ConfigCacheService::put('pixelfed.directory.submission-ts', now()); + + $data = (new PixelfedDirectoryController())->buildListing(); + $res = Http::withoutVerifying()->post('https://pixelfed.org/api/v1/directory/submission', $data); + return 200; + } + + public function directoryDeleteBannerImage(Request $request) + { + $bannerImage = ConfigCache::whereK('app.banner_image')->first(); + $directory = ConfigCache::whereK('pixelfed.directory')->first(); + if(!$bannerImage && !$directory || empty($directory->v)) { + return; + } + $directoryArr = json_decode($directory->v, true); + $path = isset($directoryArr['banner_image']) ? $directoryArr['banner_image'] : false; + $protected = [ + 'public/headers/.gitignore', + 'public/headers/default.jpg', + 'public/headers/missing.png' + ]; + if(!$path || in_array($path, $protected)) { + return; + } + if(Storage::exists($directoryArr['banner_image'])) { + Storage::delete($directoryArr['banner_image']); + } + + $directoryArr['banner_image'] = 'public/headers/default.jpg'; + $directory->v = $directoryArr; + $directory->save(); + $bannerImage->v = url(Storage::url('public/headers/default.jpg')); + $bannerImage->save(); + Cache::forget('api:v1:instance-data-response-v1'); + ConfigCacheService::put('pixelfed.directory', $directory); + return $bannerImage->v; + } + + public function directoryGetPopularPosts(Request $request) + { + $ids = Cache::remember('admin:api:popular_posts', 86400, function() { + return Status::whereLocal(true) + ->whereScope('public') + ->whereType('photo') + ->whereNull(['in_reply_to_id', 'reblog_of_id']) + ->orderByDesc('likes_count') + ->take(50) + ->pluck('id'); + }); + + $res = $ids->map(function($id) { + return StatusService::get($id); + }) + ->filter(function($post) { + return $post && isset($post['account']); + }) + ->values(); + + return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } + + public function directoryGetAddPostByIdSearch(Request $request) + { + $this->validate($request, [ + 'q' => 'required|integer' + ]); + + $id = $request->input('q'); + + $status = Status::whereLocal(true) + ->whereType('photo') + ->whereNull(['in_reply_to_id', 'reblog_of_id']) + ->findOrFail($id); + + $res = StatusService::get($status->id); + + return $res; + } + + public function directoryDeleteTestimonial(Request $request) + { + $this->validate($request, [ + 'profile_id' => 'required', + ]); + $profile_id = $request->input('profile_id'); + $testimonials = ConfigCache::whereK('pixelfed.directory.testimonials')->firstOrFail(); + $existing = collect(json_decode($testimonials->v, true)) + ->filter(function($t) use($profile_id) { + return $t['profile_id'] !== $profile_id; + }) + ->values(); + ConfigCacheService::put('pixelfed.directory.testimonials', $existing); + return $existing; + } + + public function directorySaveTestimonial(Request $request) + { + $this->validate($request, [ + 'username' => 'required', + 'body' => 'required|string|min:5|max:500' + ]); + + $user = User::whereUsername($request->input('username'))->whereNull('status')->firstOrFail(); + + $configCache = ConfigCache::firstOrCreate([ + 'k' => 'pixelfed.directory.testimonials' + ]); + + $testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]); + + abort_if($testimonials->contains('profile_id', $user->profile_id), 422, 'Testimonial already exists'); + abort_if($testimonials->count() == 10, 422, 'You can only have 10 active testimonials'); + + $testimonials->push([ + 'profile_id' => (string) $user->profile_id, + 'username' => $request->input('username'), + 'body' => $request->input('body') + ]); + + $configCache->v = json_encode($testimonials->toArray()); + $configCache->save(); + ConfigCacheService::put('pixelfed.directory.testimonials', $configCache->v); + $res = [ + 'profile' => AccountService::get($user->profile_id), + 'body' => $request->input('body') + ]; + return $res; + } + + public function directoryUpdateTestimonial(Request $request) + { + $this->validate($request, [ + 'profile_id' => 'required', + 'body' => 'required|string|min:5|max:500' + ]); + + $profile_id = $request->input('profile_id'); + $body = $request->input('body'); + $user = User::whereProfileId($profile_id)->firstOrFail(); + + $configCache = ConfigCache::firstOrCreate([ + 'k' => 'pixelfed.directory.testimonials' + ]); + + $testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]); + + $updated = $testimonials->map(function($t) use($profile_id, $body) { + if($t['profile_id'] == $profile_id) { + $t['body'] = $body; + } + return $t; + }) + ->values(); + + $configCache->v = json_encode($updated); + $configCache->save(); + ConfigCacheService::put('pixelfed.directory.testimonials', $configCache->v); + + return $updated; + } +} diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index 37e3a7c0d..8a6f019ef 100644 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -20,6 +20,7 @@ use Carbon\Carbon; use Illuminate\Http\Request; use Illuminate\Support\Facades\Redis; use App\Http\Controllers\Admin\{ + AdminDirectoryController, AdminDiscoverController, AdminInstanceController, AdminReportController, @@ -40,6 +41,7 @@ use App\Models\CustomEmoji; class AdminController extends Controller { use AdminReportController, + AdminDirectoryController, AdminDiscoverController, // AdminGroupsController, AdminMediaController, diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 3387cd273..d2c8b2028 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -8,7 +8,7 @@ use Illuminate\Support\Str; use App\Util\ActivityPub\Helpers; use App\Util\Media\Filter; use Laravel\Passport\Passport; -use Auth, Cache, DB, URL; +use Auth, Cache, DB, Storage, URL; use App\{ Avatar, Bookmark, @@ -692,10 +692,10 @@ class ApiV1Controller extends Controller (new FollowerController())->sendFollow($user->profile, $target); } } else { - $follower = new Follower(); - $follower->profile_id = $user->profile_id; - $follower->following_id = $target->id; - $follower->save(); + $follower = Follower::firstOrCreate([ + 'profile_id' => $user->profile_id, + 'following_id' => $target->id + ]); if($remote == true && config('federation.activitypub.remoteFollow') == true) { (new FollowerController())->sendFollow($user->profile, $target); @@ -1375,7 +1375,7 @@ class ApiV1Controller extends Controller 'streaming_api' => 'wss://' . config('pixelfed.domain.app') ], 'stats' => $stats, - 'thumbnail' => url('img/pixelfed-icon-color.png'), + 'thumbnail' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')), 'languages' => [config('app.locale')], 'registrations' => (bool) config_cache('pixelfed.open_registration'), 'approval_required' => false, @@ -3023,7 +3023,7 @@ class ApiV1Controller extends Controller } if($sortBy == 'all' && !$request->has('cursor')) { - $ids = Cache::remember('status:replies:all:' . $id, 86400, function() use($id) { + $ids = Cache::remember('status:replies:all:' . $id, 3600, function() use($id) { return DB::table('statuses') ->where('in_reply_to_id', $id) ->orderBy('id') @@ -3058,8 +3058,15 @@ class ApiV1Controller extends Controller $status['favourited'] = LikeService::liked($pid, $post->id); return $status; }) + ->map(function($post) { + if(isset($post['account']) && isset($post['account']['id'])) { + $account = AccountService::get($post['account']['id'], true); + $post['account'] = $account; + } + return $post; + }) ->filter(function($post) { - return $post && isset($post['id']) && isset($post['account']); + return $post && isset($post['id']) && isset($post['account']) && isset($post['account']['id']); }) ->values(); diff --git a/app/Http/Controllers/FederationController.php b/app/Http/Controllers/FederationController.php index 0a8254cbc..87a393545 100644 --- a/app/Http/Controllers/FederationController.php +++ b/app/Http/Controllers/FederationController.php @@ -96,17 +96,18 @@ class FederationController extends Controller abort_if(!config_cache('federation.activitypub.enabled'), 404); abort_if(!config('federation.activitypub.outbox'), 404); - $profile = Profile::whereNull('domain') - ->whereNull('status') - ->whereIsPrivate(false) - ->whereUsername($username) - ->firstOrFail(); + // $profile = Profile::whereNull('domain') + // ->whereNull('status') + // ->whereIsPrivate(false) + // ->whereUsername($username) + // ->firstOrFail(); - $key = 'ap:outbox:latest_10:pid:' . $profile->id; - $ttl = now()->addMinutes(15); - $res = Cache::remember($key, $ttl, function() use($profile) { - return Outbox::get($profile); - }); + // $key = 'ap:outbox:latest_10:pid:' . $profile->id; + // $ttl = now()->addMinutes(15); + // $res = Cache::remember($key, $ttl, function() use($profile) { + // return Outbox::get($profile); + // }); + $res = []; return response(json_encode($res, JSON_UNESCAPED_SLASHES))->header('Content-Type', 'application/activity+json'); } @@ -124,6 +125,7 @@ class FederationController extends Controller if(!isset($obj['id'])) { return; } + usleep(5000); $lockKey = 'pf:ap:del-lock:' . hash('sha256', $obj['id']); if( isset($obj['actor']) && isset($obj['object']) && @@ -140,6 +142,15 @@ class FederationController extends Controller Cache::put($lockKey, 1, 3600); dispatch(new DeleteWorker($headers, $payload))->onQueue('delete'); } else { + if(!isset($obj['id'])) { + return; + } + usleep(5000); + $lockKey = 'pf:ap:user-inbox:activity:' . hash('sha256', $obj['id']); + if(Cache::get($lockKey) !== null) { + return; + } + Cache::put($lockKey, 1, 3600); dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high'); } return; diff --git a/app/Http/Controllers/PixelfedDirectoryController.php b/app/Http/Controllers/PixelfedDirectoryController.php new file mode 100644 index 000000000..6290cd398 --- /dev/null +++ b/app/Http/Controllers/PixelfedDirectoryController.php @@ -0,0 +1,167 @@ +filled('sk')) { + abort(404); + } + + if(!config_cache('pixelfed.directory.submission-key')) { + abort(404); + } + + if(!hash_equals(config_cache('pixelfed.directory.submission-key'), $request->input('sk'))) { + abort(403); + } + + $res = $this->buildListing(); + return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } + + public function buildListing() + { + $res = config_cache('pixelfed.directory'); + if($res) { + $res = is_string($res) ? json_decode($res, true) : $res; + } + + $res['_domain'] = config_cache('pixelfed.domain.app'); + $res['_sk'] = config_cache('pixelfed.directory.submission-key'); + $res['_ts'] = config_cache('pixelfed.directory.submission-ts'); + $res['version'] = config_cache('pixelfed.version'); + + if(empty($res['summary'])) { + $summary = ConfigCache::whereK('app.short_description')->pluck('v'); + $res['summary'] = $summary ? $summary[0] : null; + } + + if(isset($res['admin'])) { + $res['admin'] = AccountService::get($res['admin'], true); + } + + if(isset($res['banner_image']) && !empty($res['banner_image'])) { + $res['banner_image'] = url(Storage::url($res['banner_image'])); + } + + if(isset($res['favourite_posts'])) { + $res['favourite_posts'] = collect($res['favourite_posts'])->map(function($id) { + return StatusService::get($id); + }) + ->filter(function($post) { + return $post && isset($post['account']); + }) + ->map(function($post) { + return [ + 'avatar' => $post['account']['avatar'], + 'display_name' => $post['account']['display_name'], + 'username' => $post['account']['username'], + 'media' => $post['media_attachments'][0]['url'], + 'url' => $post['url'] + ]; + }) + ->values(); + } + + $guidelines = ConfigCache::whereK('app.rules')->first(); + if($guidelines) { + $res['community_guidelines'] = json_decode($guidelines->v, true); + } + + $openRegistration = ConfigCache::whereK('pixelfed.open_registration')->first(); + if($openRegistration) { + $res['open_registration'] = (bool) $openRegistration; + } + + $oauthEnabled = ConfigCache::whereK('pixelfed.oauth_enabled')->first(); + if($oauthEnabled) { + $keys = file_exists(storage_path('oauth-public.key')) && file_exists(storage_path('oauth-private.key')); + $res['oauth_enabled'] = (bool) $oauthEnabled && $keys; + } + + $activityPubEnabled = ConfigCache::whereK('federation.activitypub.enabled')->first(); + if($activityPubEnabled) { + $res['activitypub_enabled'] = (bool) $activityPubEnabled; + } + + $res['feature_config'] = [ + 'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','), + 'image_quality' => config_cache('pixelfed.image_quality'), + 'optimize_image' => config_cache('pixelfed.optimize_image'), + 'max_photo_size' => config_cache('pixelfed.max_photo_size'), + 'max_caption_length' => config_cache('pixelfed.max_caption_length'), + 'max_altext_length' => config_cache('pixelfed.max_altext_length'), + 'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit'), + 'max_account_size' => config_cache('pixelfed.max_account_size'), + 'max_album_length' => config_cache('pixelfed.max_album_length'), + 'account_deletion' => config_cache('pixelfed.account_deletion'), + ]; + + $res['is_eligible'] = $this->validVal($res, 'admin') && + $this->validVal($res, 'summary', null, 10) && + $this->validVal($res, 'favourite_posts', 3) && + $this->validVal($res, 'contact_email') && + $this->validVal($res, 'privacy_pledge') && + $this->validVal($res, 'location'); + + if(config_cache('pixelfed.directory.testimonials')) { + $res['testimonials'] = collect(json_decode(config_cache('pixelfed.directory.testimonials'), true)) + ->map(function($testimonial) { + $profile = AccountService::get($testimonial['profile_id']); + return [ + 'profile' => [ + 'username' => $profile['username'], + 'display_name' => $profile['display_name'], + 'avatar' => $profile['avatar'], + 'created_at' => $profile['created_at'] + ], + 'body' => $testimonial['body'] + ]; + }); + } + + $res['features_enabled'] = [ + 'stories' => (bool) config_cache('instance.stories.enabled') + ]; + + $res['stats'] = [ + 'user_count' => \App\User::count(), + 'post_count' => \App\Status::whereNull('uri')->count(), + ]; + + $res['primary_locale'] = config('app.locale'); + $hash = hash('sha256', json_encode($res)); + $res['_hash'] = $hash; + ksort($res); + + return $res; + } + + protected function validVal($res, $val, $count = false, $minLen = false) + { + if(!isset($res[$val])) { + return false; + } + + if($count) { + return count($res[$val]) >= $count; + } + + if($minLen) { + return strlen($res[$val]) >= $minLen; + } + + return $res[$val]; + } + +} diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index 30957cf28..3f6795d5b 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -187,10 +187,12 @@ class ProfileController extends Controller abort_if(!config_cache('federation.activitypub.enabled'), 404); abort_if($user->domain, 404); - $fractal = new Fractal\Manager(); - $resource = new Fractal\Resource\Item($user, new ProfileTransformer); - $res = $fractal->createData($resource)->toArray(); - return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json'); + return Cache::remember('pf:activitypub:user-object:by-id:' . $user->id, 3600, function() use($user) { + $fractal = new Fractal\Manager(); + $resource = new Fractal\Resource\Item($user, new ProfileTransformer); + $res = $fractal->createData($resource)->toArray(); + return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json'); + }); } public function showAtomFeed(Request $request, $user) @@ -201,36 +203,47 @@ class ProfileController extends Controller abort_if(!$pid, 404); - $profile = AccountService::get($pid); + $profile = AccountService::get($pid, true); abort_if(!$profile || $profile['locked'] || !$profile['local'], 404); - $items = DB::table('statuses') - ->whereProfileId($pid) - ->whereVisibility('public') - ->whereType('photo') - ->orderByDesc('id') - ->take(10) - ->get() - ->map(function($status) { - return StatusService::get($status->id); - }) - ->filter(function($status) { - return $status && - isset($status['account']) && - isset($status['media_attachments']) && - count($status['media_attachments']); - }) - ->values(); - $permalink = config('app.url') . "/users/{$profile['username']}.atom"; - $headers = ['Content-Type' => 'application/atom+xml']; + $data = Cache::remember('pf:atom:user-feed:by-id:' . $profile['id'], 86400, function() use($pid, $profile) { + $items = DB::table('statuses') + ->whereProfileId($pid) + ->whereVisibility('public') + ->whereType('photo') + ->orderByDesc('id') + ->take(10) + ->get() + ->map(function($status) { + return StatusService::get($status->id); + }) + ->filter(function($status) { + return $status && + isset($status['account']) && + isset($status['media_attachments']) && + count($status['media_attachments']); + }) + ->values(); + $permalink = config('app.url') . "/users/{$profile['username']}.atom"; + $headers = ['Content-Type' => 'application/atom+xml']; - if($items && $items->count()) { - $headers['Last-Modified'] = now()->parse($items->first()['created_at'])->toRfc7231String(); - } + if($items && $items->count()) { + $headers['Last-Modified'] = now()->parse($items->first()['created_at'])->toRfc7231String(); + } + + return compact('items', 'permalink', 'headers'); + }); + abort_if(!$data, 404); return response() - ->view('atom.user', compact('profile', 'items', 'permalink')) - ->withHeaders($headers); + ->view('atom.user', + [ + 'profile' => $profile, + 'items' => $data['items'], + 'permalink' => $data['permalink'] + ] + ) + ->withHeaders($data['headers']); } public function meRedirect() diff --git a/app/Http/Controllers/Settings/HomeSettings.php b/app/Http/Controllers/Settings/HomeSettings.php index 23d434d30..e8d3d195e 100644 --- a/app/Http/Controllers/Settings/HomeSettings.php +++ b/app/Http/Controllers/Settings/HomeSettings.php @@ -159,7 +159,7 @@ trait HomeSettings public function emailUpdate(Request $request) { $this->validate($request, [ - 'email' => 'required|email', + 'email' => 'required|email|unique:users,email', ]); $changes = false; $email = $request->input('email'); diff --git a/app/Jobs/InboxPipeline/DeleteWorker.php b/app/Jobs/InboxPipeline/DeleteWorker.php index 25dbde6dd..dead58163 100644 --- a/app/Jobs/InboxPipeline/DeleteWorker.php +++ b/app/Jobs/InboxPipeline/DeleteWorker.php @@ -200,7 +200,7 @@ class DeleteWorker implements ShouldQueue if(Helpers::validateUrl($actor->remote_url) == false) { return; } - $res = Zttp::timeout(5)->withHeaders([ + $res = Zttp::timeout(60)->withHeaders([ 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', 'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org', ])->get($actor->remote_url); diff --git a/app/Jobs/InboxPipeline/InboxValidator.php b/app/Jobs/InboxPipeline/InboxValidator.php index 4913cf84b..22a023304 100644 --- a/app/Jobs/InboxPipeline/InboxValidator.php +++ b/app/Jobs/InboxPipeline/InboxValidator.php @@ -176,7 +176,7 @@ class InboxValidator implements ShouldQueue if(Helpers::validateUrl($actor->remote_url) == false) { return; } - $res = Zttp::timeout(5)->withHeaders([ + $res = Zttp::timeout(60)->withHeaders([ 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', 'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org', ])->get($actor->remote_url); diff --git a/app/Jobs/InboxPipeline/InboxWorker.php b/app/Jobs/InboxPipeline/InboxWorker.php index 7f5bee9a0..23371c3ce 100644 --- a/app/Jobs/InboxPipeline/InboxWorker.php +++ b/app/Jobs/InboxPipeline/InboxWorker.php @@ -166,7 +166,7 @@ class InboxWorker implements ShouldQueue if(Helpers::validateUrl($actor->remote_url) == false) { return; } - $res = Zttp::timeout(5)->withHeaders([ + $res = Zttp::timeout(60)->withHeaders([ 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', 'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org', ])->get($actor->remote_url); diff --git a/app/Jobs/MediaPipeline/MediaDeletePipeline.php b/app/Jobs/MediaPipeline/MediaDeletePipeline.php index df91c8316..2b7a5f733 100644 --- a/app/Jobs/MediaPipeline/MediaDeletePipeline.php +++ b/app/Jobs/MediaPipeline/MediaDeletePipeline.php @@ -38,8 +38,14 @@ class MediaDeletePipeline implements ShouldQueue if(config_cache('pixelfed.cloud_storage') == true) { $disk = Storage::disk(config('filesystems.cloud')); - $disk->delete($path); - $disk->delete($thumb); + + if($path) { + $disk->delete($path); + } + + if($thumb) { + $disk->delete($thumb); + } if(count($e) > 4 && count($disk->files($i)) == 0) { $disk->deleteDirectory($i); @@ -47,10 +53,10 @@ class MediaDeletePipeline implements ShouldQueue } $disk = Storage::disk(config('filesystems.local')); - if($disk->exists($path)) { + if($path && $disk->exists($path)) { $disk->delete($path); } - if($disk->exists($thumb)) { + if($thumb && $disk->exists($thumb)) { $disk->delete($thumb); } if(count($e) > 4 && count($disk->files($i)) == 0) { diff --git a/app/Models/ConfigCache.php b/app/Models/ConfigCache.php index 4698b1c6b..1b4a18108 100644 --- a/app/Models/ConfigCache.php +++ b/app/Models/ConfigCache.php @@ -10,5 +10,5 @@ class ConfigCache extends Model use HasFactory; protected $table = 'config_cache'; - public $fillable = ['*']; + public $guarded = []; } diff --git a/app/Profile.php b/app/Profile.php index 491606031..f02144f09 100644 --- a/app/Profile.php +++ b/app/Profile.php @@ -164,15 +164,16 @@ class Profile extends Model if(substr($avatar->cdn_url, 0, 8) === 'https://') { return $avatar->cdn_url; } else { - return url($avatar->cdn_url); + return url('/storage/avatars/default.jpg'); } } - if($avatar->is_remote) { + $path = $avatar->media_path; + + if(substr($path, 0, 6) !== 'public') { return url('/storage/avatars/default.jpg'); } - - $path = $avatar->media_path; + $path = "{$path}?v={$avatar->change_count}"; return config('app.url') . Storage::url($path); diff --git a/app/Services/AccountService.php b/app/Services/AccountService.php index 13cccffab..eb744ff9d 100644 --- a/app/Services/AccountService.php +++ b/app/Services/AccountService.php @@ -19,19 +19,21 @@ class AccountService public static function get($id, $softFail = false) { - return Cache::remember(self::CACHE_KEY . $id, 43200, function() use($id, $softFail) { + $res = Cache::remember(self::CACHE_KEY . $id, 43200, function() use($id) { $fractal = new Fractal\Manager(); $fractal->setSerializer(new ArraySerializer()); $profile = Profile::find($id); - if(!$profile) { - if($softFail) { - return null; - } - abort(404); + if(!$profile || $profile->status === 'delete') { + return null; } $resource = new Fractal\Resource\Item($profile, new AccountTransformer()); return $fractal->createData($resource)->toArray(); - }); + }); + + if(!$res) { + return $softFail ? null : abort(404); + } + return $res; } public static function getMastodon($id, $softFail = false) diff --git a/app/Services/ConfigCacheService.php b/app/Services/ConfigCacheService.php index b334e9e33..0a9055287 100644 --- a/app/Services/ConfigCacheService.php +++ b/app/Services/ConfigCacheService.php @@ -55,6 +55,15 @@ class ConfigCacheService 'config.discover.features', 'instance.has_legal_notice', + + 'pixelfed.directory', + 'app.banner_image', + 'pixelfed.directory.submission-key', + 'pixelfed.directory.submission-ts', + 'pixelfed.directory.has_submitted', + 'pixelfed.directory.latest_response', + 'pixelfed.directory.is_synced', + 'pixelfed.directory.testimonials', // 'system.user_mode' ]; diff --git a/app/Services/MediaStorageService.php b/app/Services/MediaStorageService.php index eac7da1a7..1c3272ec1 100644 --- a/app/Services/MediaStorageService.php +++ b/app/Services/MediaStorageService.php @@ -246,7 +246,7 @@ class MediaStorageService { $file = $disk->putFileAs($base, new File($tmpName), $path, 'public'); $permalink = $disk->url($file); - $avatar->media_path = $base . $path; + $avatar->media_path = $base . '/' . $path; $avatar->is_remote = true; $avatar->cdn_url = $local ? config('app.url') . $permalink : $permalink; $avatar->size = $head['length']; diff --git a/app/Transformer/Api/NotificationTransformer.php b/app/Transformer/Api/NotificationTransformer.php index df8d0d30c..d4f84bbef 100644 --- a/app/Transformer/Api/NotificationTransformer.php +++ b/app/Transformer/Api/NotificationTransformer.php @@ -32,18 +32,22 @@ class NotificationTransformer extends Fractal\TransformerAbstract if($n->item_id && $n->item_type == 'App\ModLog') { $ml = $n->item; - $res['modlog'] = [ - 'id' => $ml->object_uid, - 'url' => url('/i/admin/users/modlogs/' . $ml->object_uid) - ]; + if($ml && $ml->object_uid) { + $res['modlog'] = [ + 'id' => $ml->object_uid, + 'url' => url('/i/admin/users/modlogs/' . $ml->object_uid) + ]; + } } if($n->item_id && $n->item_type == 'App\MediaTag') { $ml = $n->item; - $res['tagged'] = [ - 'username' => $ml->tagged_username, - 'post_url' => '/p/'.HashidService::encode($ml->status_id) - ]; + if($ml && $ml->tagged_username) { + $res['tagged'] = [ + 'username' => $ml->tagged_username, + 'post_url' => '/p/'.HashidService::encode($ml->status_id) + ]; + } } return $res; diff --git a/app/Util/ActivityPub/Helpers.php b/app/Util/ActivityPub/Helpers.php index f71c95a64..1516478d0 100644 --- a/app/Util/ActivityPub/Helpers.php +++ b/app/Util/ActivityPub/Helpers.php @@ -742,10 +742,10 @@ class Helpers { [ 'domain' => strtolower($domain), 'username' => Purify::clean($webfinger), - 'webfinger' => Purify::clean($webfinger), - 'key_id' => $res['publicKey']['id'], ], [ + 'webfinger' => Purify::clean($webfinger), + 'key_id' => $res['publicKey']['id'], 'remote_url' => $res['id'], 'name' => isset($res['name']) ? Purify::clean($res['name']) : 'user', 'bio' => isset($res['summary']) ? Purify::clean($res['summary']) : null, diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php index f103ddc7e..a8bfb87fe 100644 --- a/app/Util/ActivityPub/Inbox.php +++ b/app/Util/ActivityPub/Inbox.php @@ -192,7 +192,7 @@ class Inbox if(!isset($activity['to'])) { return; } - $to = $activity['to']; + $to = isset($activity['to']) ? $activity['to'] : []; $cc = isset($activity['cc']) ? $activity['cc'] : []; if($activity['type'] == 'Question') { @@ -200,7 +200,9 @@ class Inbox return; } - if(count($to) == 1 && + if( is_array($to) && + is_array($cc) && + count($to) == 1 && count($cc) == 0 && parse_url($to[0], PHP_URL_HOST) == config('pixelfed.domain.app') ) { @@ -727,6 +729,9 @@ class Inbox $profile = self::actorFirstOrCreate($actor); $obj = $this->payload['object']; + if(!$profile) { + return; + } // TODO: Some implementations do not inline the object, skip for now if(!$obj || !is_array($obj) || !isset($obj['type'])) { return; @@ -786,7 +791,7 @@ class Inbox Like::whereProfileId($profile->id) ->whereStatusId($status->id) ->forceDelete(); - Notification::whereProfileId($status->profile->id) + Notification::whereProfileId($status->profile_id) ->whereActorId($profile->id) ->whereAction('like') ->whereItemId($status->id) diff --git a/config/federation.php b/config/federation.php index 91575abd9..4b6795687 100644 --- a/config/federation.php +++ b/config/federation.php @@ -16,7 +16,7 @@ return [ 'inbox' => env('AP_INBOX', true), 'sharedInbox' => env('AP_SHAREDINBOX', true), - 'remoteFollow' => env('AP_REMOTE_FOLLOW', false), + 'remoteFollow' => env('AP_REMOTE_FOLLOW', true), 'delivery' => [ 'timeout' => env('ACTIVITYPUB_DELIVERY_TIMEOUT', 30.0), diff --git a/config/laravel-ffmpeg.php b/config/laravel-ffmpeg.php index 21315f199..44cd1b6eb 100644 --- a/config/laravel-ffmpeg.php +++ b/config/laravel-ffmpeg.php @@ -13,9 +13,9 @@ return [ 'timeout' => 3600, - 'enable_logging' => env('FFMPEG_LOG', false), - - 'set_command_and_error_output_on_exception' => false, + 'log_channel' => env('FFMPEG_LOG_CHANNEL', false), // set to false to completely disable logging 'temporary_files_root' => env('FFMPEG_TEMPORARY_FILES_ROOT', sys_get_temp_dir()), + + 'temporary_files_encrypted_hls' => env('FFMPEG_TEMPORARY_ENCRYPTED_HLS', env('FFMPEG_TEMPORARY_FILES_ROOT', sys_get_temp_dir())), ]; diff --git a/public/css/admin.css b/public/css/admin.css index 1f0afb047..2df11cc56 100644 Binary files a/public/css/admin.css and b/public/css/admin.css differ diff --git a/public/js/admin.js b/public/js/admin.js index a9626e9fc..c84e6e2fe 100644 Binary files a/public/js/admin.js and b/public/js/admin.js differ diff --git a/public/mix-manifest.json b/public/mix-manifest.json index 2f76f51b0..7cada2bac 100644 Binary files a/public/mix-manifest.json and b/public/mix-manifest.json differ diff --git a/resources/lang/cs/helpcenter.php b/resources/lang/cs/helpcenter.php index 29dbc5aec..70ff2d0f9 100644 --- a/resources/lang/cs/helpcenter.php +++ b/resources/lang/cs/helpcenter.php @@ -25,4 +25,4 @@ return [ 'taggingPeople' => 'Označování lidí' -] +]; diff --git a/resources/lang/me/web.php b/resources/lang/me/web.php deleted file mode 100644 index 3844f847a..000000000 --- a/resources/lang/me/web.php +++ /dev/null @@ -1,186 +0,0 @@ - [ - 'comment' => 'Comment', - 'commented' => 'Commented', - 'comments' => 'Comments', - 'like' => 'Like', - 'liked' => 'Liked', - 'likes' => 'Likes', - 'share' => 'Share', - 'shared' => 'Shared', - 'shares' => 'Shares', - 'unshare' => 'Unshare', - - 'cancel' => 'Cancel', - 'copyLink' => 'Copy Link', - 'delete' => 'Delete', - 'error' => 'Error', - 'errorMsg' => 'Something went wrong. Please try again later.', - 'oops' => 'Oops!', - 'other' => 'Other', - 'readMore' => 'Read more', - 'success' => 'Success', - - 'sensitive' => 'Sensitive', - 'sensitiveContent' => 'Sensitive Content', - 'sensitiveContentWarning' => 'This post may contain sensitive content', - ], - - 'site' => [ - 'terms' => 'Terms of Use', - 'privacy' => 'Privacy Policy', - ], - - 'navmenu' => [ - 'search' => 'Search', - 'admin' => 'Admin Dashboard', - - // Timelines - 'homeFeed' => 'Home Feed', - 'localFeed' => 'Local Feed', - 'globalFeed' => 'Global Feed', - - // Core features - 'discover' => 'Discover', - 'directMessages' => 'Direct Messages', - 'notifications' => 'Notifications', - 'groups' => 'Groups', - 'stories' => 'Stories', - - // Self links - 'profile' => 'Profile', - 'drive' => 'Drive', - 'settings' => 'Settings', - 'compose' => 'Create New', - 'logout' => 'Logout', - - // Nav footer - 'about' => 'About', - 'help' => 'Help', - 'language' => 'Language', - 'privacy' => 'Privacy', - 'terms' => 'Terms', - - // Temporary links - 'backToPreviousDesign' => 'Go back to previous design' - ], - - 'directMessages' => [ - 'inbox' => 'Inbox', - 'sent' => 'Sent', - 'requests' => 'Requests' - ], - - 'notifications' => [ - 'liked' => 'liked your', - 'commented' => 'commented on your', - 'reacted' => 'reacted to your', - 'shared' => 'shared your', - 'tagged' => 'tagged you in a', - - 'updatedA' => 'updated a', - 'sentA' => 'sent a', - - 'followed' => 'followed', - 'mentioned' => 'mentioned', - 'you' => 'you', - - 'yourApplication' => 'Your application to join', - 'applicationApproved' => 'was approved!', - 'applicationRejected' => 'was rejected. You can re-apply to join in 6 months.', - - 'dm' => 'dm', - 'groupPost' => 'group post', - 'modlog' => 'modlog', - 'post' => 'post', - 'story' => 'story', - ], - - 'post' => [ - 'shareToFollowers' => 'Share to followers', - 'shareToOther' => 'Share to other', - 'noLikes' => 'No likes yet', - 'uploading' => 'Uploading', - ], - - 'profile' => [ - 'posts' => 'Posts', - 'followers' => 'Followers', - 'following' => 'Following', - 'admin' => 'Admin', - 'collections' => 'Collections', - 'follow' => 'Follow', - 'unfollow' => 'Unfollow', - 'editProfile' => 'Edit Profile', - 'followRequested' => 'Follow Requested', - 'joined' => 'Joined', - - 'emptyCollections' => 'We can\'t seem to find any collections', - 'emptyPosts' => 'We can\'t seem to find any posts', - ], - - 'menu' => [ - 'viewPost' => 'View Post', - 'viewProfile' => 'View Profile', - 'moderationTools' => 'Moderation Tools', - 'report' => 'Report', - 'archive' => 'Archive', - 'unarchive' => 'Unarchive', - 'embed' => 'Embed', - - 'selectOneOption' => 'Select one of the following options', - 'unlistFromTimelines' => 'Unlist from Timelines', - 'addCW' => 'Add Content Warning', - 'removeCW' => 'Remove Content Warning', - 'markAsSpammer' => 'Mark as Spammer', - 'markAsSpammerText' => 'Unlist + CW existing and future posts', - 'spam' => 'Spam', - 'sensitive' => 'Sensitive Content', - 'abusive' => 'Abusive or Harmful', - 'underageAccount' => 'Underage Account', - 'copyrightInfringement' => 'Copyright Infringement', - 'impersonation' => 'Impersonation', - 'scamOrFraud' => 'Scam or Fraud', - 'confirmReport' => 'Confirm Report', - 'confirmReportText' => 'Are you sure you want to report this post?', - 'reportSent' => 'Report Sent!', - 'reportSentText' => 'We have successfully received your report.', - 'reportSentError' => 'There was an issue reporting this post.', - - 'modAddCWConfirm' => 'Are you sure you want to add a content warning to this post?', - 'modCWSuccess' => 'Successfully added content warning', - 'modRemoveCWConfirm' => 'Are you sure you want to remove the content warning on this post?', - 'modRemoveCWSuccess' => 'Successfully removed content warning', - 'modUnlistConfirm' => 'Are you sure you want to unlist this post?', - 'modUnlistSuccess' => 'Successfully unlisted post', - 'modMarkAsSpammerConfirm' => 'Are you sure you want to mark this user as a spammer? All existing and future posts will be unlisted on timelines and a content warning will be applied.', - 'modMarkAsSpammerSuccess' => 'Successfully marked account as spammer', - - 'toFollowers' => 'to Followers', - - 'showCaption' => 'Show Caption', - 'showLikes' => 'Show Likes', - 'compactMode' => 'Compact Mode', - 'embedConfirmText' => 'By using this embed, you agree to our', - - 'deletePostConfirm' => 'Are you sure you want to delete this post?', - 'archivePostConfirm' => 'Are you sure you want to archive this post?', - 'unarchivePostConfirm' => 'Are you sure you want to unarchive this post?', - ], - - 'story' => [ - 'add' => 'Add Story' - ], - - 'timeline' => [ - 'peopleYouMayKnow' => 'People you may know' - ], - - 'hashtags' => [ - 'emptyFeed' => 'We can\'t seem to find any posts for this hashtag' - ], - -]; diff --git a/resources/views/admin/directory/home.blade.php b/resources/views/admin/directory/home.blade.php new file mode 100644 index 000000000..dcc5e050e --- /dev/null +++ b/resources/views/admin/directory/home.blade.php @@ -0,0 +1,12 @@ +@extends('admin.partial.template-full') + +@section('section') + + +@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/admin/partial/sidenav.blade.php b/resources/views/admin/partial/sidenav.blade.php index 31a2a8a05..a34d572d2 100644 --- a/resources/views/admin/partial/sidenav.blade.php +++ b/resources/views/admin/partial/sidenav.blade.php @@ -62,6 +62,13 @@