Merge branch 'contrib-dev' into main

This commit is contained in:
Felipe Mateus 2023-05-04 23:13:26 -03:00
commit 774d2b7d32
No known key found for this signature in database
GPG key ID: 94D494618F214123
132 changed files with 4949 additions and 2079 deletions

View file

@ -7,7 +7,7 @@ jobs:
build:
docker:
# Specify the version you desire here
- image: cimg/php:7.4.26
- image: cimg/php:8.1.12
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images

View file

@ -2,6 +2,33 @@
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.4...dev)
### 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))
- Update PublicApiController, remove expensive and unused relationships ([2ecc3144](https://github.com/pixelfed/pixelfed/commit/2ecc3144))
- 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)
### New Features
@ -94,7 +121,6 @@
- Update CollectionController, limit max title and description length ([6e76cf4b](https://github.com/pixelfed/pixelfed/commit/6e76cf4b))
- Update collection components, fix title/description padding/overflow bug and add title/description limit and input counter ([6e4272a8](https://github.com/pixelfed/pixelfed/commit/6e4272a8))
- Update Media model, fix thumbnail cdn paths ([9888af12](https://github.com/pixelfed/pixelfed/commit/9888af12))
- ([](https://github.com/pixelfed/pixelfed/commit/))
## [v0.11.3 (2022-05-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.2...v0.11.3)

View file

@ -0,0 +1,181 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Storage;
use App\Profile;
use App\User;
use App\Instance;
use App\Util\ActivityPub\Helpers;
use Symfony\Component\HttpKernel\Exception\HttpException;
class SendUpdateActor extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ap:update-actors {--force}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send Update Actor activities to known remote servers to force updates';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$totalUserCount = Profile::whereNotNull('user_id')->count();
$totalInstanceCount = Instance::count();
$this->info('Found ' . $totalUserCount . ' local accounts and ' . $totalInstanceCount . ' remote instances');
$task = $this->choice(
'What do you want to do?',
[
'View top instances',
'Send updates to an instance'
],
0
);
if($task === 'View top instances') {
$this->table(
['domain', 'user_count', 'last_synced'],
Instance::orderByDesc('user_count')->take(20)->get(['domain', 'user_count', 'actors_last_synced_at'])->toArray()
);
return Command::SUCCESS;
} else {
$domain = $this->anticipate('Enter the instance domain', function ($input) {
return Instance::where('domain', 'like', '%' . $input . '%')->pluck('domain')->toArray();
});
if(!$this->confirm('Are you sure you want to send actor updates to ' . $domain . '?')) {
return;
}
if($cur = Instance::whereDomain($domain)->whereNotNull('actors_last_synced_at')->first()) {
if(!$this->option('force')) {
$this->error('ERROR: Cannot re-sync this instance, it was already synced on ' . $cur->actors_last_synced_at);
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();
if(!$sharedInbox) {
$this->error('ERROR: Cannot find the sharedInbox of ' . $domain);
return;
}
$url = $sharedInbox->sharedInbox;
$this->line(' ');
$this->info('Found sharedInbox: ' . $url);
$bar = $this->output->createProgressBar($totalUserCount);
$bar->start();
$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;
}
$body = $this->updateObject($profile);
try {
Helpers::sendSignedObject($profile, $url, $body);
} catch (HttpException $e) {
continue;
}
$bar->advance();
}
});
$bar->finish();
$this->line(' ');
$instance = Instance::whereDomain($domain)->firstOrFail();
$instance->actors_last_synced_at = now();
$instance->save();
$this->info('Finished!');
return Command::SUCCESS;
}
return Command::SUCCESS;
}
protected function updateObject($profile)
{
return [
'@context' => [
'https://w3id.org/security/v1',
'https://www.w3.org/ns/activitystreams',
[
'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
],
],
'id' => $profile->permalink('#updates/' . time()),
'actor' => $profile->permalink(),
'type' => 'Update',
'object' => $this->actorObject($profile)
];
}
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();
return [
'id' => $permalink,
'type' => 'Person',
'following' => $permalink . '/following',
'followers' => $permalink . '/followers',
'inbox' => $permalink . '/inbox',
'outbox' => $permalink . '/outbox',
'preferredUsername' => $profile->username,
'name' => $profile->name,
'summary' => $profile->bio,
'url' => $profile->url(),
'manuallyApprovesFollowers' => (bool) $profile->is_private,
'publicKey' => [
'id' => $permalink . '#main-key',
'owner' => $permalink,
'publicKeyPem' => $profile->public_key,
],
'icon' => [
'type' => 'Image',
'mediaType' => 'image/jpeg',
'url' => $profile->avatarUrl(),
],
'endpoints' => [
'sharedInbox' => config('app.url') . '/f/inbox'
]
];
}
}

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

@ -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)
{

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

@ -290,7 +290,7 @@ trait AdminReportController
->save();
Cache::forget('profiles:private');
DeleteAccountPipeline::dispatch($user)->onQueue('high');
DeleteAccountPipeline::dispatch($user);
return;
}

View file

@ -222,7 +222,7 @@ trait AdminUserController
->save();
Cache::forget('profiles:private');
DeleteAccountPipeline::dispatch($user)->onQueue('high');
DeleteAccountPipeline::dispatch($user);
$msg = "Successfully deleted {$user->username}!";
$request->session()->flash('status', $msg);

View file

@ -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,

View file

@ -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,
@ -2449,12 +2449,12 @@ class ApiV1Controller extends Controller
->limit($limit)
->get()
->map(function($like) {
$account = AccountService::getMastodon($like->profile_id);
$account = AccountService::getMastodon($like->profile_id, true);
$account['follows'] = isset($like->created_at);
return $account;
})
->filter(function($account) use($user) {
return $account && isset($account['id']) && $account['id'] != $user->profile_id;
return $account && isset($account['id']);
})
->values();
@ -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();
@ -3109,7 +3116,7 @@ class ApiV1Controller extends Controller
});
$ids = $ids->map(function($profile) {
return AccountService::getMastodon($profile->id, true);
return AccountService::get($profile->id, true);
})
->filter(function($profile) use($pid) {
return $profile && isset($profile['id']);

View file

@ -90,7 +90,7 @@ class BaseApiController extends Controller
if(empty($res) && !Cache::has('pf:services:notifications:hasSynced:'.$pid)) {
Cache::put('pf:services:notifications:hasSynced:'.$pid, 1, 1209600);
NotificationService::warmCache($pid, 400, true);
NotificationService::warmCache($pid, 100, true);
}
return response()->json($res);

View file

@ -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;

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

@ -0,0 +1,318 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Portfolio;
use Cache;
use DB;
use App\Status;
use App\User;
use App\Services\AccountService;
use App\Services\StatusService;
class PortfolioController extends Controller
{
public function index(Request $request)
{
return view('portfolio.index');
}
public function show(Request $request, $username)
{
$user = User::whereUsername($username)->first();
if(!$user) {
return view('portfolio.404');
}
$portfolio = Portfolio::whereUserId($user->id)->firstOrFail();
$user = AccountService::get($user->profile_id);
if($user['locked']) {
return view('portfolio.404');
}
if($portfolio->active != true) {
if(!$request->user()) {
return view('portfolio.404');
}
if($request->user()->profile_id == $user['id']) {
return redirect(config('portfolio.path') . '/settings');
}
return view('portfolio.404');
}
return view('portfolio.show', compact('user', 'portfolio'));
}
public function showPost(Request $request, $username, $id)
{
$authed = $request->user();
$post = StatusService::get($id);
if(!$post) {
return view('portfolio.404');
}
$user = AccountService::get($post['account']['id']);
$portfolio = Portfolio::whereProfileId($user['id'])->first();
if($user['locked'] || $portfolio->active != true) {
return view('portfolio.404');
}
if(!$post || $post['visibility'] != 'public' || $post['pf_type'] != 'photo' || $user['id'] != $post['account']['id']) {
return view('portfolio.404');
}
return view('portfolio.show_post', compact('user', 'post', 'authed'));
}
public function myRedirect(Request $request)
{
abort_if(!$request->user(), 404);
$user = $request->user();
if(Portfolio::whereProfileId($user->profile_id)->exists() === false) {
$portfolio = new Portfolio;
$portfolio->profile_id = $user->profile_id;
$portfolio->user_id = $user->id;
$portfolio->active = false;
$portfolio->save();
}
$domain = config('portfolio.domain');
$path = config('portfolio.path');
$url = 'https://' . $domain . $path;
return redirect($url);
}
public function settings(Request $request)
{
if(!$request->user()) {
return redirect(route('home'));
}
$portfolio = Portfolio::whereUserId($request->user()->id)->first();
if(!$portfolio) {
$portfolio = new Portfolio;
$portfolio->user_id = $request->user()->id;
$portfolio->profile_id = $request->user()->profile_id;
$portfolio->save();
}
return view('portfolio.settings', compact('portfolio'));
}
public function store(Request $request)
{
abort_unless($request->user(), 404);
$this->validate($request, [
'profile_source' => 'required|in:recent,custom',
'layout' => 'required|in:grid,masonry',
'layout_container' => 'required|in:fixed,fluid'
]);
$portfolio = Portfolio::whereUserId($request->user()->id)->first();
if(!$portfolio) {
$portfolio = new Portfolio;
$portfolio->user_id = $request->user()->id;
$portfolio->profile_id = $request->user()->profile_id;
$portfolio->save();
}
$portfolio->active = $request->input('enabled') === 'on';
$portfolio->show_captions = $request->input('show_captions') === 'on';
$portfolio->show_license = $request->input('show_license') === 'on';
$portfolio->show_location = $request->input('show_location') === 'on';
$portfolio->show_timestamp = $request->input('show_timestamp') === 'on';
$portfolio->show_link = $request->input('show_link') === 'on';
$portfolio->profile_source = $request->input('profile_source');
$portfolio->show_avatar = $request->input('show_avatar') === 'on';
$portfolio->show_bio = $request->input('show_bio') === 'on';
$portfolio->profile_layout = $request->input('layout');
$portfolio->profile_container = $request->input('layout_container');
$portfolio->save();
return redirect('/' . $request->user()->username);
}
public function getFeed(Request $request, $id)
{
$user = AccountService::get($id, true);
if(!$user || !isset($user['id'])) {
return response()->json([], 404);
}
$portfolio = Portfolio::whereProfileId($user['id'])->first();
if(!$portfolio || !$portfolio->active) {
return response()->json([], 404);
}
if($portfolio->profile_source === 'custom' && $portfolio->metadata) {
return $this->getCustomFeed($portfolio);
}
return $this->getRecentFeed($user['id']);
}
protected function getCustomFeed($portfolio) {
if(!$portfolio->metadata['posts']) {
return response()->json([], 400);
}
return collect($portfolio->metadata['posts'])->map(function($p) {
return StatusService::get($p);
})
->filter(function($p) {
return $p && isset($p['account']);
})->values();
}
protected function getRecentFeed($id) {
$media = Cache::remember('portfolio:recent-feed:' . $id, 3600, function() use($id) {
return DB::table('media')
->whereProfileId($id)
->whereNotNull('status_id')
->groupBy('status_id')
->orderByDesc('id')
->take(50)
->pluck('status_id');
});
return $media->map(function($sid) use($id) {
return StatusService::get($sid);
})
->filter(function($post) {
return $post &&
isset($post['media_attachments']) &&
!empty($post['media_attachments']) &&
$post['pf_type'] === 'photo' &&
$post['visibility'] === 'public';
})
->take(24)
->values();
}
public function getSettings(Request $request)
{
abort_if(!$request->user(), 403);
$res = Portfolio::whereUserId($request->user()->id)->get();
if(!$res) {
return [];
}
return $res->map(function($p) {
return [
'url' => $p->url(),
'pid' => (string) $p->profile_id,
'active' => (bool) $p->active,
'show_captions' => (bool) $p->show_captions,
'show_license' => (bool) $p->show_license,
'show_location' => (bool) $p->show_location,
'show_timestamp' => (bool) $p->show_timestamp,
'show_link' => (bool) $p->show_link,
'show_avatar' => (bool) $p->show_avatar,
'show_bio' => (bool) $p->show_bio,
'profile_layout' => $p->profile_layout,
'profile_source' => $p->profile_source,
'metadata' => $p->metadata
];
})->first();
}
public function getAccountSettings(Request $request)
{
$this->validate($request, [
'id' => 'required|integer'
]);
$account = AccountService::get($request->input('id'));
abort_if(!$account, 404);
$p = Portfolio::whereProfileId($request->input('id'))->whereActive(1)->firstOrFail();
if(!$p) {
return [];
}
return [
'url' => $p->url(),
'show_captions' => (bool) $p->show_captions,
'show_license' => (bool) $p->show_license,
'show_location' => (bool) $p->show_location,
'show_timestamp' => (bool) $p->show_timestamp,
'show_link' => (bool) $p->show_link,
'show_avatar' => (bool) $p->show_avatar,
'show_bio' => (bool) $p->show_bio,
'profile_layout' => $p->profile_layout,
'profile_source' => $p->profile_source
];
}
public function storeSettings(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'profile_layout' => 'sometimes|in:grid,masonry,album'
]);
$res = Portfolio::whereUserId($request->user()->id)
->update($request->only([
'active',
'show_captions',
'show_license',
'show_location',
'show_timestamp',
'show_link',
'show_avatar',
'show_bio',
'profile_layout',
'profile_source'
]));
Cache::forget('portfolio:recent-feed:' . $request->user()->profile_id);
return 200;
}
public function storeCurated(Request $request)
{
abort_if(!$request->user(), 403);
$this->validate($request, [
'ids' => 'required|array|max:24'
]);
$pid = $request->user()->profile_id;
$ids = $request->input('ids');
Status::whereProfileId($pid)
->whereScope('public')
->whereIn('type', ['photo', 'photo:album'])
->findOrFail($ids);
$p = Portfolio::whereProfileId($pid)->firstOrFail();
$p->metadata = ['posts' => $ids];
$p->save();
Cache::forget('portfolio:recent-feed:' . $pid);
return $request->ids;
}
}

View file

@ -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()

View file

@ -364,7 +364,6 @@ class PublicApiController extends Controller
)
->whereNull(['in_reply_to_id', 'reblog_of_id'])
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
->with('profile', 'hashtags', 'mentions')
->whereLocal(true)
->whereScope('public')
->orderBy('id', 'desc')
@ -517,7 +516,6 @@ class PublicApiController extends Controller
->when($textOnlyReplies != true, function($q, $textOnlyReplies) {
return $q->whereNull('in_reply_to_id');
})
->with('profile', 'hashtags', 'mentions')
->where('id', $dir, $id)
->whereIn('profile_id', $following)
->whereIn('visibility',['public', 'unlisted', 'private'])
@ -564,7 +562,6 @@ class PublicApiController extends Controller
->when(!$textOnlyReplies, function($q, $textOnlyReplies) {
return $q->whereNull('in_reply_to_id');
})
->with('profile', 'hashtags', 'mentions')
->whereIn('profile_id', $following)
->whereIn('visibility',['public', 'unlisted', 'private'])
->orderBy('created_at', 'desc')

View file

@ -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');

View file

@ -29,7 +29,7 @@ use App\Services\ReblogService;
class StatusController extends Controller
{
public function show(Request $request, $username, int $id)
public function show(Request $request, $username, $id)
{
// redirect authed users to Metro 2.0
if($request->user()) {
@ -225,7 +225,7 @@ class StatusController extends Controller
StatusService::del($status->id, true);
if ($status->profile_id == $user->profile->id || $user->is_admin == true) {
Cache::forget('profile:status_count:'.$status->profile_id);
StatusDelete::dispatch($status);
StatusDelete::dispatchNow($status);
}
if($request->wantsJson()) {

View file

@ -2,7 +2,7 @@
namespace App\Http\Middleware;
use Fideloper\Proxy\TrustProxies as Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware

View file

@ -0,0 +1,74 @@
<?php
namespace App\Jobs\DeletePipeline;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Bookmark;
use App\DirectMessage;
use App\Like;
use App\Media;
use App\MediaTag;
use App\Mention;
use App\Report;
use App\Status;
use App\StatusHashtag;
use App\StatusView;
use App\Notification;
use App\Services\NetworkTimelineService;
use App\Services\StatusService;
use App\Jobs\ProfilePipeline\DecrementPostCount;
use App\Jobs\MediaPipeline\MediaDeletePipeline;
class DeleteRemoteStatusPipeline implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Status $status)
{
$this->status = $status->withoutRelations();
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$status = $this->status;
NetworkTimelineService::del($status->id);
StatusService::del($status->id, true);
DecrementPostCount::dispatchNow($status->profile_id);
Bookmark::whereStatusId($status->id)->delete();
Notification::whereItemType('App\Status')
->whereItemId($status->id)
->forceDelete();
DirectMessage::whereStatusId($status->id)->delete();
Like::whereStatusId($status->id)->forceDelete();
MediaTag::whereStatusId($status->id)->delete();
Media::whereStatusId($status->id)
->get()
->each(function($media) {
MediaDeletePipeline::dispatchNow($media);
});
Mention::whereStatusId($status->id)->forceDelete();
Report::whereObjectType('App\Status')->whereObjectId($status->id)->delete();
StatusHashtag::whereStatusId($status->id)->delete();
StatusView::whereStatusId($status->id)->delete();
Status::whereReblogOfId($status->id)->forceDelete();
$status->delete();
}
}

View file

@ -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);

View file

@ -70,58 +70,6 @@ class InboxValidator implements ShouldQueue
return;
}
if( $payload['type'] === 'Delete' &&
( ( is_string($payload['object']) &&
$payload['object'] === $payload['actor'] ) ||
( is_array($payload['object']) &&
isset($payload['object']['id'], $payload['object']['type']) &&
$payload['object']['type'] === 'Person' &&
$payload['actor'] === $payload['object']['id']
))
) {
$actor = $payload['actor'];
$hash = strlen($actor) <= 48 ?
'b:' . base64_encode($actor) :
'h:' . hash('sha256', $actor);
$lockKey = 'ap:inbox:actor-delete-exists:lock:' . $hash;
Cache::lock($lockKey, 10)->block(5, function () use(
$headers,
$payload,
$actor,
$hash,
$profile
) {
$key = 'ap:inbox:actor-delete-exists:' . $hash;
$actorDelete = Cache::remember($key, now()->addMinutes(15), function() use($actor) {
return Profile::whereRemoteUrl($actor)
->whereNotNull('domain')
->exists();
});
if($actorDelete) {
if($this->verifySignature($headers, $profile, $payload) == true) {
Cache::set($key, false);
$profile = Profile::whereNotNull('domain')
->whereNull('status')
->whereRemoteUrl($actor)
->first();
if($profile) {
DeleteRemoteProfilePipeline::dispatchNow($profile);
}
return;
} else {
// Signature verification failed, exit.
return;
}
} else {
// Remote user doesn't exist, exit early.
return;
}
});
return;
}
if($profile->status != null) {
return;
}
@ -228,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);

View file

@ -66,57 +66,6 @@ class InboxWorker implements ShouldQueue
return;
}
if( $payload['type'] === 'Delete' &&
( ( is_string($payload['object']) &&
$payload['object'] === $payload['actor'] ) ||
( is_array($payload['object']) &&
isset($payload['object']['id'], $payload['object']['type']) &&
$payload['object']['type'] === 'Person' &&
$payload['actor'] === $payload['object']['id']
))
) {
$actor = $payload['actor'];
$hash = strlen($actor) <= 48 ?
'b:' . base64_encode($actor) :
'h:' . hash('sha256', $actor);
$lockKey = 'ap:inbox:actor-delete-exists:lock:' . $hash;
Cache::lock($lockKey, 10)->block(5, function () use(
$headers,
$payload,
$actor,
$hash
) {
$key = 'ap:inbox:actor-delete-exists:' . $hash;
$actorDelete = Cache::remember($key, now()->addMinutes(15), function() use($actor) {
return Profile::whereRemoteUrl($actor)
->whereNotNull('domain')
->exists();
});
if($actorDelete) {
if($this->verifySignature($headers, $payload) == true) {
Cache::set($key, false);
$profile = Profile::whereNotNull('domain')
->whereNull('status')
->whereRemoteUrl($actor)
->first();
if($profile) {
DeleteRemoteProfilePipeline::dispatchNow($profile);
}
return;
} else {
// Signature verification failed, exit.
return;
}
} else {
// Remote user doesn't exist, exit early.
return;
}
});
return;
}
if($this->verifySignature($headers, $payload) == true) {
(new Inbox($headers, $profile, $payload))->handle();
return;
@ -217,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);

View file

@ -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) {

View file

@ -5,12 +5,19 @@ namespace App\Jobs\StatusPipeline;
use DB, Storage;
use App\{
AccountInterstitial,
Bookmark,
CollectionItem,
DirectMessage,
Like,
Media,
MediaTag,
Mention,
Notification,
Report,
Status,
StatusArchived,
StatusHashtag,
StatusView
};
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
@ -28,7 +35,7 @@ use GuzzleHttp\Promise;
use App\Util\ActivityPub\HttpSignature;
use App\Services\CollectionService;
use App\Services\StatusService;
use App\Services\MediaStorageService;
use App\Jobs\MediaPipeline\MediaDeletePipeline;
class StatusDelete implements ShouldQueue
{
@ -71,75 +78,65 @@ class StatusDelete implements ShouldQueue
}
if(config_cache('federation.activitypub.enabled') == true) {
$this->fanoutDelete($status);
return $this->fanoutDelete($status);
} else {
$this->unlinkRemoveMedia($status);
return $this->unlinkRemoveMedia($status);
}
}
public function unlinkRemoveMedia($status)
{
foreach ($status->media as $media) {
MediaStorageService::delete($media, true);
}
if($status->in_reply_to_id) {
DB::transaction(function() use($status) {
$parent = Status::findOrFail($status->in_reply_to_id);
--$parent->reply_count;
$parent->save();
});
}
DB::transaction(function() use($status) {
CollectionItem::whereObjectType('App\Status')
->whereObjectId($status->id)
->get()
->each(function($col) {
$id = $col->collection_id;
$sid = $col->object_id;
$col->delete();
CollectionService::removeItem($id, $sid);
});
Media::whereStatusId($status->id)
->get()
->each(function($media) {
MediaDeletePipeline::dispatchNow($media);
});
DB::transaction(function() use($status) {
$comments = Status::where('in_reply_to_id', $status->id)->get();
foreach ($comments as $comment) {
$comment->in_reply_to_id = null;
$comment->save();
Notification::whereItemType('App\Status')
->whereItemId($comment->id)
->delete();
}
$status->likes()->delete();
Notification::whereItemType('App\Status')
->whereItemId($status->id)
->delete();
StatusHashtag::whereStatusId($status->id)->delete();
Report::whereObjectType('App\Status')
->whereObjectId($status->id)
->delete();
MediaTag::where('status_id', $status->id)
->cursor()
->each(function($tag) {
Notification::where('item_type', 'App\MediaTag')
->where('item_id', $tag->id)
->forceDelete();
$tag->delete();
});
AccountInterstitial::where('item_type', 'App\Status')
->where('item_id', $status->id)
->delete();
if($status->in_reply_to_id) {
$parent = Status::findOrFail($status->in_reply_to_id);
--$parent->reply_count;
$parent->save();
}
$status->forceDelete();
});
Bookmark::whereStatusId($status->id)->delete();
return true;
CollectionItem::whereObjectType('App\Status')
->whereObjectId($status->id)
->get()
->each(function($col) {
CollectionService::removeItem($col->collection_id, $col->object_id);
$col->delete();
});
DirectMessage::whereStatusId($status->id)->delete();
Like::whereStatusId($status->id)->delete();
MediaTag::where('status_id', $status->id)->delete();
Mention::whereStatusId($status->id)->forceDelete();
Notification::whereItemType('App\Status')
->whereItemId($status->id)
->forceDelete();
Report::whereObjectType('App\Status')
->whereObjectId($status->id)
->delete();
StatusArchived::whereStatusId($status->id)->delete();
StatusHashtag::whereStatusId($status->id)->delete();
StatusView::whereStatusId($status->id)->delete();
Status::whereInReplyToId($status->id)->update(['in_reply_to_id' => null]);
AccountInterstitial::where('item_type', 'App\Status')
->where('item_id', $status->id)
->delete();
$status->forceDelete();
return 1;
}
protected function fanoutDelete($status)
public function fanoutDelete($status)
{
$audience = $status->profile->getAudienceInbox();
$profile = $status->profile;
@ -189,5 +186,6 @@ class StatusDelete implements ShouldQueue
$promise->wait();
return 1;
}
}

View file

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

39
app/Models/Portfolio.php Normal file
View file

@ -0,0 +1,39 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Services\AccountService;
class Portfolio extends Model
{
use HasFactory;
public $fillable = [
'active',
'show_captions',
'show_license',
'show_location',
'show_timestamp',
'show_link',
'show_avatar',
'show_bio',
'profile_layout',
'profile_source'
];
protected $casts = [
'metadata' => 'json'
];
public function url()
{
$account = AccountService::get($this->profile_id);
if(!$account) {
return null;
}
return 'https://' . config('portfolio.domain') . config('portfolio.path') . '/' . $account['username'];
}
}

View file

@ -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);

View file

@ -29,6 +29,7 @@ use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Facades\Validator;
class AppServiceProvider extends ServiceProvider
{
@ -54,6 +55,7 @@ class AppServiceProvider extends ServiceProvider
Horizon::auth(function ($request) {
return Auth::check() && $request->user()->is_admin;
});
Validator::includeUnvalidatedArrayKeys();
}
/**

View file

@ -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)

View file

@ -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'
];

View file

@ -236,16 +236,19 @@ class MediaStorageService {
$tmpBase = storage_path('app/remcache/');
$tmpPath = 'avatar_' . $avatar->profile_id . '-' . $path;
$tmpName = $tmpBase . $tmpPath;
$data = file_get_contents($url, false, null, 0, $head['length']);
$data = @file_get_contents($url, false, null, 0, $head['length']);
if(!$data) {
return;
}
file_put_contents($tmpName, $data);
$disk = Storage::disk($driver);
$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 = $permalink;
$avatar->cdn_url = $local ? config('app.url') . $permalink : $permalink;
$avatar->size = $head['length'];
$avatar->change_count = $avatar->change_count + 1;
$avatar->last_fetched_at = now();

View file

@ -23,14 +23,11 @@ class ProfileTransformer extends Fractal\TransformerAbstract
'followers' => $profile->permalink('/followers'),
'inbox' => $profile->permalink('/inbox'),
'outbox' => $profile->permalink('/outbox'),
//'featured' => $profile->permalink('/collections/featured'),
'preferredUsername' => $profile->username,
'name' => $profile->name,
'summary' => $profile->bio,
'url' => $profile->url(),
'manuallyApprovesFollowers' => (bool) $profile->is_private,
// 'follower_count' => $profile->followers()->count(),
// 'following_count' => $profile->following()->count(),
'publicKey' => [
'id' => $profile->permalink().'#main-key',
'owner' => $profile->permalink(),

View file

@ -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;

View file

@ -419,9 +419,8 @@ class Helpers {
$cw = true;
}
$statusLockKey = 'helpers:status-lock:' . hash('sha256', $res['id']);
$status = Cache::lock($statusLockKey)
->get(function () use(
if($res['type'] === 'Question') {
$status = self::storePoll(
$profile,
$res,
$url,
@ -430,24 +429,11 @@ class Helpers {
$cw,
$scope,
$id
) {
if($res['type'] === 'Question') {
$status = self::storePoll(
$profile,
$res,
$url,
$ts,
$reply_to,
$cw,
$scope,
$id
);
return $status;
}
return self::storeStatus($url, $profile, $res);
});
);
return $status;
} else {
$status = self::storeStatus($url, $profile, $res);
}
return $status;
}
@ -756,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,

View file

@ -24,6 +24,7 @@ use Illuminate\Support\Str;
use App\Jobs\LikePipeline\LikePipeline;
use App\Jobs\FollowPipeline\FollowPipeline;
use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
use App\Jobs\DeletePipeline\DeleteRemoteStatusPipeline;
use App\Jobs\StoryPipeline\StoryExpire;
use App\Jobs\StoryPipeline\StoryFetch;
@ -191,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') {
@ -199,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')
) {
@ -622,7 +625,7 @@ class Inbox
if(!$profile || $profile->private_key != null) {
return;
}
DeleteRemoteProfilePipeline::dispatchNow($profile);
DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('delete');
return;
} else {
if(!isset($obj['id'], $this->payload['object'], $this->payload['object']['id'])) {
@ -643,7 +646,7 @@ class Inbox
if(!$profile || $profile->private_key != null) {
return;
}
DeleteRemoteProfilePipeline::dispatchNow($profile);
DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('delete');
return;
break;
@ -660,18 +663,7 @@ class Inbox
if(!$status) {
return;
}
NetworkTimelineService::del($status->id);
StatusService::del($status->id, true);
Notification::whereActorId($profile->id)
->whereItemType('App\Status')
->whereItemId($status->id)
->forceDelete();
$status->directMessage()->delete();
$status->media()->delete();
$status->likes()->delete();
$status->shares()->delete();
$status->delete();
DecrementPostCount::dispatch($profile->id)->onQueue('low');
DeleteRemoteStatusPipeline::dispatch($status)->onQueue('delete');
return;
break;
@ -737,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;
@ -796,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)

View file

@ -5,7 +5,7 @@
"license": "AGPL-3.0-only",
"type": "project",
"require": {
"php": "^7.4|^8.0",
"php": "^8.0.2|^8.1",
"ext-bcmath": "*",
"ext-ctype": "*",
"ext-curl": "*",
@ -18,37 +18,36 @@
"brick/math": "^0.9.3",
"buzz/laravel-h-captcha": "1.0.3",
"doctrine/dbal": "^2.7",
"fideloper/proxy": "^4.0",
"fruitcake/laravel-cors": "^2.0",
"intervention/image": "^2.4",
"jenssegers/agent": "^2.6",
"laravel/framework": "^8.0",
"laravel/framework": "^9.0",
"laravel/helpers": "^1.1",
"laravel/horizon": "^5.0",
"laravel/passport": "^10.0",
"laravel/tinker": "^2.0",
"laravel/ui": "^2.0|^3.4",
"league/flysystem-aws-s3-v3": "~1.0",
"league/flysystem-cached-adapter": "~1.0",
"league/flysystem-aws-s3-v3": "^3.0",
"league/iso3166": "^2.1|^4.0",
"pbmedia/laravel-ffmpeg": "^7.0",
"pbmedia/laravel-ffmpeg": "^8.0",
"phpseclib/phpseclib": "~2.0",
"pixelfed/fractal": "^0.18.0",
"pixelfed/laravel-snowflake": "^2.0",
"pixelfed/zttp": "^0.4",
"pixelfed/zttp": "^0.5",
"pragmarx/google2fa": "^8.0",
"predis/predis": "^1.1",
"spatie/laravel-backup": "^6.0.0",
"spatie/laravel-image-optimizer": "^1.1",
"stevebauman/purify": "3.0.*",
"symfony/http-kernel": "5.4.8"
"spatie/laravel-backup": "^8.0.0",
"spatie/laravel-image-optimizer": "^1.7",
"stevebauman/purify": "4.0.*",
"symfony/http-client": "^6.1",
"symfony/http-kernel": "^6.0.0",
"symfony/mailgun-mailer": "^6.1"
},
"require-dev": {
"brianium/paratest": "^6.1",
"facade/ignition": "^2.3.6",
"laravel/telescope": "^4.9",
"mockery/mockery": "^1.0",
"nunomaduro/collision": "^5.0",
"nunomaduro/collision": "^6.1",
"phpunit/phpunit": "^9.0"
},
"autoload": {

3279
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -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),

View file

@ -53,6 +53,7 @@ return [
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
'throw' => true,
],
's3' => [
@ -65,6 +66,7 @@ return [
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => true,
],
'spaces' => [
@ -79,6 +81,7 @@ return [
'CacheControl' => 'max-age=31536000'
],
'root' => env('DO_SPACES_ROOT','/'),
'throw' => true,
],
'backup' => [

View file

@ -27,7 +27,7 @@ return [
],
'network' => [
'cached' => env('PF_NETWORK_TIMELINE') ? env('INSTANCE_NETWORK_TIMELINE_CACHED', false) : false,
'cached' => env('PF_NETWORK_TIMELINE') ? env('INSTANCE_NETWORK_TIMELINE_CACHED', true) : false,
'cache_dropoff' => env('INSTANCE_NETWORK_TIMELINE_CACHE_DROPOFF', 100),
'max_hours_old' => env('INSTANCE_NETWORK_TIMELINE_CACHE_MAX_HOUR_INGEST', 6)
]

View file

@ -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())),
];

31
config/portfolio.php Normal file
View file

@ -0,0 +1,31 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Portfolio Domain
|--------------------------------------------------------------------------
|
| This value is the domain used for the portfolio feature. Only change
| the default value if you have a subdomain configured. You must use
| a subdomain on the same app domain.
|
*/
'domain' => env('PORTFOLIO_DOMAIN', config('pixelfed.domain.app')),
/*
|--------------------------------------------------------------------------
| Portfolio Path
|--------------------------------------------------------------------------
|
| This value is the path used for the portfolio feature. Only change
| the default value if you have a subdomain configured. If you want
| to use the root path of the subdomain, leave this value empty.
|
| WARNING: SETTING THIS VALUE WITHOUT A SUBDOMAIN COULD BREAK YOUR
| INSTANCE, SO ONLY CHANGE THIS IF YOU KNOW WHAT YOU'RE DOING.
|
*/
'path' => env('PORTFOLIO_PATH', '/i/portfolio'),
];

View file

@ -0,0 +1,45 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePortfoliosTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('portfolios', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('user_id')->nullable()->unique()->index();
$table->bigInteger('profile_id')->unsigned()->unique()->index();
$table->boolean('active')->nullable()->index();
$table->boolean('show_captions')->default(true)->nullable();
$table->boolean('show_license')->default(true)->nullable();
$table->boolean('show_location')->default(true)->nullable();
$table->boolean('show_timestamp')->default(true)->nullable();
$table->boolean('show_link')->default(true)->nullable();
$table->string('profile_source')->default('recent')->nullable();
$table->boolean('show_avatar')->default(true)->nullable();
$table->boolean('show_bio')->default(true)->nullable();
$table->string('profile_layout')->default('grid')->nullable();
$table->string('profile_container')->default('fixed')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('portfolios');
}
}

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddReblogOfIdIndexToStatusesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('statuses', function (Blueprint $table) {
$table->index('in_reply_to_id');
$table->index('reblog_of_id');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('statuses', function (Blueprint $table) {
$table->dropIndex('statuses_in_reply_to_id_index');
$table->dropIndex('statuses_reblog_of_id_index');
});
}
}

View file

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class RemoveOldCompoundIndexFromStatusesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('statuses', function (Blueprint $table) {
$sc = Schema::getConnection()->getDoctrineSchemaManager();
if(array_key_exists('statuses_in_reply_to_id_reblog_of_id_index', $sc->listTableIndexes('statuses'))) {
$table->dropIndex('statuses_in_reply_to_id_reblog_of_id_index');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('statuses', function (Blueprint $table) {
//
});
}
}

View file

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddStatusIdIndexToBookmarksTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('bookmarks', function (Blueprint $table) {
$table->index('status_id');
$table->index('profile_id');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View file

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddStatusIdIndexToDirectMessagesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('direct_messages', function (Blueprint $table) {
$table->index('status_id');
$table->index('group_message');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View file

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddStatusIdIndexToMentionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('mentions', function (Blueprint $table) {
$table->index('status_id');
$table->index('profile_id');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View file

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddIndexesToReportsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('reports', function (Blueprint $table) {
$table->index('user_id');
$table->index('profile_id');
$table->index('object_id');
$table->index('object_type');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View file

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddItemIdAndItemTypeIndexesToNotificationsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('notifications', function (Blueprint $table) {
$table->index('item_id');
$table->index('item_type');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('notifications', function (Blueprint $table) {
//
});
}
}

View file

@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Cache;
use App\Services\AccountService;
use App\Avatar;
class FixCdnUrlInAvatarsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$baseUrl = 'https://' . config('pixelfed.domain.app');
Avatar::whereNotNull('cdn_url')
->chunk(50, function($avatars) use($baseUrl) {
foreach($avatars as $avatar) {
if(substr($avatar->cdn_url, 0, 23) === '/storage/cache/avatars/') {
$avatar->cdn_url = $baseUrl . $avatar->cdn_url;
$avatar->save();
}
Cache::forget('avatar:' . $avatar->profile_id);
AccountService::del($avatar->profile_id);
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
}
}

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('instances', function (Blueprint $table) {
$table->timestamp('actors_last_synced_at')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('instances', function (Blueprint $table) {
$table->dropColumn('actors_last_synced_at');
});
}
};

BIN
public/css/admin.css vendored

Binary file not shown.

BIN
public/css/app.css vendored

Binary file not shown.

BIN
public/css/appdark.css vendored

Binary file not shown.

BIN
public/css/landing.css vendored

Binary file not shown.

BIN
public/css/portfolio.css vendored Normal file

Binary file not shown.

BIN
public/js/admin.js vendored

Binary file not shown.

BIN
public/js/app.js vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/compose-llsjbikoc.js vendored Normal file

Binary file not shown.

BIN
public/js/compose.js vendored

Binary file not shown.

Binary file not shown.

BIN
public/js/daci-llsjbikoc.js vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/dffc-llsjbikoc.js vendored Normal file

Binary file not shown.

BIN
public/js/direct.js vendored

Binary file not shown.

Binary file not shown.

BIN
public/js/discover-llsjbikoc.js vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
public/js/dms-llsjbikoc.js vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
public/js/dmsg-llsjbikoc.js vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
public/js/dmyh-llsjbikoc.js vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
public/js/dmym-llsjbikoc.js vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
public/js/dsfc-llsjbikoc.js vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
public/js/dssc-llsjbikoc.js vendored Normal file

Binary file not shown.

BIN
public/js/hashtag.js vendored

Binary file not shown.

Binary file not shown.

BIN
public/js/home-llsjbikoc.js vendored Normal file

Binary file not shown.

BIN
public/js/installer.js vendored

Binary file not shown.

Binary file not shown.

BIN
public/js/manifest.js vendored

Binary file not shown.

BIN
public/js/portfolio.js vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
public/js/post-llsjbikoc.js vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
public/js/profile-llsjbikoc.js vendored Normal file

Binary file not shown.

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