Merge branch 'contrib-dev' of https://github.com/eufelipemateus/pixelfed into contrib-dev

This commit is contained in:
Felipe Mateus 2022-11-25 08:50:54 -03:00
commit 5dec7646ba
No known key found for this signature in database
GPG key ID: 94D494618F214123
36 changed files with 922 additions and 294 deletions

View file

@ -4,6 +4,8 @@
### New Features ### New Features
- Portfolios ([#3705](https://github.com/pixelfed/pixelfed/pull/3705)) - 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 ### Updates
- Update ApiV1Controller, include self likes in favourited_by endpoint ([58b331d2](https://github.com/pixelfed/pixelfed/commit/58b331d2)) - 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)) - 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)) - 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)) - 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/)) - ([](https://github.com/pixelfed/pixelfed/commit/))
## [v0.11.4 (2022-10-04)](https://github.com/pixelfed/pixelfed/compare/v0.11.3...v0.11.4) ## [v0.11.4 (2022-10-04)](https://github.com/pixelfed/pixelfed/compare/v0.11.3...v0.11.4)

View file

@ -3,6 +3,7 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Storage;
use App\Profile; use App\Profile;
use App\User; use App\User;
use App\Instance; use App\Instance;
@ -64,6 +65,7 @@ class SendUpdateActor extends Command
return; return;
} }
} }
$this->touchStorageCache($domain);
$this->line(' '); $this->line(' ');
$this->error('Keep this window open during this process or it will not complete!'); $this->error('Keep this window open during this process or it will not complete!');
$sharedInbox = Profile::whereDomain($domain)->whereNotNull('sharedInbox')->first(); $sharedInbox = Profile::whereDomain($domain)->whereNotNull('sharedInbox')->first();
@ -76,8 +78,14 @@ class SendUpdateActor extends Command
$this->info('Found sharedInbox: ' . $url); $this->info('Found sharedInbox: ' . $url);
$bar = $this->output->createProgressBar($totalUserCount); $bar = $this->output->createProgressBar($totalUserCount);
$bar->start(); $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) { foreach($users as $user) {
$this->updateStorageCache($domain, $user->id);
$profile = Profile::find($user->profile_id); $profile = Profile::find($user->profile_id);
if(!$profile) { if(!$profile) {
continue; 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) protected function actorObject($profile)
{ {
$permalink = $profile->permalink(); $permalink = $profile->permalink();

View file

@ -0,0 +1,53 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
use App\User;
class UserVerifyEmail extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'user:verifyemail {username}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Verify user email address';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$user = User::whereUsername($this->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);
}
}

View file

@ -525,10 +525,9 @@ class AccountController extends Controller
$user->save(); $user->save();
$request->session()->push('2fa.session.active', true); $request->session()->push('2fa.session.active', true);
return true; return true;
} else { }
}
return false; return false;
}
}
} else { } else {
return false; return false;
} }

View file

@ -0,0 +1,453 @@
<?php
namespace App\Http\Controllers\Admin;
use DB, Cache;
use App\{
DiscoverCategory,
DiscoverCategoryHashtag,
Hashtag,
Media,
Profile,
Status,
StatusHashtag,
User
};
use App\Models\ConfigCache;
use App\Services\AccountService;
use App\Services\ConfigCacheService;
use App\Services\StatusService;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use League\ISO3166\ISO3166;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Http;
use App\Http\Controllers\PixelfedDirectoryController;
trait AdminDirectoryController
{
public function directoryHome(Request $request)
{
return view('admin.directory.home');
}
public function directoryInitialData(Request $request)
{
$res = [];
$res['countries'] = collect((new ISO3166)->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;
}
}

View file

@ -20,6 +20,7 @@ use Carbon\Carbon;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redis; use Illuminate\Support\Facades\Redis;
use App\Http\Controllers\Admin\{ use App\Http\Controllers\Admin\{
AdminDirectoryController,
AdminDiscoverController, AdminDiscoverController,
AdminInstanceController, AdminInstanceController,
AdminReportController, AdminReportController,
@ -40,6 +41,7 @@ use App\Models\CustomEmoji;
class AdminController extends Controller class AdminController extends Controller
{ {
use AdminReportController, use AdminReportController,
AdminDirectoryController,
AdminDiscoverController, AdminDiscoverController,
// AdminGroupsController, // AdminGroupsController,
AdminMediaController, AdminMediaController,

View file

@ -8,7 +8,7 @@ use Illuminate\Support\Str;
use App\Util\ActivityPub\Helpers; use App\Util\ActivityPub\Helpers;
use App\Util\Media\Filter; use App\Util\Media\Filter;
use Laravel\Passport\Passport; use Laravel\Passport\Passport;
use Auth, Cache, DB, URL; use Auth, Cache, DB, Storage, URL;
use App\{ use App\{
Avatar, Avatar,
Bookmark, Bookmark,
@ -692,10 +692,10 @@ class ApiV1Controller extends Controller
(new FollowerController())->sendFollow($user->profile, $target); (new FollowerController())->sendFollow($user->profile, $target);
} }
} else { } else {
$follower = new Follower(); $follower = Follower::firstOrCreate([
$follower->profile_id = $user->profile_id; 'profile_id' => $user->profile_id,
$follower->following_id = $target->id; 'following_id' => $target->id
$follower->save(); ]);
if($remote == true && config('federation.activitypub.remoteFollow') == true) { if($remote == true && config('federation.activitypub.remoteFollow') == true) {
(new FollowerController())->sendFollow($user->profile, $target); (new FollowerController())->sendFollow($user->profile, $target);
@ -1375,7 +1375,7 @@ class ApiV1Controller extends Controller
'streaming_api' => 'wss://' . config('pixelfed.domain.app') 'streaming_api' => 'wss://' . config('pixelfed.domain.app')
], ],
'stats' => $stats, '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')], 'languages' => [config('app.locale')],
'registrations' => (bool) config_cache('pixelfed.open_registration'), 'registrations' => (bool) config_cache('pixelfed.open_registration'),
'approval_required' => false, 'approval_required' => false,
@ -3023,7 +3023,7 @@ class ApiV1Controller extends Controller
} }
if($sortBy == 'all' && !$request->has('cursor')) { 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') return DB::table('statuses')
->where('in_reply_to_id', $id) ->where('in_reply_to_id', $id)
->orderBy('id') ->orderBy('id')
@ -3058,8 +3058,15 @@ class ApiV1Controller extends Controller
$status['favourited'] = LikeService::liked($pid, $post->id); $status['favourited'] = LikeService::liked($pid, $post->id);
return $status; 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) { ->filter(function($post) {
return $post && isset($post['id']) && isset($post['account']); return $post && isset($post['id']) && isset($post['account']) && isset($post['account']['id']);
}) })
->values(); ->values();

View file

@ -96,17 +96,18 @@ class FederationController extends Controller
abort_if(!config_cache('federation.activitypub.enabled'), 404); abort_if(!config_cache('federation.activitypub.enabled'), 404);
abort_if(!config('federation.activitypub.outbox'), 404); abort_if(!config('federation.activitypub.outbox'), 404);
$profile = Profile::whereNull('domain') // $profile = Profile::whereNull('domain')
->whereNull('status') // ->whereNull('status')
->whereIsPrivate(false) // ->whereIsPrivate(false)
->whereUsername($username) // ->whereUsername($username)
->firstOrFail(); // ->firstOrFail();
$key = 'ap:outbox:latest_10:pid:' . $profile->id; // $key = 'ap:outbox:latest_10:pid:' . $profile->id;
$ttl = now()->addMinutes(15); // $ttl = now()->addMinutes(15);
$res = Cache::remember($key, $ttl, function() use($profile) { // $res = Cache::remember($key, $ttl, function() use($profile) {
return Outbox::get($profile); // return Outbox::get($profile);
}); // });
$res = [];
return response(json_encode($res, JSON_UNESCAPED_SLASHES))->header('Content-Type', 'application/activity+json'); 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'])) { if(!isset($obj['id'])) {
return; return;
} }
usleep(5000);
$lockKey = 'pf:ap:del-lock:' . hash('sha256', $obj['id']); $lockKey = 'pf:ap:del-lock:' . hash('sha256', $obj['id']);
if( isset($obj['actor']) && if( isset($obj['actor']) &&
isset($obj['object']) && isset($obj['object']) &&
@ -140,6 +142,15 @@ class FederationController extends Controller
Cache::put($lockKey, 1, 3600); Cache::put($lockKey, 1, 3600);
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete'); dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
} else { } 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'); dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high');
} }
return; return;

View file

@ -0,0 +1,167 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\ConfigCache;
use Storage;
use App\Services\AccountService;
use App\Services\StatusService;
use Illuminate\Support\Str;
class PixelfedDirectoryController extends Controller
{
public function get(Request $request)
{
if(!$request->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];
}
}

View file

@ -187,10 +187,12 @@ class ProfileController extends Controller
abort_if(!config_cache('federation.activitypub.enabled'), 404); abort_if(!config_cache('federation.activitypub.enabled'), 404);
abort_if($user->domain, 404); abort_if($user->domain, 404);
return Cache::remember('pf:activitypub:user-object:by-id:' . $user->id, 3600, function() use($user) {
$fractal = new Fractal\Manager(); $fractal = new Fractal\Manager();
$resource = new Fractal\Resource\Item($user, new ProfileTransformer); $resource = new Fractal\Resource\Item($user, new ProfileTransformer);
$res = $fractal->createData($resource)->toArray(); $res = $fractal->createData($resource)->toArray();
return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json'); return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json');
});
} }
public function showAtomFeed(Request $request, $user) public function showAtomFeed(Request $request, $user)
@ -201,10 +203,11 @@ class ProfileController extends Controller
abort_if(!$pid, 404); abort_if(!$pid, 404);
$profile = AccountService::get($pid); $profile = AccountService::get($pid, true);
abort_if(!$profile || $profile['locked'] || !$profile['local'], 404); abort_if(!$profile || $profile['locked'] || !$profile['local'], 404);
$data = Cache::remember('pf:atom:user-feed:by-id:' . $profile['id'], 86400, function() use($pid, $profile) {
$items = DB::table('statuses') $items = DB::table('statuses')
->whereProfileId($pid) ->whereProfileId($pid)
->whereVisibility('public') ->whereVisibility('public')
@ -228,9 +231,19 @@ class ProfileController extends Controller
if($items && $items->count()) { if($items && $items->count()) {
$headers['Last-Modified'] = now()->parse($items->first()['created_at'])->toRfc7231String(); $headers['Last-Modified'] = now()->parse($items->first()['created_at'])->toRfc7231String();
} }
return compact('items', 'permalink', 'headers');
});
abort_if(!$data, 404);
return response() return response()
->view('atom.user', compact('profile', 'items', 'permalink')) ->view('atom.user',
->withHeaders($headers); [
'profile' => $profile,
'items' => $data['items'],
'permalink' => $data['permalink']
]
)
->withHeaders($data['headers']);
} }
public function meRedirect() public function meRedirect()

View file

@ -159,7 +159,7 @@ trait HomeSettings
public function emailUpdate(Request $request) public function emailUpdate(Request $request)
{ {
$this->validate($request, [ $this->validate($request, [
'email' => 'required|email', 'email' => 'required|email|unique:users,email',
]); ]);
$changes = false; $changes = false;
$email = $request->input('email'); $email = $request->input('email');

View file

@ -200,7 +200,7 @@ class DeleteWorker implements ShouldQueue
if(Helpers::validateUrl($actor->remote_url) == false) { if(Helpers::validateUrl($actor->remote_url) == false) {
return; return;
} }
$res = Zttp::timeout(5)->withHeaders([ $res = Zttp::timeout(60)->withHeaders([
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org', 'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org',
])->get($actor->remote_url); ])->get($actor->remote_url);

View file

@ -176,7 +176,7 @@ class InboxValidator implements ShouldQueue
if(Helpers::validateUrl($actor->remote_url) == false) { if(Helpers::validateUrl($actor->remote_url) == false) {
return; return;
} }
$res = Zttp::timeout(5)->withHeaders([ $res = Zttp::timeout(60)->withHeaders([
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org', 'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org',
])->get($actor->remote_url); ])->get($actor->remote_url);

View file

@ -166,7 +166,7 @@ class InboxWorker implements ShouldQueue
if(Helpers::validateUrl($actor->remote_url) == false) { if(Helpers::validateUrl($actor->remote_url) == false) {
return; return;
} }
$res = Zttp::timeout(5)->withHeaders([ $res = Zttp::timeout(60)->withHeaders([
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org', 'User-Agent' => 'PixelfedBot v0.1 - https://pixelfed.org',
])->get($actor->remote_url); ])->get($actor->remote_url);

View file

@ -38,8 +38,14 @@ class MediaDeletePipeline implements ShouldQueue
if(config_cache('pixelfed.cloud_storage') == true) { if(config_cache('pixelfed.cloud_storage') == true) {
$disk = Storage::disk(config('filesystems.cloud')); $disk = Storage::disk(config('filesystems.cloud'));
if($path) {
$disk->delete($path); $disk->delete($path);
}
if($thumb) {
$disk->delete($thumb); $disk->delete($thumb);
}
if(count($e) > 4 && count($disk->files($i)) == 0) { if(count($e) > 4 && count($disk->files($i)) == 0) {
$disk->deleteDirectory($i); $disk->deleteDirectory($i);
@ -47,10 +53,10 @@ class MediaDeletePipeline implements ShouldQueue
} }
$disk = Storage::disk(config('filesystems.local')); $disk = Storage::disk(config('filesystems.local'));
if($disk->exists($path)) { if($path && $disk->exists($path)) {
$disk->delete($path); $disk->delete($path);
} }
if($disk->exists($thumb)) { if($thumb && $disk->exists($thumb)) {
$disk->delete($thumb); $disk->delete($thumb);
} }
if(count($e) > 4 && count($disk->files($i)) == 0) { if(count($e) > 4 && count($disk->files($i)) == 0) {

View file

@ -10,5 +10,5 @@ class ConfigCache extends Model
use HasFactory; use HasFactory;
protected $table = 'config_cache'; protected $table = 'config_cache';
public $fillable = ['*']; public $guarded = [];
} }

View file

@ -164,15 +164,16 @@ class Profile extends Model
if(substr($avatar->cdn_url, 0, 8) === 'https://') { if(substr($avatar->cdn_url, 0, 8) === 'https://') {
return $avatar->cdn_url; return $avatar->cdn_url;
} else { } else {
return url($avatar->cdn_url);
}
}
if($avatar->is_remote) {
return url('/storage/avatars/default.jpg'); return url('/storage/avatars/default.jpg');
} }
}
$path = $avatar->media_path; $path = $avatar->media_path;
if(substr($path, 0, 6) !== 'public') {
return url('/storage/avatars/default.jpg');
}
$path = "{$path}?v={$avatar->change_count}"; $path = "{$path}?v={$avatar->change_count}";
return config('app.url') . Storage::url($path); return config('app.url') . Storage::url($path);

View file

@ -19,19 +19,21 @@ class AccountService
public static function get($id, $softFail = false) 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 = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer()); $fractal->setSerializer(new ArraySerializer());
$profile = Profile::find($id); $profile = Profile::find($id);
if(!$profile) { if(!$profile || $profile->status === 'delete') {
if($softFail) {
return null; return null;
} }
abort(404);
}
$resource = new Fractal\Resource\Item($profile, new AccountTransformer()); $resource = new Fractal\Resource\Item($profile, new AccountTransformer());
return $fractal->createData($resource)->toArray(); return $fractal->createData($resource)->toArray();
}); });
if(!$res) {
return $softFail ? null : abort(404);
}
return $res;
} }
public static function getMastodon($id, $softFail = false) public static function getMastodon($id, $softFail = false)

View file

@ -55,6 +55,15 @@ class ConfigCacheService
'config.discover.features', 'config.discover.features',
'instance.has_legal_notice', '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' // 'system.user_mode'
]; ];

View file

@ -246,7 +246,7 @@ class MediaStorageService {
$file = $disk->putFileAs($base, new File($tmpName), $path, 'public'); $file = $disk->putFileAs($base, new File($tmpName), $path, 'public');
$permalink = $disk->url($file); $permalink = $disk->url($file);
$avatar->media_path = $base . $path; $avatar->media_path = $base . '/' . $path;
$avatar->is_remote = true; $avatar->is_remote = true;
$avatar->cdn_url = $local ? config('app.url') . $permalink : $permalink; $avatar->cdn_url = $local ? config('app.url') . $permalink : $permalink;
$avatar->size = $head['length']; $avatar->size = $head['length'];

View file

@ -32,19 +32,23 @@ class NotificationTransformer extends Fractal\TransformerAbstract
if($n->item_id && $n->item_type == 'App\ModLog') { if($n->item_id && $n->item_type == 'App\ModLog') {
$ml = $n->item; $ml = $n->item;
if($ml && $ml->object_uid) {
$res['modlog'] = [ $res['modlog'] = [
'id' => $ml->object_uid, 'id' => $ml->object_uid,
'url' => url('/i/admin/users/modlogs/' . $ml->object_uid) 'url' => url('/i/admin/users/modlogs/' . $ml->object_uid)
]; ];
} }
}
if($n->item_id && $n->item_type == 'App\MediaTag') { if($n->item_id && $n->item_type == 'App\MediaTag') {
$ml = $n->item; $ml = $n->item;
if($ml && $ml->tagged_username) {
$res['tagged'] = [ $res['tagged'] = [
'username' => $ml->tagged_username, 'username' => $ml->tagged_username,
'post_url' => '/p/'.HashidService::encode($ml->status_id) 'post_url' => '/p/'.HashidService::encode($ml->status_id)
]; ];
} }
}
return $res; return $res;
} }

View file

@ -742,10 +742,10 @@ class Helpers {
[ [
'domain' => strtolower($domain), 'domain' => strtolower($domain),
'username' => Purify::clean($webfinger), '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'], 'remote_url' => $res['id'],
'name' => isset($res['name']) ? Purify::clean($res['name']) : 'user', 'name' => isset($res['name']) ? Purify::clean($res['name']) : 'user',
'bio' => isset($res['summary']) ? Purify::clean($res['summary']) : null, 'bio' => isset($res['summary']) ? Purify::clean($res['summary']) : null,

View file

@ -192,7 +192,7 @@ class Inbox
if(!isset($activity['to'])) { if(!isset($activity['to'])) {
return; return;
} }
$to = $activity['to']; $to = isset($activity['to']) ? $activity['to'] : [];
$cc = isset($activity['cc']) ? $activity['cc'] : []; $cc = isset($activity['cc']) ? $activity['cc'] : [];
if($activity['type'] == 'Question') { if($activity['type'] == 'Question') {
@ -200,7 +200,9 @@ class Inbox
return; return;
} }
if(count($to) == 1 && if( is_array($to) &&
is_array($cc) &&
count($to) == 1 &&
count($cc) == 0 && count($cc) == 0 &&
parse_url($to[0], PHP_URL_HOST) == config('pixelfed.domain.app') parse_url($to[0], PHP_URL_HOST) == config('pixelfed.domain.app')
) { ) {
@ -727,6 +729,9 @@ class Inbox
$profile = self::actorFirstOrCreate($actor); $profile = self::actorFirstOrCreate($actor);
$obj = $this->payload['object']; $obj = $this->payload['object'];
if(!$profile) {
return;
}
// TODO: Some implementations do not inline the object, skip for now // TODO: Some implementations do not inline the object, skip for now
if(!$obj || !is_array($obj) || !isset($obj['type'])) { if(!$obj || !is_array($obj) || !isset($obj['type'])) {
return; return;
@ -786,7 +791,7 @@ class Inbox
Like::whereProfileId($profile->id) Like::whereProfileId($profile->id)
->whereStatusId($status->id) ->whereStatusId($status->id)
->forceDelete(); ->forceDelete();
Notification::whereProfileId($status->profile->id) Notification::whereProfileId($status->profile_id)
->whereActorId($profile->id) ->whereActorId($profile->id)
->whereAction('like') ->whereAction('like')
->whereItemId($status->id) ->whereItemId($status->id)

View file

@ -16,7 +16,7 @@ return [
'inbox' => env('AP_INBOX', true), 'inbox' => env('AP_INBOX', true),
'sharedInbox' => env('AP_SHAREDINBOX', true), 'sharedInbox' => env('AP_SHAREDINBOX', true),
'remoteFollow' => env('AP_REMOTE_FOLLOW', false), 'remoteFollow' => env('AP_REMOTE_FOLLOW', true),
'delivery' => [ 'delivery' => [
'timeout' => env('ACTIVITYPUB_DELIVERY_TIMEOUT', 30.0), 'timeout' => env('ACTIVITYPUB_DELIVERY_TIMEOUT', 30.0),

View file

@ -13,9 +13,9 @@ return [
'timeout' => 3600, 'timeout' => 3600,
'enable_logging' => env('FFMPEG_LOG', false), 'log_channel' => env('FFMPEG_LOG_CHANNEL', false), // set to false to completely disable logging
'set_command_and_error_output_on_exception' => false,
'temporary_files_root' => env('FFMPEG_TEMPORARY_FILES_ROOT', sys_get_temp_dir()), '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())),
]; ];

BIN
public/css/admin.css vendored

Binary file not shown.

BIN
public/js/admin.js vendored

Binary file not shown.

Binary file not shown.

View file

@ -25,4 +25,4 @@ return [
'taggingPeople' => 'Označování lidí' 'taggingPeople' => 'Označování lidí'
] ];

View file

@ -1,186 +0,0 @@
<?php
return [
'common' => [
'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'
],
];

View file

@ -0,0 +1,12 @@
@extends('admin.partial.template-full')
@section('section')
</div>
<admin-directory />
@endsection
@push('scripts')
<script type="text/javascript">
new Vue({ el: '#panel'});
</script>
@endpush

View file

@ -62,6 +62,13 @@
<ul class="navbar-nav mb-md-3"> <ul class="navbar-nav mb-md-3">
<li class="nav-item">
<a class="nav-link {{request()->is('*directory*')?'active':''}}" href="{{route('admin.directory')}}">
<i class="ni ni-bold-right text-primary"></i>
<span class="nav-link-text">Directory <span class="badge badge-primary ml-1">NEW</span></span>
</a>
</li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {{request()->is('*apps*')?'active':''}}" href="{{route('admin.apps')}}"> <a class="nav-link {{request()->is('*apps*')?'active':''}}" href="{{route('admin.apps')}}">
<i class="ni ni-bold-right text-primary"></i> <i class="ni ni-bold-right text-primary"></i>

View file

@ -53,12 +53,15 @@
<p class="display-2 font-weight-bold">{{ __('site.title1') }}</p> <p class="display-2 font-weight-bold">{{ __('site.title1') }}</p>
<p class="h1 font-weight-bold">{{ __('site.title2') }}</p> <p class="h1 font-weight-bold">{{ __('site.title2') }}</p>
</div> </div>
<p class="lead font-weight-light mt-5">{{ config_cache('app.short_description') ?? 'Pixelfed is an image sharing platform, an ethical alternative to centralized platforms.' }}</p>
<p><a href="https://pixelfed.org" target="_blank" class="font-weight-bold">Learn more</a></p>
</div> </div>
<div class="col-12 col-md-5 offset-md-1"> <div class="col-12 col-md-5 offset-md-1">
<div> <div>
<div class="pt-md-3 d-flex justify-content-center align-items-center"> <div class="pt-md-3 d-flex justify-content-center align-items-center">
<img src="/img/pixelfed-icon-color.svg" loading="lazy" width="50px" height="50px"> <img src="/img/pixelfed-icon-color.svg" loading="lazy" width="50px" height="50px">
<span class="font-weight-bold h3 ml-2 pt-2">Pixelfed</span> <span class="font-weight-bold h3 ml-2 pt-2">{{ config_cache('app.name') ?? 'Pixelfed' }}</span>
</div> </div>
<div class="d-block d-md-none"> <div class="d-block d-md-none">
<p class="font-weight-bold mb-0 text-center">__('site.mainTitle')</p> <p class="font-weight-bold mb-0 text-center">__('site.mainTitle')</p>

View file

@ -145,6 +145,10 @@ Route::group(['prefix' => 'api'], function() use($middleware) {
Route::get('posts/trending', 'DiscoverController@trendingApi')->middleware($middleware); Route::get('posts/trending', 'DiscoverController@trendingApi')->middleware($middleware);
Route::get('posts/hashtags', 'DiscoverController@trendingHashtags')->middleware($middleware); Route::get('posts/hashtags', 'DiscoverController@trendingHashtags')->middleware($middleware);
}); });
Route::group(['prefix' => 'directory'], function () use($middleware) {
Route::get('listing', 'PixelfedDirectoryController@get');
});
}); });
Route::group(['prefix' => 'live'], function() use($middleware) { Route::group(['prefix' => 'live'], function() use($middleware) {

View file

@ -92,11 +92,22 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio
Route::post('custom-emoji/delete/{id}', 'AdminController@customEmojiDelete'); Route::post('custom-emoji/delete/{id}', 'AdminController@customEmojiDelete');
Route::get('custom-emoji/duplicates/{id}', 'AdminController@customEmojiShowDuplicates'); Route::get('custom-emoji/duplicates/{id}', 'AdminController@customEmojiShowDuplicates');
Route::get('directory/home', 'AdminController@directoryHome')->name('admin.directory');
Route::prefix('api')->group(function() { Route::prefix('api')->group(function() {
Route::get('stats', 'AdminController@getStats'); Route::get('stats', 'AdminController@getStats');
Route::get('accounts', 'AdminController@getAccounts'); Route::get('accounts', 'AdminController@getAccounts');
Route::get('posts', 'AdminController@getPosts'); Route::get('posts', 'AdminController@getPosts');
Route::get('instances', 'AdminController@getInstances'); Route::get('instances', 'AdminController@getInstances');
Route::post('directory/save', 'AdminController@directoryStore');
Route::get('directory/initial-data', 'AdminController@directoryInitialData');
Route::get('directory/popular-posts', 'AdminController@directoryGetPopularPosts');
Route::post('directory/add-by-id', 'AdminController@directoryGetAddPostByIdSearch');
Route::delete('directory/banner-image', 'AdminController@directoryDeleteBannerImage');
Route::post('directory/submit', 'AdminController@directoryHandleServerSubmission');
Route::post('directory/testimonial/save', 'AdminController@directorySaveTestimonial');
Route::post('directory/testimonial/delete', 'AdminController@directoryDeleteTestimonial');
Route::post('directory/testimonial/update', 'AdminController@directoryUpdateTestimonial');
}); });
}); });
@ -124,6 +135,7 @@ Route::domain(config('portfolio.domain'))->group(function () {
Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofactor', 'localization'])->group(function () { Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofactor', 'localization'])->group(function () {
Route::get('/', 'SiteController@home')->name('timeline.personal'); Route::get('/', 'SiteController@home')->name('timeline.personal');
Route::redirect('/home', '/')->name('home');
Auth::routes(); Auth::routes();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

After

Width:  |  Height:  |  Size: 60 KiB