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')
+
+