This commit is contained in:
Christian Winther 2024-02-10 00:23:53 +00:00
commit 3d6efd098d
73 changed files with 1582 additions and 658 deletions

View file

@ -15,6 +15,7 @@
- Added User Domain Blocks ([#4834](https://github.com/pixelfed/pixelfed/pull/4834)) ([fa0380ac](https://github.com/pixelfed/pixelfed/commit/fa0380ac)) - Added User Domain Blocks ([#4834](https://github.com/pixelfed/pixelfed/pull/4834)) ([fa0380ac](https://github.com/pixelfed/pixelfed/commit/fa0380ac))
- Added Parental Controls ([#4862](https://github.com/pixelfed/pixelfed/pull/4862)) ([c91f1c59](https://github.com/pixelfed/pixelfed/commit/c91f1c59)) - Added Parental Controls ([#4862](https://github.com/pixelfed/pixelfed/pull/4862)) ([c91f1c59](https://github.com/pixelfed/pixelfed/commit/c91f1c59))
- Added Forgot Email Feature ([67c650b1](https://github.com/pixelfed/pixelfed/commit/67c650b1)) - Added Forgot Email Feature ([67c650b1](https://github.com/pixelfed/pixelfed/commit/67c650b1))
- Added S3 IG Import Media Storage support ([#4891](https://github.com/pixelfed/pixelfed/pull/4891)) ([081360b9](https://github.com/pixelfed/pixelfed/commit/081360b9))
### Federation ### Federation
- Update Privacy Settings, add support for Mastodon `indexable` search flag ([fc24630e](https://github.com/pixelfed/pixelfed/commit/fc24630e)) - Update Privacy Settings, add support for Mastodon `indexable` search flag ([fc24630e](https://github.com/pixelfed/pixelfed/commit/fc24630e))
@ -89,6 +90,21 @@
- Update meta tags, improve descriptions and seo/og tags ([fd44c80c](https://github.com/pixelfed/pixelfed/commit/fd44c80c)) - Update meta tags, improve descriptions and seo/og tags ([fd44c80c](https://github.com/pixelfed/pixelfed/commit/fd44c80c))
- Update login view, add email prefill logic ([d76f0168](https://github.com/pixelfed/pixelfed/commit/d76f0168)) - Update login view, add email prefill logic ([d76f0168](https://github.com/pixelfed/pixelfed/commit/d76f0168))
- Update LoginController, fix captcha validation error message ([0325e171](https://github.com/pixelfed/pixelfed/commit/0325e171)) - Update LoginController, fix captcha validation error message ([0325e171](https://github.com/pixelfed/pixelfed/commit/0325e171))
- Update ApiV1Controller, properly cast boolean sensitive parameter. Fixes #4888 ([0aff126a](https://github.com/pixelfed/pixelfed/commit/0aff126a))
- Update AccountImport.vue, fix new IG export format ([59aa6a4b](https://github.com/pixelfed/pixelfed/commit/59aa6a4b))
- Update TransformImports command, fix import service condition ([32c59f04](https://github.com/pixelfed/pixelfed/commit/32c59f04))
- Update AP helpers, more efficently update post count ([7caed381](https://github.com/pixelfed/pixelfed/commit/7caed381))
- Update AP helpers, refactor post count decrement logic ([b81ae577](https://github.com/pixelfed/pixelfed/commit/b81ae577))
- Update AP helpers, fix sensitive bug ([00ed330c](https://github.com/pixelfed/pixelfed/commit/00ed330c))
- Update NotificationEpochUpdatePipeline, use more efficient query ([4d401389](https://github.com/pixelfed/pixelfed/commit/4d401389))
- Update notification pipelines, fix non-local saving ([fa97a1f3](https://github.com/pixelfed/pixelfed/commit/fa97a1f3))
- Update NodeinfoService, disable redirects ([240e6bbe](https://github.com/pixelfed/pixelfed/commit/240e6bbe))
- Update Instance model, add entity casts ([289cad47](https://github.com/pixelfed/pixelfed/commit/289cad47))
- Update FetchNodeinfoPipeline, use more efficient dispatch ([ac01f51a](https://github.com/pixelfed/pixelfed/commit/ac01f51a))
- Update horizon.php config ([1e3acade](https://github.com/pixelfed/pixelfed/commit/1e3acade))
- Update PublicApiController, consume InstanceService blocked domains for account and statuses endpoints ([01b33fb3](https://github.com/pixelfed/pixelfed/commit/01b33fb3))
- Update ApiV1Controller, enforce blocked instance domain logic ([5b284cac](https://github.com/pixelfed/pixelfed/commit/5b284cac))
- Update ApiV2Controller, add vapid key to instance object. Thanks thisismissem! ([4d02d6f1](https://github.com/pixelfed/pixelfed/commit/4d02d6f1))
- ([](https://github.com/pixelfed/pixelfed/commit/)) - ([](https://github.com/pixelfed/pixelfed/commit/))
## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9) ## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9)

View file

@ -0,0 +1,57 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\AccountService;
use App\Services\Account\AccountStatService;
use App\Status;
use App\Profile;
class AccountPostCountStatUpdate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:account-post-count-stat-update';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update post counts from recent activities';
/**
* Execute the console command.
*/
public function handle()
{
$ids = AccountStatService::getAllPostCountIncr();
if(!$ids || !count($ids)) {
return;
}
foreach($ids as $id) {
$acct = AccountService::get($id, true);
if(!$acct) {
AccountStatService::removeFromPostCount($id);
continue;
}
$statusCount = Status::whereProfileId($id)->count();
if($statusCount != $acct['statuses_count']) {
$profile = Profile::find($id);
if(!$profile) {
AccountStatService::removeFromPostCount($id);
continue;
}
$profile->status_count = $statusCount;
$profile->save();
AccountService::del($id);
}
AccountStatService::removeFromPostCount($id);
}
return;
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\ImportPost;
use App\Jobs\ImportPipeline\ImportMediaToCloudPipeline;
use function Laravel\Prompts\progress;
class ImportUploadMediaToCloudStorage extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:import-upload-media-to-cloud-storage {--limit=500}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Migrate media imported from Instagram to S3 cloud storage.';
/**
* Execute the console command.
*/
public function handle()
{
if(
(bool) config('import.instagram.storage.cloud.enabled') === false ||
(bool) config_cache('pixelfed.cloud_storage') === false
) {
$this->error('Aborted. Cloud storage is not enabled for IG imports.');
return;
}
$limit = $this->option('limit');
$progress = progress(label: 'Migrating import media', steps: $limit);
$progress->start();
$posts = ImportPost::whereUploadedToS3(false)->take($limit)->get();
foreach($posts as $post) {
ImportMediaToCloudPipeline::dispatch($post)->onQueue('low');
$progress->advance();
}
$progress->finish();
}
}

View file

@ -0,0 +1,298 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Instance;
use App\Profile;
use App\Services\InstanceService;
use App\Jobs\InstancePipeline\FetchNodeinfoPipeline;
use function Laravel\Prompts\select;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\progress;
use function Laravel\Prompts\search;
use function Laravel\Prompts\table;
class InstanceManager extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:instance-manager';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Manage Instances';
/**
* Execute the console command.
*/
public function handle()
{
$action = select(
'What action do you want to perform?',
[
'Recalculate Stats',
'Ban Instance',
'Unlist Instance',
'Unlisted Instances',
'Banned Instances',
'Unban Instance',
'Relist Instance',
],
);
switch($action) {
case 'Recalculate Stats':
return $this->recalculateStats();
break;
case 'Unlisted Instances':
return $this->viewUnlistedInstances();
break;
case 'Banned Instances':
return $this->viewBannedInstances();
break;
case 'Unlist Instance':
return $this->unlistInstance();
break;
case 'Ban Instance':
return $this->banInstance();
break;
case 'Unban Instance':
return $this->unbanInstance();
break;
case 'Relist Instance':
return $this->relistInstance();
break;
}
}
protected function recalculateStats()
{
$instanceCount = Instance::count();
$confirmed = confirm('Do you want to recalculate stats for all ' . $instanceCount . ' instances?');
if(!$confirmed) {
$this->error('Aborting...');
exit;
}
$users = progress(
label: 'Updating instance stats...',
steps: Instance::all(),
callback: fn ($instance) => $this->updateInstanceStats($instance),
);
}
protected function updateInstanceStats($instance)
{
FetchNodeinfoPipeline::dispatch($instance)->onQueue('intbg');
}
protected function unlistInstance()
{
$id = search(
'Search by domain',
fn (string $value) => strlen($value) > 0
? Instance::whereUnlisted(false)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
: []
);
$instance = Instance::find($id);
if(!$instance) {
$this->error('Oops, an error occured');
exit;
}
$tbl = [
[
$instance->domain,
number_format($instance->status_count),
number_format($instance->user_count),
]
];
table(
['Domain', 'Status Count', 'User Count'],
$tbl
);
$confirmed = confirm('Are you sure you want to unlist this instance?');
if(!$confirmed) {
$this->error('Aborting instance unlisting');
exit;
}
$instance->unlisted = true;
$instance->save();
InstanceService::refresh();
$this->info('Successfully unlisted ' . $instance->domain . '!');
exit;
}
protected function relistInstance()
{
$id = search(
'Search by domain',
fn (string $value) => strlen($value) > 0
? Instance::whereUnlisted(true)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
: []
);
$instance = Instance::find($id);
if(!$instance) {
$this->error('Oops, an error occured');
exit;
}
$tbl = [
[
$instance->domain,
number_format($instance->status_count),
number_format($instance->user_count),
]
];
table(
['Domain', 'Status Count', 'User Count'],
$tbl
);
$confirmed = confirm('Are you sure you want to re-list this instance?');
if(!$confirmed) {
$this->error('Aborting instance re-listing');
exit;
}
$instance->unlisted = false;
$instance->save();
InstanceService::refresh();
$this->info('Successfully re-listed ' . $instance->domain . '!');
exit;
}
protected function banInstance()
{
$id = search(
'Search by domain',
fn (string $value) => strlen($value) > 0
? Instance::whereBanned(false)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
: []
);
$instance = Instance::find($id);
if(!$instance) {
$this->error('Oops, an error occured');
exit;
}
$tbl = [
[
$instance->domain,
number_format($instance->status_count),
number_format($instance->user_count),
]
];
table(
['Domain', 'Status Count', 'User Count'],
$tbl
);
$confirmed = confirm('Are you sure you want to ban this instance?');
if(!$confirmed) {
$this->error('Aborting instance ban');
exit;
}
$instance->banned = true;
$instance->save();
InstanceService::refresh();
$this->info('Successfully banned ' . $instance->domain . '!');
exit;
}
protected function unbanInstance()
{
$id = search(
'Search by domain',
fn (string $value) => strlen($value) > 0
? Instance::whereBanned(true)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
: []
);
$instance = Instance::find($id);
if(!$instance) {
$this->error('Oops, an error occured');
exit;
}
$tbl = [
[
$instance->domain,
number_format($instance->status_count),
number_format($instance->user_count),
]
];
table(
['Domain', 'Status Count', 'User Count'],
$tbl
);
$confirmed = confirm('Are you sure you want to unban this instance?');
if(!$confirmed) {
$this->error('Aborting instance unban');
exit;
}
$instance->banned = false;
$instance->save();
InstanceService::refresh();
$this->info('Successfully un-banned ' . $instance->domain . '!');
exit;
}
protected function viewBannedInstances()
{
$data = Instance::whereBanned(true)
->get(['domain', 'user_count', 'status_count'])
->map(function($d) {
return [
'domain' => $d->domain,
'user_count' => number_format($d->user_count),
'status_count' => number_format($d->status_count),
];
})
->toArray();
table(
['Domain', 'User Count', 'Status Count'],
$data
);
}
protected function viewUnlistedInstances()
{
$data = Instance::whereUnlisted(true)
->get(['domain', 'user_count', 'status_count', 'banned'])
->map(function($d) {
return [
'domain' => $d->domain,
'user_count' => number_format($d->user_count),
'status_count' => number_format($d->status_count),
'banned' => $d->banned ? '✅' : null
];
})
->toArray();
table(
['Domain', 'User Count', 'Status Count', 'Banned'],
$data
);
}
}

View file

@ -70,6 +70,11 @@ class TransformImports extends Command
} }
$idk = ImportService::getId($ip->user_id, $ip->creation_year, $ip->creation_month, $ip->creation_day); $idk = ImportService::getId($ip->user_id, $ip->creation_year, $ip->creation_month, $ip->creation_day);
if(!$idk) {
$ip->skip_missing_media = true;
$ip->save();
continue;
}
if(Storage::exists('imports/' . $id . '/' . $ip->filename) === false) { if(Storage::exists('imports/' . $id . '/' . $ip->filename) === false) {
ImportService::clearAttempts($profile->id); ImportService::clearAttempts($profile->id);

View file

@ -7,56 +7,60 @@ use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel class Kernel extends ConsoleKernel
{ {
/** /**
* The Artisan commands provided by your application. * The Artisan commands provided by your application.
* *
* @var array * @var array
*/ */
protected $commands = [ protected $commands = [
// //
]; ];
/** /**
* Define the application's command schedule. * Define the application's command schedule.
* *
* @param \Illuminate\Console\Scheduling\Schedule $schedule * @param \Illuminate\Console\Scheduling\Schedule $schedule
* *
* @return void * @return void
*/ */
protected function schedule(Schedule $schedule) protected function schedule(Schedule $schedule)
{ {
$schedule->command('media:optimize')->hourlyAt(40)->onOneServer(); $schedule->command('media:optimize')->hourlyAt(40)->onOneServer();
$schedule->command('media:gc')->hourlyAt(5)->onOneServer(); $schedule->command('media:gc')->hourlyAt(5)->onOneServer();
$schedule->command('horizon:snapshot')->everyFiveMinutes()->onOneServer(); $schedule->command('horizon:snapshot')->everyFiveMinutes()->onOneServer();
$schedule->command('story:gc')->everyFiveMinutes()->onOneServer(); $schedule->command('story:gc')->everyFiveMinutes()->onOneServer();
$schedule->command('gc:failedjobs')->dailyAt(3)->onOneServer(); $schedule->command('gc:failedjobs')->dailyAt(3)->onOneServer();
$schedule->command('gc:passwordreset')->dailyAt('09:41')->onOneServer(); $schedule->command('gc:passwordreset')->dailyAt('09:41')->onOneServer();
$schedule->command('gc:sessions')->twiceDaily(13, 23)->onOneServer(); $schedule->command('gc:sessions')->twiceDaily(13, 23)->onOneServer();
if (in_array(config_cache('pixelfed.cloud_storage'), ['1', true, 'true']) && config('media.delete_local_after_cloud')) { if (in_array(config_cache('pixelfed.cloud_storage'), ['1', true, 'true']) && config('media.delete_local_after_cloud')) {
$schedule->command('media:s3gc')->hourlyAt(15)->onOneServer(); $schedule->command('media:s3gc')->hourlyAt(15)->onOneServer();
} }
if (config('import.instagram.enabled')) { if (config('import.instagram.enabled')) {
$schedule->command('app:transform-imports')->everyFourMinutes()->onOneServer(); $schedule->command('app:transform-imports')->everyTenMinutes()->onOneServer();
$schedule->command('app:import-upload-garbage-collection')->hourlyAt(51)->onOneServer(); $schedule->command('app:import-upload-garbage-collection')->hourlyAt(51)->onOneServer();
$schedule->command('app:import-remove-deleted-accounts')->hourlyAt(37)->onOneServer(); $schedule->command('app:import-remove-deleted-accounts')->hourlyAt(37)->onOneServer();
$schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32)->onOneServer(); $schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32)->onOneServer();
}
$schedule->command('app:notification-epoch-update')->weeklyOn(1, '2:21'); if (config('import.instagram.storage.cloud.enabled') && (bool) config_cache('pixelfed.cloud_storage')) {
$schedule->command('app:hashtag-cached-count-update')->hourlyAt(25); $schedule->command('app:import-upload-media-to-cloud-storage')->hourlyAt(39)->onOneServer();
} }
}
$schedule->command('app:notification-epoch-update')->weeklyOn(1, '2:21')->onOneServer();
$schedule->command('app:hashtag-cached-count-update')->hourlyAt(25)->onOneServer();
$schedule->command('app:account-post-count-stat-update')->everySixHours(25)->onOneServer();
}
/** /**
* Register the commands for the application. * Register the commands for the application.
* *
* @return void * @return void
*/ */
protected function commands() protected function commands()
{ {
$this->load(__DIR__ . '/Commands'); $this->load(__DIR__ . '/Commands');
require base_path('routes/console.php'); require base_path('routes/console.php');
} }
} }

View file

@ -219,6 +219,10 @@ class ApiV1Controller extends Controller
if(!$res) { if(!$res) {
return response()->json(['error' => 'Record not found'], 404); return response()->json(['error' => 'Record not found'], 404);
} }
if($res && strpos($res['acct'], '@') != -1) {
$domain = parse_url($res['url'], PHP_URL_HOST);
abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
}
return $this->json($res); return $this->json($res);
} }
@ -483,6 +487,11 @@ class ApiV1Controller extends Controller
$limit = $request->input('limit', 10); $limit = $request->input('limit', 10);
$napi = $request->has(self::PF_API_ENTITY_KEY); $napi = $request->has(self::PF_API_ENTITY_KEY);
if($account && strpos($account['acct'], '@') != -1) {
$domain = parse_url($account['url'], PHP_URL_HOST);
abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
}
if(intval($pid) !== intval($account['id'])) { if(intval($pid) !== intval($account['id'])) {
if($account['locked']) { if($account['locked']) {
if(!FollowerService::follows($pid, $account['id'])) { if(!FollowerService::follows($pid, $account['id'])) {
@ -575,6 +584,11 @@ class ApiV1Controller extends Controller
$limit = $request->input('limit', 10); $limit = $request->input('limit', 10);
$napi = $request->has(self::PF_API_ENTITY_KEY); $napi = $request->has(self::PF_API_ENTITY_KEY);
if($account && strpos($account['acct'], '@') != -1) {
$domain = parse_url($account['url'], PHP_URL_HOST);
abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
}
if(intval($pid) !== intval($account['id'])) { if(intval($pid) !== intval($account['id'])) {
if($account['locked']) { if($account['locked']) {
if(!FollowerService::follows($pid, $account['id'])) { if(!FollowerService::follows($pid, $account['id'])) {
@ -676,6 +690,11 @@ class ApiV1Controller extends Controller
return $this->json(['error' => 'Account not found'], 404); return $this->json(['error' => 'Account not found'], 404);
} }
if($profile && strpos($profile['acct'], '@') != -1) {
$domain = parse_url($profile['url'], PHP_URL_HOST);
abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
}
$limit = $request->limit ?? 20; $limit = $request->limit ?? 20;
$max_id = $request->max_id; $max_id = $request->max_id;
$min_id = $request->min_id; $min_id = $request->min_id;
@ -766,6 +785,11 @@ class ApiV1Controller extends Controller
->whereNull('status') ->whereNull('status')
->findOrFail($id); ->findOrFail($id);
if($target && $target->domain) {
$domain = $target->domain;
abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
}
$private = (bool) $target->is_private; $private = (bool) $target->is_private;
$remote = (bool) $target->domain; $remote = (bool) $target->domain;
$blocked = UserFilter::whereUserId($target->id) $blocked = UserFilter::whereUserId($target->id)
@ -1252,14 +1276,19 @@ class ApiV1Controller extends Controller
$user = $request->user(); $user = $request->user();
abort_if($user->has_roles && !UserRoleService::can('can-like', $user->id), 403, 'Invalid permissions for this action'); abort_if($user->has_roles && !UserRoleService::can('can-like', $user->id), 403, 'Invalid permissions for this action');
AccountService::setLastActive($user->id);
$status = StatusService::getMastodon($id, false); $status = StatusService::getMastodon($id, false);
abort_unless($status, 400); abort_unless($status, 404);
if($status && isset($status['account'], $status['account']['acct']) && strpos($status['account']['acct'], '@') != -1) {
$domain = parse_url($status['account']['url'], PHP_URL_HOST);
abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
}
$spid = $status['account']['id']; $spid = $status['account']['id'];
AccountService::setLastActive($user->id);
if(intval($spid) !== intval($user->profile_id)) { if(intval($spid) !== intval($user->profile_id)) {
if($status['visibility'] == 'private') { if($status['visibility'] == 'private') {
abort_if(!FollowerService::follows($user->profile_id, $spid), 403); abort_if(!FollowerService::follows($user->profile_id, $spid), 403);
@ -1404,6 +1433,11 @@ class ApiV1Controller extends Controller
return response()->json(['error' => 'Record not found'], 404); return response()->json(['error' => 'Record not found'], 404);
} }
if($target && strpos($target['acct'], '@') != -1) {
$domain = parse_url($target['url'], PHP_URL_HOST);
abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
}
$followRequest = FollowRequest::whereFollowingId($pid)->whereFollowerId($id)->first(); $followRequest = FollowRequest::whereFollowingId($pid)->whereFollowerId($id)->first();
if(!$followRequest) { if(!$followRequest) {
@ -2011,6 +2045,11 @@ class ApiV1Controller extends Controller
$account = Profile::findOrFail($id); $account = Profile::findOrFail($id);
if($account && $account->domain) {
$domain = $account->domain;
abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
}
$count = UserFilterService::muteCount($pid); $count = UserFilterService::muteCount($pid);
$maxLimit = intval(config('instance.user_filters.max_user_mutes')); $maxLimit = intval(config('instance.user_filters.max_user_mutes'));
if($count == 0) { if($count == 0) {
@ -2653,6 +2692,11 @@ class ApiV1Controller extends Controller
abort(404); abort(404);
} }
if($res && isset($res['account'], $res['account']['acct'], $res['account']['url']) && strpos($res['account']['acct'], '@') != -1) {
$domain = parse_url($res['account']['url'], PHP_URL_HOST);
abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
}
$scope = $res['visibility']; $scope = $res['visibility'];
if(!in_array($scope, ['public', 'unlisted'])) { if(!in_array($scope, ['public', 'unlisted'])) {
if($scope === 'private') { if($scope === 'private') {
@ -2697,6 +2741,11 @@ class ApiV1Controller extends Controller
return response('', 404); return response('', 404);
} }
if($status && isset($status['account'], $status['account']['acct']) && strpos($status['account']['acct'], '@') != -1) {
$domain = parse_url($status['account']['url'], PHP_URL_HOST);
abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
}
if(intval($status['account']['id']) !== intval($user->profile_id)) { if(intval($status['account']['id']) !== intval($user->profile_id)) {
if($status['visibility'] == 'private') { if($status['visibility'] == 'private') {
if(!FollowerService::follows($user->profile_id, $status['account']['id'])) { if(!FollowerService::follows($user->profile_id, $status['account']['id'])) {
@ -2780,6 +2829,10 @@ class ApiV1Controller extends Controller
$status = Status::findOrFail($id); $status = Status::findOrFail($id);
$account = AccountService::get($status->profile_id, true); $account = AccountService::get($status->profile_id, true);
abort_if(!$account, 404); abort_if(!$account, 404);
if($account && strpos($account['acct'], '@') != -1) {
$domain = parse_url($account['url'], PHP_URL_HOST);
abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
}
$author = intval($status->profile_id) === intval($pid) || $user->is_admin; $author = intval($status->profile_id) === intval($pid) || $user->is_admin;
$napi = $request->has(self::PF_API_ENTITY_KEY); $napi = $request->has(self::PF_API_ENTITY_KEY);
@ -2871,6 +2924,10 @@ class ApiV1Controller extends Controller
$pid = $user->profile_id; $pid = $user->profile_id;
$status = Status::findOrFail($id); $status = Status::findOrFail($id);
$account = AccountService::get($status->profile_id, true); $account = AccountService::get($status->profile_id, true);
if($account && strpos($account['acct'], '@') != -1) {
$domain = parse_url($account['url'], PHP_URL_HOST);
abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
}
abort_if(!$account, 404); abort_if(!$account, 404);
$author = intval($status->profile_id) === intval($pid) || $user->is_admin; $author = intval($status->profile_id) === intval($pid) || $user->is_admin;
$napi = $request->has(self::PF_API_ENTITY_KEY); $napi = $request->has(self::PF_API_ENTITY_KEY);
@ -3031,7 +3088,7 @@ class ApiV1Controller extends Controller
$content = strip_tags($request->input('status')); $content = strip_tags($request->input('status'));
$rendered = Autolink::create()->autolink($content); $rendered = Autolink::create()->autolink($content);
$cw = $user->profile->cw == true ? true : $request->input('sensitive', false); $cw = $user->profile->cw == true ? true : $request->boolean('sensitive', false);
$spoilerText = $cw && $request->filled('spoiler_text') ? $request->input('spoiler_text') : null; $spoilerText = $cw && $request->filled('spoiler_text') ? $request->input('spoiler_text') : null;
if($in_reply_to_id) { if($in_reply_to_id) {
@ -3200,7 +3257,11 @@ class ApiV1Controller extends Controller
abort_if($user->has_roles && !UserRoleService::can('can-share', $user->id), 403, 'Invalid permissions for this action'); abort_if($user->has_roles && !UserRoleService::can('can-share', $user->id), 403, 'Invalid permissions for this action');
AccountService::setLastActive($user->id); AccountService::setLastActive($user->id);
$status = Status::whereScope('public')->findOrFail($id); $status = Status::whereScope('public')->findOrFail($id);
if($status && ($status->uri || $status->url || $status->object_url)) {
$url = $status->uri ?? $status->url ?? $status->object_url;
$domain = parse_url($url, PHP_URL_HOST);
abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
}
if(intval($status->profile_id) !== intval($user->profile_id)) { if(intval($status->profile_id) !== intval($user->profile_id)) {
if($status->scope == 'private') { if($status->scope == 'private') {
abort_if(!FollowerService::follows($user->profile_id, $status->profile_id), 403); abort_if(!FollowerService::follows($user->profile_id, $status->profile_id), 403);

View file

@ -96,6 +96,9 @@ class ApiV2Controller extends Controller
'streaming' => 'wss://' . config('pixelfed.domain.app'), 'streaming' => 'wss://' . config('pixelfed.domain.app'),
'status' => null 'status' => null
], ],
'vapid' => [
'public_key' => config('webpush.vapid.public_key'),
],
'accounts' => [ 'accounts' => [
'max_featured_tags' => 0, 'max_featured_tags' => 0,
], ],

View file

@ -42,6 +42,7 @@ use App\Services\{
use App\Jobs\StatusPipeline\NewStatusPipeline; use App\Jobs\StatusPipeline\NewStatusPipeline;
use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter; use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use App\Services\InstanceService;
class PublicApiController extends Controller class PublicApiController extends Controller
{ {
@ -661,6 +662,10 @@ class PublicApiController extends Controller
public function account(Request $request, $id) public function account(Request $request, $id)
{ {
$res = AccountService::get($id); $res = AccountService::get($id);
if($res && isset($res['local'], $res['url']) && !$res['local']) {
$domain = parse_url($res['url'], PHP_URL_HOST);
abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
}
return response()->json($res); return response()->json($res);
} }
@ -680,6 +685,11 @@ class PublicApiController extends Controller
$profile = AccountService::get($id); $profile = AccountService::get($id);
abort_if(!$profile, 404); abort_if(!$profile, 404);
if($profile && isset($profile['local'], $profile['url']) && !$profile['local']) {
$domain = parse_url($profile['url'], PHP_URL_HOST);
abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
}
$limit = $request->limit ?? 9; $limit = $request->limit ?? 9;
$max_id = $request->max_id; $max_id = $request->max_id;
$min_id = $request->min_id; $min_id = $request->min_id;

View file

@ -6,63 +6,77 @@ use Illuminate\Database\Eloquent\Model;
class Instance extends Model class Instance extends Model
{ {
protected $fillable = ['domain', 'banned', 'auto_cw', 'unlisted', 'notes']; protected $casts = [
'last_crawled_at' => 'datetime',
'actors_last_synced_at' => 'datetime',
'notes' => 'array',
'nodeinfo_last_fetched' => 'datetime',
'delivery_next_after' => 'datetime',
];
public function profiles() protected $fillable = [
{ 'domain',
return $this->hasMany(Profile::class, 'domain', 'domain'); 'banned',
} 'auto_cw',
'unlisted',
'notes'
];
public function statuses() public function profiles()
{ {
return $this->hasManyThrough( return $this->hasMany(Profile::class, 'domain', 'domain');
Status::class, }
Profile::class,
'domain',
'profile_id',
'domain',
'id'
);
}
public function reported() public function statuses()
{ {
return $this->hasManyThrough( return $this->hasManyThrough(
Report::class, Status::class,
Profile::class, Profile::class,
'domain', 'domain',
'reported_profile_id', 'profile_id',
'domain', 'domain',
'id' 'id'
); );
} }
public function reports() public function reported()
{ {
return $this->hasManyThrough( return $this->hasManyThrough(
Report::class, Report::class,
Profile::class, Profile::class,
'domain', 'domain',
'profile_id', 'reported_profile_id',
'domain', 'domain',
'id' 'id'
); );
} }
public function media() public function reports()
{ {
return $this->hasManyThrough( return $this->hasManyThrough(
Media::class, Report::class,
Profile::class, Profile::class,
'domain', 'domain',
'profile_id', 'profile_id',
'domain', 'domain',
'id' 'id'
); );
} }
public function getUrl() public function media()
{ {
return url("/i/admin/instances/show/{$this->id}"); return $this->hasManyThrough(
} Media::class,
Profile::class,
'domain',
'profile_id',
'domain',
'id'
);
}
public function getUrl()
{
return url("/i/admin/instances/show/{$this->id}");
}
} }

View file

@ -91,19 +91,21 @@ class CommentPipeline implements ShouldQueue
return; return;
} }
DB::transaction(function() use($target, $actor, $comment) { if($target->user_id && $target->domain === null) {
$notification = new Notification(); DB::transaction(function() use($target, $actor, $comment) {
$notification->profile_id = $target->id; $notification = new Notification();
$notification->actor_id = $actor->id; $notification->profile_id = $target->id;
$notification->action = 'comment'; $notification->actor_id = $actor->id;
$notification->item_id = $comment->id; $notification->action = 'comment';
$notification->item_type = "App\Status"; $notification->item_id = $comment->id;
$notification->save(); $notification->item_type = "App\Status";
$notification->save();
NotificationService::setNotification($notification); NotificationService::setNotification($notification);
NotificationService::set($notification->profile_id, $notification->id); NotificationService::set($notification->profile_id, $notification->id);
StatusService::del($comment->id); StatusService::del($comment->id);
}); });
}
if($exists = Cache::get('status:replies:all:' . $status->id)) { if($exists = Cache::get('status:replies:all:' . $status->id)) {
if($exists && $exists->count() == 3) { if($exists && $exists->count() == 3) {

View file

@ -22,9 +22,9 @@ use App\Notification;
use App\Services\AccountService; use App\Services\AccountService;
use App\Services\NetworkTimelineService; use App\Services\NetworkTimelineService;
use App\Services\StatusService; use App\Services\StatusService;
use App\Jobs\ProfilePipeline\DecrementPostCount;
use App\Jobs\MediaPipeline\MediaDeletePipeline; use App\Jobs\MediaPipeline\MediaDeletePipeline;
use Cache; use Cache;
use App\Services\Account\AccountStatService;
class DeleteRemoteStatusPipeline implements ShouldQueue class DeleteRemoteStatusPipeline implements ShouldQueue
{ {
@ -56,10 +56,7 @@ class DeleteRemoteStatusPipeline implements ShouldQueue
{ {
$status = $this->status; $status = $this->status;
if(AccountService::get($status->profile_id, true)) { AccountStatService::decrementPostCount($status->profile_id);
DecrementPostCount::dispatch($status->profile_id)->onQueue('low');
}
NetworkTimelineService::del($status->id); NetworkTimelineService::del($status->id);
StatusService::del($status->id, true); StatusService::del($status->id, true);
Bookmark::whereStatusId($status->id)->delete(); Bookmark::whereStatusId($status->id)->delete();

View file

@ -72,16 +72,18 @@ class FollowPipeline implements ShouldQueue
$target->save(); $target->save();
AccountService::del($target->id); AccountService::del($target->id);
try { if($target->user_id && $target->domain === null) {
$notification = new Notification(); try {
$notification->profile_id = $target->id; $notification = new Notification();
$notification->actor_id = $actor->id; $notification->profile_id = $target->id;
$notification->action = 'follow'; $notification->actor_id = $actor->id;
$notification->item_id = $target->id; $notification->action = 'follow';
$notification->item_type = "App\Profile"; $notification->item_id = $target->id;
$notification->save(); $notification->item_type = "App\Profile";
} catch (Exception $e) { $notification->save();
Log::error($e); } catch (Exception $e) {
Log::error($e);
}
} }
} }
} }

View file

@ -0,0 +1,129 @@
<?php
namespace App\Jobs\ImportPipeline;
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 Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
use App\Models\ImportPost;
use App\Media;
use App\Services\MediaStorageService;
use Illuminate\Support\Facades\Storage;
use App\Jobs\VideoPipeline\VideoThumbnailToCloudPipeline;
class ImportMediaToCloudPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $importPost;
public $timeout = 900;
public $tries = 3;
public $maxExceptions = 1;
public $failOnTimeout = true;
/**
* The number of seconds after which the job's unique lock will be released.
*
* @var int
*/
public $uniqueFor = 3600;
/**
* Get the unique ID for the job.
*/
public function uniqueId(): string
{
return 'import-media-to-cloud-pipeline:ip-id:' . $this->importPost->id;
}
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new WithoutOverlapping("import-media-to-cloud-pipeline:ip-id:{$this->importPost->id}"))->shared()->dontRelease()];
}
/**
* Delete the job if its models no longer exist.
*
* @var bool
*/
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*/
public function __construct(ImportPost $importPost)
{
$this->importPost = $importPost;
}
/**
* Execute the job.
*/
public function handle(): void
{
$ip = $this->importPost;
if(
$ip->status_id === null ||
$ip->uploaded_to_s3 === true ||
(bool) config_cache('pixelfed.cloud_storage') === false) {
return;
}
$media = Media::whereStatusId($ip->status_id)->get();
if(!$media || !$media->count()) {
$importPost = ImportPost::find($ip->id);
$importPost->uploaded_to_s3 = true;
$importPost->save();
return;
}
foreach($media as $mediaPart) {
$this->handleMedia($mediaPart);
}
}
protected function handleMedia($media)
{
$ip = $this->importPost;
$importPost = ImportPost::find($ip->id);
if(!$importPost) {
return;
}
$res = MediaStorageService::move($media);
$importPost->uploaded_to_s3 = true;
$importPost->save();
if(!$res) {
return;
}
if($res === 'invalid file') {
return;
}
if($res === 'success') {
if($media->mime === 'video/mp4') {
VideoThumbnailToCloudPipeline::dispatch($media)->onQueue('low');
} else {
Storage::disk('local')->delete($media->media_path);
}
}
}
}

View file

@ -4,6 +4,7 @@ namespace App\Jobs\InstancePipeline;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
@ -12,45 +13,71 @@ use Illuminate\Support\Facades\Http;
use App\Instance; use App\Instance;
use App\Profile; use App\Profile;
use App\Services\NodeinfoService; use App\Services\NodeinfoService;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;
class FetchNodeinfoPipeline implements ShouldQueue class FetchNodeinfoPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $instance; protected $instance;
/** /**
* Create a new job instance. * Create a new job instance.
* *
* @return void * @return void
*/ */
public function __construct(Instance $instance) public function __construct(Instance $instance)
{ {
$this->instance = $instance; $this->instance = $instance;
} }
/** /**
* Execute the job. * The number of seconds after which the job's unique lock will be released.
* *
* @return void * @var int
*/ */
public function handle() public $uniqueFor = 14400;
{
$instance = $this->instance;
$ni = NodeinfoService::get($instance->domain); /**
if($ni) { * Get the unique ID for the job.
if(isset($ni['software']) && is_array($ni['software']) && isset($ni['software']['name'])) { */
$software = $ni['software']['name']; public function uniqueId(): string
$instance->software = strtolower(strip_tags($software)); {
$instance->last_crawled_at = now(); return $this->instance->id;
$instance->user_count = Profile::whereDomain($instance->domain)->count(); }
$instance->save();
} /**
} else { * Execute the job.
$instance->user_count = Profile::whereDomain($instance->domain)->count(); *
$instance->last_crawled_at = now(); * @return void
$instance->save(); */
} public function handle()
} {
$instance = $this->instance;
if( $instance->nodeinfo_last_fetched &&
$instance->nodeinfo_last_fetched->gt(now()->subHours(12)) ||
$instance->delivery_timeout &&
$instance->delivery_next_after->gt(now())
) {
return;
}
$ni = NodeinfoService::get($instance->domain);
$instance->last_crawled_at = now();
if($ni) {
if(isset($ni['software']) && is_array($ni['software']) && isset($ni['software']['name'])) {
$software = $ni['software']['name'];
$instance->software = strtolower(strip_tags($software));
$instance->user_count = Profile::whereDomain($instance->domain)->count();
$instance->nodeinfo_last_fetched = now();
$instance->save();
}
} else {
$instance->delivery_timeout = 1;
$instance->delivery_next_after = now()->addHours(14);
$instance->save();
}
}
} }

View file

@ -61,7 +61,12 @@ class NotificationEpochUpdatePipeline implements ShouldQueue, ShouldBeUniqueUnti
*/ */
public function handle(): void public function handle(): void
{ {
$rec = Notification::where('created_at', '>', now()->subMonths(6))->first(); $pid = Cache::get(NotificationService::EPOCH_CACHE_KEY . '6');
if($pid && $pid > 1) {
$rec = Notification::where('id', '>', $pid)->whereDate('created_at', now()->subMonths(6)->format('Y-m-d'))->first();
} else {
$rec = Notification::whereDate('created_at', now()->subMonths(6)->format('Y-m-d'))->first();
}
$id = 1; $id = 1;
if($rec) { if($rec) {
$id = $rec->id; $id = $rec->id;

View file

@ -79,16 +79,18 @@ class LikePipeline implements ShouldQueue
return true; return true;
} }
try { if($status->uri === null && $status->object_url === null && $status->url === null) {
$notification = new Notification(); try {
$notification->profile_id = $status->profile_id; $notification = new Notification();
$notification->actor_id = $actor->id; $notification->profile_id = $status->profile_id;
$notification->action = 'like'; $notification->actor_id = $actor->id;
$notification->item_id = $status->id; $notification->action = 'like';
$notification->item_type = "App\Status"; $notification->item_id = $status->id;
$notification->save(); $notification->item_type = "App\Status";
$notification->save();
} catch (Exception $e) { } catch (Exception $e) {
}
} }
} }

View file

@ -35,18 +35,7 @@ class DecrementPostCount implements ShouldQueue
*/ */
public function handle() public function handle()
{ {
$id = $this->id; // deprecated
return;
$profile = Profile::find($id);
if(!$profile) {
return 1;
}
$profile->status_count = $profile->status_count ? $profile->status_count - 1 : 0;
$profile->save();
AccountService::del($id);
return 1;
} }
} }

View file

@ -14,42 +14,12 @@ use App\Profile;
use App\Status; use App\Status;
use App\Services\AccountService; use App\Services\AccountService;
class IncrementPostCount implements ShouldQueue, ShouldBeUniqueUntilProcessing class IncrementPostCount implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $id; public $id;
public $timeout = 900;
public $tries = 3;
public $maxExceptions = 1;
public $failOnTimeout = true;
/**
* The number of seconds after which the job's unique lock will be released.
*
* @var int
*/
public $uniqueFor = 3600;
/**
* Get the unique ID for the job.
*/
public function uniqueId(): string
{
return 'propipe:ipc:' . $this->id;
}
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new WithoutOverlapping("propipe:ipc:{$this->id}"))->shared()->dontRelease()];
}
/** /**
* Create a new job instance. * Create a new job instance.
* *
@ -67,20 +37,7 @@ class IncrementPostCount implements ShouldQueue, ShouldBeUniqueUntilProcessing
*/ */
public function handle() public function handle()
{ {
$id = $this->id; // deprecated
return;
$profile = Profile::find($id);
if(!$profile) {
return 1;
}
$profile->status_count = $profile->status_count + 1;
$profile->last_status_at = now();
$profile->save();
AccountService::del($id);
AccountService::get($id);
return 1;
} }
} }

View file

@ -39,8 +39,8 @@ use App\Services\AccountService;
use App\Services\CollectionService; use App\Services\CollectionService;
use App\Services\StatusService; use App\Services\StatusService;
use App\Jobs\MediaPipeline\MediaDeletePipeline; use App\Jobs\MediaPipeline\MediaDeletePipeline;
use App\Jobs\ProfilePipeline\DecrementPostCount;
use App\Services\NotificationService; use App\Services\NotificationService;
use App\Services\Account\AccountStatService;
class RemoteStatusDelete implements ShouldQueue, ShouldBeUniqueUntilProcessing class RemoteStatusDelete implements ShouldQueue, ShouldBeUniqueUntilProcessing
{ {
@ -109,9 +109,7 @@ class RemoteStatusDelete implements ShouldQueue, ShouldBeUniqueUntilProcessing
} }
StatusService::del($status->id, true); StatusService::del($status->id, true);
AccountStatService::decrementPostCount($status->profile_id);
DecrementPostCount::dispatch($status->profile_id)->onQueue('inbox');
return $this->unlinkRemoveMedia($status); return $this->unlinkRemoveMedia($status);
} }

View file

@ -87,18 +87,20 @@ class StatusReplyPipeline implements ShouldQueue
Cache::forget('status:replies:all:' . $reply->id); Cache::forget('status:replies:all:' . $reply->id);
Cache::forget('status:replies:all:' . $status->id); Cache::forget('status:replies:all:' . $status->id);
DB::transaction(function() use($target, $actor, $status) { if($target->user_id && $target->domain === null) {
$notification = new Notification(); DB::transaction(function() use($target, $actor, $status) {
$notification->profile_id = $target->id; $notification = new Notification();
$notification->actor_id = $actor->id; $notification->profile_id = $target->id;
$notification->action = 'comment'; $notification->actor_id = $actor->id;
$notification->item_id = $status->id; $notification->action = 'comment';
$notification->item_type = "App\Status"; $notification->item_id = $status->id;
$notification->save(); $notification->item_type = "App\Status";
$notification->save();
NotificationService::setNotification($notification); NotificationService::setNotification($notification);
NotificationService::set($notification->profile_id, $notification->id); NotificationService::set($notification->profile_id, $notification->id);
}); });
}
if($exists = Cache::get('status:replies:all:' . $reply->id)) { if($exists = Cache::get('status:replies:all:' . $reply->id)) {
if($exists && $exists->count() == 3) { if($exists && $exists->count() == 3) {

View file

@ -0,0 +1,147 @@
<?php
namespace App\Jobs\VideoPipeline;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
use Illuminate\Http\File;
use Cache;
use FFMpeg;
use Storage;
use App\Media;
use App\Jobs\MediaPipeline\MediaStoragePipeline;
use App\Util\Media\Blurhash;
use App\Services\MediaService;
use App\Services\StatusService;
use App\Services\ResilientMediaStorageService;
class VideoThumbnailToCloudPipeline implements ShouldQueue, ShouldBeUniqueUntilProcessing
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $media;
public $timeout = 900;
public $tries = 3;
public $maxExceptions = 1;
public $failOnTimeout = true;
public $deleteWhenMissingModels = true;
/**
* The number of seconds after which the job's unique lock will be released.
*
* @var int
*/
public $uniqueFor = 3600;
/**
* Get the unique ID for the job.
*/
public function uniqueId(): string
{
return 'media:video-thumb-to-cloud:id-' . $this->media->id;
}
/**
* Get the middleware the job should pass through.
*
* @return array<int, object>
*/
public function middleware(): array
{
return [(new WithoutOverlapping("media:video-thumb-to-cloud:id-{$this->media->id}"))->shared()->dontRelease()];
}
/**
* Create a new job instance.
*/
public function __construct(Media $media)
{
$this->media = $media;
}
/**
* Execute the job.
*/
public function handle(): void
{
if((bool) config_cache('pixelfed.cloud_storage') === false) {
return;
}
$media = $this->media;
if($media->mime != 'video/mp4') {
return;
}
if($media->profile_id === null || $media->status_id === null) {
return;
}
if($media->thumbnail_url) {
return;
}
$base = $media->media_path;
$path = explode('/', $base);
$name = last($path);
try {
$t = explode('.', $name);
$t = $t[0].'_thumb.jpeg';
$i = count($path) - 1;
$path[$i] = $t;
$save = implode('/', $path);
$video = FFMpeg::open($base)
->getFrameFromSeconds(1)
->export()
->toDisk('local')
->save($save);
if(!$save) {
return;
}
$media->thumbnail_path = $save;
$p = explode('/', $media->media_path);
array_pop($p);
$pt = explode('/', $save);
$thumbname = array_pop($pt);
$storagePath = implode('/', $p);
$thumb = storage_path('app/' . $save);
$thumbUrl = ResilientMediaStorageService::store($storagePath, $thumb, $thumbname);
$media->thumbnail_url = $thumbUrl;
$media->save();
$blurhash = Blurhash::generate($media);
if($blurhash) {
$media->blurhash = $blurhash;
$media->save();
}
if(str_starts_with($save, 'public/m/_v2/') && str_ends_with($save, '.jpeg')) {
Storage::delete($save);
}
if(str_starts_with($media->media_path, 'public/m/_v2/') && str_ends_with($media->media_path, '.mp4')) {
Storage::disk('local')->delete($media->media_path);
}
} catch (Exception $e) {
}
if($media->status_id) {
Cache::forget('status:transformer:media:attachments:' . $media->status_id);
MediaService::del($media->status_id);
Cache::forget('status:thumb:nsfw0' . $media->status_id);
Cache::forget('status:thumb:nsfw1' . $media->status_id);
Cache::forget('pf:services:sh:id:' . $media->status_id);
StatusService::del($media->status_id);
}
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace App\Services\Account;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Redis;
class AccountStatService
{
const REFRESH_CACHE_KEY = 'pf:services:accountstats:refresh:daily';
public static function incrementPostCount($pid)
{
return Redis::zadd(self::REFRESH_CACHE_KEY, $pid, $pid);
}
public static function decrementPostCount($pid)
{
return Redis::zadd(self::REFRESH_CACHE_KEY, $pid, $pid);
}
public static function removeFromPostCount($pid)
{
return Redis::zrem(self::REFRESH_CACHE_KEY, $pid);
}
public static function getAllPostCountIncr($limit = -1)
{
return Redis::zrange(self::REFRESH_CACHE_KEY, 0, $limit);
}
}

View file

@ -21,28 +21,40 @@ use App\Jobs\AvatarPipeline\AvatarStorageCleanup;
class MediaStorageService { class MediaStorageService {
public static function store(Media $media) public static function store(Media $media)
{ {
if(config_cache('pixelfed.cloud_storage') == true) { if(config_cache('pixelfed.cloud_storage') == true) {
(new self())->cloudStore($media); (new self())->cloudStore($media);
} }
return; return;
} }
public static function avatar($avatar, $local = false, $skipRecentCheck = false) public static function move(Media $media)
{ {
return (new self())->fetchAvatar($avatar, $local, $skipRecentCheck); if($media->remote_media) {
} return;
}
public static function head($url) if(config_cache('pixelfed.cloud_storage') == true) {
{ return (new self())->cloudMove($media);
$c = new Client(); }
try { return;
$r = $c->request('HEAD', $url); }
} catch (RequestException $e) {
return false; public static function avatar($avatar, $local = false, $skipRecentCheck = false)
} {
return (new self())->fetchAvatar($avatar, $local, $skipRecentCheck);
}
public static function head($url)
{
$c = new Client();
try {
$r = $c->request('HEAD', $url);
} catch (RequestException $e) {
return false;
}
$h = Arr::mapWithKeys($r->getHeaders(), function($item, $key) { $h = Arr::mapWithKeys($r->getHeaders(), function($item, $key) {
return [strtolower($key) => last($item)]; return [strtolower($key) => last($item)];
@ -55,224 +67,261 @@ class MediaStorageService {
$len = (int) $h['content-length']; $len = (int) $h['content-length'];
$mime = $h['content-type']; $mime = $h['content-type'];
if($len < 10 || $len > ((config_cache('pixelfed.max_photo_size') * 1000))) { if($len < 10 || $len > ((config_cache('pixelfed.max_photo_size') * 1000))) {
return false; return false;
} }
return [ return [
'length' => $len, 'length' => $len,
'mime' => $mime 'mime' => $mime
]; ];
} }
protected function cloudStore($media) protected function cloudStore($media)
{ {
if($media->remote_media == true) { if($media->remote_media == true) {
if(config('media.storage.remote.cloud')) { if(config('media.storage.remote.cloud')) {
(new self())->remoteToCloud($media); (new self())->remoteToCloud($media);
} }
} else { } else {
(new self())->localToCloud($media); (new self())->localToCloud($media);
} }
} }
protected function localToCloud($media) protected function localToCloud($media)
{ {
$path = storage_path('app/'.$media->media_path); $path = storage_path('app/'.$media->media_path);
$thumb = storage_path('app/'.$media->thumbnail_path); $thumb = storage_path('app/'.$media->thumbnail_path);
$p = explode('/', $media->media_path); $p = explode('/', $media->media_path);
$name = array_pop($p); $name = array_pop($p);
$pt = explode('/', $media->thumbnail_path); $pt = explode('/', $media->thumbnail_path);
$thumbname = array_pop($pt); $thumbname = array_pop($pt);
$storagePath = implode('/', $p); $storagePath = implode('/', $p);
$url = ResilientMediaStorageService::store($storagePath, $path, $name); $url = ResilientMediaStorageService::store($storagePath, $path, $name);
if($thumb) { if($thumb) {
$thumbUrl = ResilientMediaStorageService::store($storagePath, $thumb, $thumbname); $thumbUrl = ResilientMediaStorageService::store($storagePath, $thumb, $thumbname);
$media->thumbnail_url = $thumbUrl; $media->thumbnail_url = $thumbUrl;
} }
$media->cdn_url = $url; $media->cdn_url = $url;
$media->optimized_url = $url; $media->optimized_url = $url;
$media->replicated_at = now(); $media->replicated_at = now();
$media->save(); $media->save();
if($media->status_id) { if($media->status_id) {
Cache::forget('status:transformer:media:attachments:' . $media->status_id); Cache::forget('status:transformer:media:attachments:' . $media->status_id);
MediaService::del($media->status_id); MediaService::del($media->status_id);
StatusService::del($media->status_id, false); StatusService::del($media->status_id, false);
} }
} }
protected function remoteToCloud($media) protected function remoteToCloud($media)
{ {
$url = $media->remote_url; $url = $media->remote_url;
if(!Helpers::validateUrl($url)) { if(!Helpers::validateUrl($url)) {
return; return;
} }
$head = $this->head($media->remote_url); $head = $this->head($media->remote_url);
if(!$head) { if(!$head) {
return; return;
} }
$mimes = [ $mimes = [
'image/jpeg', 'image/jpeg',
'image/png', 'image/png',
'video/mp4' 'video/mp4'
]; ];
$mime = $head['mime']; $mime = $head['mime'];
$max_size = (int) config_cache('pixelfed.max_photo_size') * 1000; $max_size = (int) config_cache('pixelfed.max_photo_size') * 1000;
$media->size = $head['length']; $media->size = $head['length'];
$media->remote_media = true; $media->remote_media = true;
$media->save(); $media->save();
if(!in_array($mime, $mimes)) { if(!in_array($mime, $mimes)) {
return; return;
} }
if($head['length'] >= $max_size) { if($head['length'] >= $max_size) {
return; return;
} }
switch ($mime) { switch ($mime) {
case 'image/png': case 'image/png':
$ext = '.png'; $ext = '.png';
break; break;
case 'image/gif': case 'image/gif':
$ext = '.gif'; $ext = '.gif';
break; break;
case 'image/jpeg': case 'image/jpeg':
$ext = '.jpg'; $ext = '.jpg';
break; break;
case 'video/mp4': case 'video/mp4':
$ext = '.mp4'; $ext = '.mp4';
break; break;
} }
$base = MediaPathService::get($media->profile); $base = MediaPathService::get($media->profile);
$path = Str::random(40) . $ext; $path = Str::random(40) . $ext;
$tmpBase = storage_path('app/remcache/'); $tmpBase = storage_path('app/remcache/');
$tmpPath = $media->profile_id . '-' . $path; $tmpPath = $media->profile_id . '-' . $path;
$tmpName = $tmpBase . $tmpPath; $tmpName = $tmpBase . $tmpPath;
$data = file_get_contents($url, false, null, 0, $head['length']); $data = file_get_contents($url, false, null, 0, $head['length']);
file_put_contents($tmpName, $data); file_put_contents($tmpName, $data);
$hash = hash_file('sha256', $tmpName); $hash = hash_file('sha256', $tmpName);
$disk = Storage::disk(config('filesystems.cloud')); $disk = Storage::disk(config('filesystems.cloud'));
$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);
$media->media_path = $file; $media->media_path = $file;
$media->cdn_url = $permalink; $media->cdn_url = $permalink;
$media->original_sha256 = $hash; $media->original_sha256 = $hash;
$media->replicated_at = now(); $media->replicated_at = now();
$media->save(); $media->save();
if($media->status_id) { if($media->status_id) {
Cache::forget('status:transformer:media:attachments:' . $media->status_id); Cache::forget('status:transformer:media:attachments:' . $media->status_id);
} }
unlink($tmpName); unlink($tmpName);
} }
protected function fetchAvatar($avatar, $local = false, $skipRecentCheck = false) protected function fetchAvatar($avatar, $local = false, $skipRecentCheck = false)
{ {
$queue = random_int(1, 15) > 5 ? 'mmo' : 'low'; $queue = random_int(1, 15) > 5 ? 'mmo' : 'low';
$url = $avatar->remote_url; $url = $avatar->remote_url;
$driver = $local ? 'local' : config('filesystems.cloud'); $driver = $local ? 'local' : config('filesystems.cloud');
if(empty($url) || Helpers::validateUrl($url) == false) { if(empty($url) || Helpers::validateUrl($url) == false) {
return; return;
} }
$head = $this->head($url); $head = $this->head($url);
if($head == false) { if($head == false) {
return; return;
} }
$mimes = [ $mimes = [
'application/octet-stream', 'application/octet-stream',
'image/jpeg', 'image/jpeg',
'image/png', 'image/png',
]; ];
$mime = $head['mime']; $mime = $head['mime'];
$max_size = (int) config('pixelfed.max_avatar_size') * 1000; $max_size = (int) config('pixelfed.max_avatar_size') * 1000;
if(!$skipRecentCheck) { if(!$skipRecentCheck) {
if($avatar->last_fetched_at && $avatar->last_fetched_at->gt(now()->subMonths(3))) { if($avatar->last_fetched_at && $avatar->last_fetched_at->gt(now()->subMonths(3))) {
return; return;
} }
} }
Cache::forget('avatar:' . $avatar->profile_id); Cache::forget('avatar:' . $avatar->profile_id);
AccountService::del($avatar->profile_id); AccountService::del($avatar->profile_id);
// handle pleroma edge case // handle pleroma edge case
if(Str::endsWith($mime, '; charset=utf-8')) { if(Str::endsWith($mime, '; charset=utf-8')) {
$mime = str_replace('; charset=utf-8', '', $mime); $mime = str_replace('; charset=utf-8', '', $mime);
} }
if(!in_array($mime, $mimes)) { if(!in_array($mime, $mimes)) {
return; return;
} }
if($head['length'] >= $max_size) { if($head['length'] >= $max_size) {
return; return;
} }
$base = ($local ? 'public/cache/' : 'cache/') . 'avatars/' . $avatar->profile_id; $base = ($local ? 'public/cache/' : 'cache/') . 'avatars/' . $avatar->profile_id;
$ext = $head['mime'] == 'image/jpeg' ? 'jpg' : 'png'; $ext = $head['mime'] == 'image/jpeg' ? 'jpg' : 'png';
$path = 'avatar_' . strtolower(Str::random(random_int(3,6))) . '.' . $ext; $path = 'avatar_' . strtolower(Str::random(random_int(3,6))) . '.' . $ext;
$tmpBase = storage_path('app/remcache/'); $tmpBase = storage_path('app/remcache/');
$tmpPath = 'avatar_' . $avatar->profile_id . '-' . $path; $tmpPath = 'avatar_' . $avatar->profile_id . '-' . $path;
$tmpName = $tmpBase . $tmpPath; $tmpName = $tmpBase . $tmpPath;
$data = @file_get_contents($url, false, null, 0, $head['length']); $data = @file_get_contents($url, false, null, 0, $head['length']);
if(!$data) { if(!$data) {
return; return;
} }
file_put_contents($tmpName, $data); file_put_contents($tmpName, $data);
$mimeCheck = Storage::mimeType('remcache/' . $tmpPath); $mimeCheck = Storage::mimeType('remcache/' . $tmpPath);
if(!$mimeCheck || !in_array($mimeCheck, ['image/png', 'image/jpeg'])) { if(!$mimeCheck || !in_array($mimeCheck, ['image/png', 'image/jpeg'])) {
$avatar->last_fetched_at = now(); $avatar->last_fetched_at = now();
$avatar->save(); $avatar->save();
unlink($tmpName); unlink($tmpName);
return; return;
} }
$disk = Storage::disk($driver); $disk = Storage::disk($driver);
$file = $disk->putFileAs($base, new File($tmpName), $path, 'public'); $file = $disk->putFileAs($base, new File($tmpName), $path, 'public');
$permalink = $disk->url($file); $permalink = $disk->url($file);
$avatar->media_path = $base . '/' . $path; $avatar->media_path = $base . '/' . $path;
$avatar->is_remote = true; $avatar->is_remote = true;
$avatar->cdn_url = $local ? config('app.url') . $permalink : $permalink; $avatar->cdn_url = $local ? config('app.url') . $permalink : $permalink;
$avatar->size = $head['length']; $avatar->size = $head['length'];
$avatar->change_count = $avatar->change_count + 1; $avatar->change_count = $avatar->change_count + 1;
$avatar->last_fetched_at = now(); $avatar->last_fetched_at = now();
$avatar->save(); $avatar->save();
Cache::forget('avatar:' . $avatar->profile_id); Cache::forget('avatar:' . $avatar->profile_id);
AccountService::del($avatar->profile_id); AccountService::del($avatar->profile_id);
AvatarStorageCleanup::dispatch($avatar)->onQueue($queue)->delay(now()->addMinutes(random_int(3, 15))); AvatarStorageCleanup::dispatch($avatar)->onQueue($queue)->delay(now()->addMinutes(random_int(3, 15)));
unlink($tmpName); unlink($tmpName);
} }
public static function delete(Media $media, $confirm = false) public static function delete(Media $media, $confirm = false)
{ {
if(!$confirm) { if(!$confirm) {
return; return;
} }
MediaDeletePipeline::dispatch($media)->onQueue('mmo'); MediaDeletePipeline::dispatch($media)->onQueue('mmo');
} }
protected function cloudMove($media)
{
if(!Storage::exists($media->media_path)) {
return 'invalid file';
}
$path = storage_path('app/'.$media->media_path);
$thumb = false;
if($media->thumbnail_path) {
$thumb = storage_path('app/'.$media->thumbnail_path);
$pt = explode('/', $media->thumbnail_path);
$thumbname = array_pop($pt);
}
$p = explode('/', $media->media_path);
$name = array_pop($p);
$storagePath = implode('/', $p);
$url = ResilientMediaStorageService::store($storagePath, $path, $name);
if($thumb) {
$thumbUrl = ResilientMediaStorageService::store($storagePath, $thumb, $thumbname);
$media->thumbnail_url = $thumbUrl;
}
$media->cdn_url = $url;
$media->optimized_url = $url;
$media->replicated_at = now();
$media->save();
if($media->status_id) {
Cache::forget('status:transformer:media:attachments:' . $media->status_id);
MediaService::del($media->status_id);
StatusService::del($media->status_id, false);
}
return 'success';
}
} }

View file

@ -22,7 +22,10 @@ class NodeinfoService
$wk = $url . '/.well-known/nodeinfo'; $wk = $url . '/.well-known/nodeinfo';
try { try {
$res = Http::withHeaders($headers) $res = Http::withOptions([
'allow_redirects' => false,
])
->withHeaders($headers)
->timeout(5) ->timeout(5)
->get($wk); ->get($wk);
} catch (RequestException $e) { } catch (RequestException $e) {
@ -61,7 +64,10 @@ class NodeinfoService
} }
try { try {
$res = Http::withHeaders($headers) $res = Http::withOptions([
'allow_redirects' => false,
])
->withHeaders($headers)
->timeout(5) ->timeout(5)
->get($href); ->get($href);
} catch (RequestException $e) { } catch (RequestException $e) {

View file

@ -39,10 +39,9 @@ use App\Jobs\HomeFeedPipeline\FeedInsertRemotePipeline;
use App\Util\Media\License; use App\Util\Media\License;
use App\Models\Poll; use App\Models\Poll;
use Illuminate\Contracts\Cache\LockTimeoutException; use Illuminate\Contracts\Cache\LockTimeoutException;
use App\Jobs\ProfilePipeline\IncrementPostCount;
use App\Jobs\ProfilePipeline\DecrementPostCount;
use App\Services\DomainService; use App\Services\DomainService;
use App\Services\UserFilterService; use App\Services\UserFilterService;
use App\Services\Account\AccountStatService;
class Helpers { class Helpers {
@ -536,7 +535,7 @@ class Helpers {
} }
} }
IncrementPostCount::dispatch($pid)->onQueue('low'); AccountStatService::incrementPostCount($pid);
if( $status->in_reply_to_id === null && if( $status->in_reply_to_id === null &&
in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) in_array($status->type, ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
@ -549,10 +548,11 @@ class Helpers {
public static function getSensitive($activity, $url) public static function getSensitive($activity, $url)
{ {
$id = isset($activity['id']) ? self::pluckval($activity['id']) : self::pluckval($url); if(!$url || !strlen($url)) {
$url = isset($activity['url']) ? self::pluckval($activity['url']) : $id; return true;
$urlDomain = parse_url($url, PHP_URL_HOST); }
$urlDomain = parse_url($url, PHP_URL_HOST);
$cw = isset($activity['sensitive']) ? (bool) $activity['sensitive'] : false; $cw = isset($activity['sensitive']) ? (bool) $activity['sensitive'] : false;
if(in_array($urlDomain, InstanceService::getNsfwDomains())) { if(in_array($urlDomain, InstanceService::getNsfwDomains())) {

View file

@ -48,8 +48,6 @@ use App\Services\UserFilterService;
use App\Services\NetworkTimelineService; use App\Services\NetworkTimelineService;
use App\Models\Conversation; use App\Models\Conversation;
use App\Models\RemoteReport; use App\Models\RemoteReport;
use App\Jobs\ProfilePipeline\IncrementPostCount;
use App\Jobs\ProfilePipeline\DecrementPostCount;
use App\Jobs\HomeFeedPipeline\FeedRemoveRemotePipeline; use App\Jobs\HomeFeedPipeline\FeedRemoveRemotePipeline;
class Inbox class Inbox

View file

@ -2,201 +2,202 @@
return [ return [
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Horizon Domain | Horizon Domain
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| This is the subdomain where Horizon will be accessible from. If this | This is the subdomain where Horizon will be accessible from. If this
| setting is null, Horizon will reside under the same domain as the | setting is null, Horizon will reside under the same domain as the
| application. Otherwise, this value will serve as the subdomain. | application. Otherwise, this value will serve as the subdomain.
| |
*/ */
'domain' => null, 'domain' => null,
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Horizon Path | Horizon Path
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| This is the URI path where Horizon will be accessible from. Feel free | This is the URI path where Horizon will be accessible from. Feel free
| to change this path to anything you like. Note that the URI will not | to change this path to anything you like. Note that the URI will not
| affect the paths of its internal API that aren't exposed to users. | affect the paths of its internal API that aren't exposed to users.
| |
*/ */
'path' => 'horizon', 'path' => 'horizon',
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Horizon Redis Connection | Horizon Redis Connection
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| This is the name of the Redis connection where Horizon will store the | This is the name of the Redis connection where Horizon will store the
| meta information required for it to function. It includes the list | meta information required for it to function. It includes the list
| of supervisors, failed jobs, job metrics, and other information. | of supervisors, failed jobs, job metrics, and other information.
| |
*/ */
'use' => 'default', 'use' => 'default',
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Horizon Redis Prefix | Horizon Redis Prefix
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| This prefix will be used when storing all Horizon data in Redis. You | This prefix will be used when storing all Horizon data in Redis. You
| may modify the prefix when you are running multiple installations | may modify the prefix when you are running multiple installations
| of Horizon on the same server so that they don't have problems. | of Horizon on the same server so that they don't have problems.
| |
*/ */
'prefix' => env('HORIZON_PREFIX', 'horizon-'), 'prefix' => env('HORIZON_PREFIX', 'horizon-'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Horizon Route Middleware | Horizon Route Middleware
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| These middleware will get attached onto each Horizon route, giving you | These middleware will get attached onto each Horizon route, giving you
| the chance to add your own middleware to this list or change any of | the chance to add your own middleware to this list or change any of
| the existing middleware. Or, you can simply stick with this list. | the existing middleware. Or, you can simply stick with this list.
| |
*/ */
'middleware' => ['web'], 'middleware' => ['web'],
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Queue Wait Time Thresholds | Queue Wait Time Thresholds
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| This option allows you to configure when the LongWaitDetected event | This option allows you to configure when the LongWaitDetected event
| will be fired. Every connection / queue combination may have its | will be fired. Every connection / queue combination may have its
| own, unique threshold (in seconds) before this event is fired. | own, unique threshold (in seconds) before this event is fired.
| |
*/ */
'waits' => [ 'waits' => [
'redis:feed' => 30, 'redis:feed' => 30,
'redis:follow' => 30, 'redis:follow' => 30,
'redis:shared' => 30, 'redis:shared' => 30,
'redis:default' => 30, 'redis:default' => 30,
'redis:inbox' => 30, 'redis:inbox' => 30,
'redis:low' => 30, 'redis:low' => 30,
'redis:high' => 30, 'redis:high' => 30,
'redis:delete' => 30, 'redis:delete' => 30,
'redis:story' => 30, 'redis:story' => 30,
'redis:mmo' => 30, 'redis:mmo' => 30,
], 'redis:intbg' => 30,
],
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Job Trimming Times | Job Trimming Times
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| Here you can configure for how long (in minutes) you desire Horizon to | Here you can configure for how long (in minutes) you desire Horizon to
| persist the recent and failed jobs. Typically, recent jobs are kept | persist the recent and failed jobs. Typically, recent jobs are kept
| for one hour while all failed jobs are stored for an entire week. | for one hour while all failed jobs are stored for an entire week.
| |
*/ */
'trim' => [ 'trim' => [
'recent' => 60, 'recent' => 60,
'pending' => 60, 'pending' => 60,
'completed' => 60, 'completed' => 60,
'recent_failed' => 10080, 'recent_failed' => 10080,
'failed' => 10080, 'failed' => 10080,
'monitored' => 10080, 'monitored' => 10080,
], ],
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Metrics | Metrics
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| Here you can configure how many snapshots should be kept to display in | Here you can configure how many snapshots should be kept to display in
| the metrics graph. This will get used in combination with Horizon's | the metrics graph. This will get used in combination with Horizon's
| `horizon:snapshot` schedule to define how long to retain metrics. | `horizon:snapshot` schedule to define how long to retain metrics.
| |
*/ */
'metrics' => [ 'metrics' => [
'trim_snapshots' => [ 'trim_snapshots' => [
'job' => 24, 'job' => 24,
'queue' => 24, 'queue' => 24,
], ],
], ],
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Fast Termination | Fast Termination
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| When this option is enabled, Horizon's "terminate" command will not | When this option is enabled, Horizon's "terminate" command will not
| wait on all of the workers to terminate unless the --wait option | wait on all of the workers to terminate unless the --wait option
| is provided. Fast termination can shorten deployment delay by | is provided. Fast termination can shorten deployment delay by
| allowing a new instance of Horizon to start while the last | allowing a new instance of Horizon to start while the last
| instance will continue to terminate each of its workers. | instance will continue to terminate each of its workers.
| |
*/ */
'fast_termination' => false, 'fast_termination' => false,
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Memory Limit (MB) | Memory Limit (MB)
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| This value describes the maximum amount of memory the Horizon worker | This value describes the maximum amount of memory the Horizon worker
| may consume before it is terminated and restarted. You should set | may consume before it is terminated and restarted. You should set
| this value according to the resources available to your server. | this value according to the resources available to your server.
| |
*/ */
'memory_limit' => env('HORIZON_MEMORY_LIMIT', 64), 'memory_limit' => env('HORIZON_MEMORY_LIMIT', 64),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Queue Worker Configuration | Queue Worker Configuration
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| |
| Here you may define the queue worker settings used by your application | Here you may define the queue worker settings used by your application
| in all environments. These supervisors and settings handle all your | in all environments. These supervisors and settings handle all your
| queued jobs and will be provisioned by Horizon during deployment. | queued jobs and will be provisioned by Horizon during deployment.
| |
*/ */
'environments' => [ 'environments' => [
'production' => [ 'production' => [
'supervisor-1' => [ 'supervisor-1' => [
'connection' => 'redis', 'connection' => 'redis',
'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo'], 'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo', 'intbg'],
'balance' => env('HORIZON_BALANCE_STRATEGY', 'auto'), 'balance' => env('HORIZON_BALANCE_STRATEGY', 'auto'),
'minProcesses' => env('HORIZON_MIN_PROCESSES', 1), 'minProcesses' => env('HORIZON_MIN_PROCESSES', 1),
'maxProcesses' => env('HORIZON_MAX_PROCESSES', 20), 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 20),
'memory' => env('HORIZON_SUPERVISOR_MEMORY', 64), 'memory' => env('HORIZON_SUPERVISOR_MEMORY', 64),
'tries' => env('HORIZON_SUPERVISOR_TRIES', 3), 'tries' => env('HORIZON_SUPERVISOR_TRIES', 3),
'nice' => env('HORIZON_SUPERVISOR_NICE', 0), 'nice' => env('HORIZON_SUPERVISOR_NICE', 0),
'timeout' => env('HORIZON_SUPERVISOR_TIMEOUT', 300), 'timeout' => env('HORIZON_SUPERVISOR_TIMEOUT', 300),
], ],
], ],
'local' => [ 'local' => [
'supervisor-1' => [ 'supervisor-1' => [
'connection' => 'redis', 'connection' => 'redis',
'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo'], 'queue' => ['high', 'default', 'follow', 'shared', 'inbox', 'feed', 'low', 'story', 'delete', 'mmo', 'intbg'],
'balance' => 'auto', 'balance' => 'auto',
'minProcesses' => 1, 'minProcesses' => 1,
'maxProcesses' => 20, 'maxProcesses' => 20,
'memory' => 128, 'memory' => 128,
'tries' => 3, 'tries' => 3,
'nice' => 0, 'nice' => 0,
'timeout' => 300 'timeout' => 300
], ],
], ],
], ],
'darkmode' => env('HORIZON_DARKMODE', false), 'darkmode' => env('HORIZON_DARKMODE', false),
]; ];

View file

@ -39,6 +39,12 @@ return [
// Limit to specific user ids, in comma separated format // Limit to specific user ids, in comma separated format
'user_ids' => env('PF_IMPORT_IG_PERM_ONLY_USER_IDS', null), 'user_ids' => env('PF_IMPORT_IG_PERM_ONLY_USER_IDS', null),
],
'storage' => [
'cloud' => [
'enabled' => env('PF_IMPORT_IG_CLOUD_STORAGE', env('PF_ENABLE_CLOUD', false)),
]
] ]
] ]
]; ];

View file

@ -54,12 +54,14 @@ class AddAccountStatusToProfilesTable extends Migration
$table->string('hub_url')->nullable(); $table->string('hub_url')->nullable();
}); });
Schema::table('stories', function (Blueprint $table) { if (Schema::hasTable('stories')) {
$table->dropColumn('id'); Schema::table('stories', function (Blueprint $table) {
}); $table->dropColumn('id');
Schema::table('stories', function (Blueprint $table) { });
$table->bigIncrements('bigIncrements')->first(); Schema::table('stories', function (Blueprint $table) {
}); $table->bigIncrements('bigIncrements')->first();
});
}
Schema::table('profiles', function (Blueprint $table) { Schema::table('profiles', function (Blueprint $table) {
$table->dropColumn('status'); $table->dropColumn('status');

View file

@ -60,13 +60,7 @@ class Stories extends Migration
{ {
Schema::dropIfExists('story_items'); Schema::dropIfExists('story_items');
Schema::dropIfExists('story_views'); Schema::dropIfExists('story_views');
Schema::dropIfExists('story_reactions');
Schema::table('stories', function (Blueprint $table) { Schema::dropIfExists('stories');
$table->dropColumn(['title','preview_photo','local_only','is_live','broadcast_url','broadcast_key']);
});
Schema::table('story_reactions', function (Blueprint $table) {
$table->dropColumn('story_id');
});
} }
} }

View file

@ -40,6 +40,6 @@ class CreateNewsroomTable extends Migration
*/ */
public function down() public function down()
{ {
Schema::dropIfExists('site_news'); Schema::dropIfExists('newsroom');
} }
} }

View file

@ -27,6 +27,6 @@ class AddCacheLocksTable extends Migration
*/ */
public function down() public function down()
{ {
Schema::dropTable('cache_locks'); Schema::dropIfExists('cache_locks');
} }
} }

View file

@ -33,14 +33,25 @@ class AddComposeSettingsToUserSettingsTable extends Migration
public function down() public function down()
{ {
Schema::table('user_settings', function (Blueprint $table) { Schema::table('user_settings', function (Blueprint $table) {
$table->dropColumn('compose_settings'); if (Schema::hasColumn('user_settings', 'compose_settings')) {
$table->dropColumn('compose_settings');
}
}); });
Schema::table('media', function (Blueprint $table) { Schema::table('media', function (Blueprint $table) {
$table->string('caption')->change(); $table->string('caption')->change();
$table->dropIndex('profile_id');
$table->dropIndex('mime'); $schemaManager = Schema::getConnection()->getDoctrineSchemaManager();
$table->dropIndex('license'); $indexesFound = $schemaManager->listTableIndexes('media');
if (array_key_exists('media_profile_id_index', $indexesFound)) {
$table->dropIndex('media_profile_id_index');
}
if (array_key_exists('media_mime_index', $indexesFound)) {
$table->dropIndex('media_mime_index');
}
if (array_key_exists('media_license_index', $indexesFound)) {
$table->dropIndex('media_license_index');
}
}); });
} }
} }

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('instances', function (Blueprint $table) {
$table->boolean('active_deliver')->nullable()->index()->after('domain');
$table->boolean('valid_nodeinfo')->nullable();
$table->timestamp('nodeinfo_last_fetched')->nullable();
$table->boolean('delivery_timeout')->default(false);
$table->timestamp('delivery_next_after')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('instances', function (Blueprint $table) {
$table->dropColumn('active_deliver');
$table->dropColumn('valid_nodeinfo');
$table->dropColumn('nodeinfo_last_fetched');
$table->dropColumn('delivery_timeout');
$table->dropColumn('delivery_next_after');
});
}
};

View file

@ -28,7 +28,7 @@ return new class extends Migration
$schemaManager = Schema::getConnection()->getDoctrineSchemaManager(); $schemaManager = Schema::getConnection()->getDoctrineSchemaManager();
$indexesFound = $schemaManager->listTableIndexes('user_roles'); $indexesFound = $schemaManager->listTableIndexes('user_roles');
if (array_key_exists('user_roles_profile_id_unique', $indexesFound)) { if (array_key_exists('user_roles_profile_id_unique', $indexesFound)) {
$table->dropIndex('user_roles_profile_id_unique'); $table->dropUnique('user_roles_profile_id_unique');
} }
$table->unsignedBigInteger('profile_id')->unique()->nullable()->index()->change(); $table->unsignedBigInteger('profile_id')->unique()->nullable()->index()->change();
}); });
@ -42,7 +42,11 @@ return new class extends Migration
Schema::dropIfExists('parental_controls'); Schema::dropIfExists('parental_controls');
Schema::table('user_roles', function (Blueprint $table) { Schema::table('user_roles', function (Blueprint $table) {
$table->dropIndex('user_roles_profile_id_unique'); $schemaManager = Schema::getConnection()->getDoctrineSchemaManager();
$indexesFound = $schemaManager->listTableIndexes('user_roles');
if (array_key_exists('user_roles_profile_id_unique', $indexesFound)) {
$table->dropUnique('user_roles_profile_id_unique');
}
$table->unsignedBigInteger('profile_id')->unique()->index()->change(); $table->unsignedBigInteger('profile_id')->unique()->index()->change();
}); });
} }

Binary file not shown.

BIN
public/js/admin.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.js vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/home.chunk.f3f4f632025b560f.js vendored Normal file

Binary file not shown.

BIN
public/js/landing.js vendored

Binary file not shown.

BIN
public/js/manifest.js vendored

Binary file not shown.

BIN
public/js/portfolio.js vendored

Binary file not shown.

BIN
public/js/profile.js vendored

Binary file not shown.

BIN
public/js/spa.js vendored

Binary file not shown.

BIN
public/js/status.js vendored

Binary file not shown.

BIN
public/js/stories.js vendored

Binary file not shown.

BIN
public/js/timeline.js vendored

Binary file not shown.

Binary file not shown.

View file

@ -381,7 +381,7 @@
let file = this.$refs.zipInput.files[0]; let file = this.$refs.zipInput.files[0];
let entries = await this.model(file); let entries = await this.model(file);
if (entries && entries.length) { if (entries && entries.length) {
let files = await entries.filter(e => e.filename === 'content/posts_1.json'); let files = await entries.filter(e => e.filename === 'content/posts_1.json' || e.filename === 'your_instagram_activity/content/posts_1.json');
if(!files || !files.length) { if(!files || !files.length) {
this.contactModal( this.contactModal(
@ -402,7 +402,7 @@
let entries = await this.model(file); let entries = await this.model(file);
if (entries && entries.length) { if (entries && entries.length) {
this.zipFiles = entries; this.zipFiles = entries;
let media = await entries.filter(e => e.filename === 'content/posts_1.json')[0].getData(new zip.TextWriter()); let media = await entries.filter(e => e.filename === 'content/posts_1.json' || e.filename === 'your_instagram_activity/content/posts_1.json')[0].getData(new zip.TextWriter());
this.filterPostMeta(media); this.filterPostMeta(media);
let imgs = await Promise.all(entries.filter(entry => { let imgs = await Promise.all(entries.filter(entry => {

View file

@ -1204,12 +1204,19 @@ export default {
}, 300); }, 300);
}).catch(function(e) { }).catch(function(e) {
switch(e.response.status) { switch(e.response.status) {
case 403:
self.uploading = false;
io.value = null;
swal('Account size limit reached', 'Contact your admin for assistance.', 'error');
self.page = 2;
break;
case 413: case 413:
self.uploading = false; self.uploading = false;
io.value = null; io.value = null;
swal('File is too large', 'The file you uploaded has the size of ' + self.formatBytes(io.size) + '. Unfortunately, only images up to ' + self.formatBytes(self.config.uploader.max_photo_size * 1024) + ' are supported.\nPlease resize the file and try again.', 'error'); swal('File is too large', 'The file you uploaded has the size of ' + self.formatBytes(io.size) + '. Unfortunately, only images up to ' + self.formatBytes(self.config.uploader.max_photo_size * 1024) + ' are supported.\nPlease resize the file and try again.', 'error');
self.page = 2; self.page = 2;
break; break;
case 451: case 451:
self.uploading = false; self.uploading = false;

View file

@ -31,5 +31,5 @@ return [
'unhealthy_backup_found_empty' => 'このアプリケーションのバックアップはありません。', 'unhealthy_backup_found_empty' => 'このアプリケーションのバックアップはありません。',
'unhealthy_backup_found_old' => ':date に作成されたバックアップは古すぎます。', 'unhealthy_backup_found_old' => ':date に作成されたバックアップは古すぎます。',
'unhealthy_backup_found_unknown' => '正確な原因が特定できませんでした。', 'unhealthy_backup_found_unknown' => '正確な原因が特定できませんでした。',
'unhealthy_backup_found_full' => 'バックアップが使用できる容量(:disk_limit)を超えています。(現在の使用量 :disk_usage), 'unhealthy_backup_found_full' => 'バックアップが使用できる容量(:disk_limit)を超えています。(現在の使用量 :disk_usage)',
]; ];