Merge branch 'staging' into dev

This commit is contained in:
daniel 2025-01-05 14:39:20 -07:00 committed by GitHub
commit 3dd515006c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
200 changed files with 8540 additions and 4877 deletions

View file

@ -2,6 +2,12 @@
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.12.3...dev)
### Features
- WebGL photo filters ([#5374](https://github.com/pixelfed/pixelfed/pull/5374))
### OAuth
- Fix oauth oob (urn:ietf:wg:oauth:2.0:oob) support. ([8afbdb03](https://github.com/pixelfed/pixelfed/commit/8afbdb03))
### Updates
- Update AP helpers, reject statuses with invalid dates ([960f3849](https://github.com/pixelfed/pixelfed/commit/960f3849))
- Update DirectMessage API, fix broken threading ([044d410c](https://github.com/pixelfed/pixelfed/commit/044d410c))
@ -12,6 +18,19 @@
- Update DirectMessageController, remove 72h limit for admins ([639df410](https://github.com/pixelfed/pixelfed/commit/639df410))
- Update StatusService, fix newlines ([56c07b7a](https://github.com/pixelfed/pixelfed/commit/56c07b7a))
- Update confirm email template, add plaintext link. Fixes #5375 ([45986707](https://github.com/pixelfed/pixelfed/commit/45986707))
- Update UserVerifyEmail command ([77da9ad8](https://github.com/pixelfed/pixelfed/commit/77da9ad8))
- Update StatusStatelessTransformer, refactor the caption field to be compliant with the MastoAPI. Fixes #5364 ([79039ba5](https://github.com/pixelfed/pixelfed/commit/79039ba5))
- Update mailgun config, add endpoint and scheme ([271d5114](https://github.com/pixelfed/pixelfed/commit/271d5114))
- Update search and status logic to fix postgres bugs ([8c39ef4](https://github.com/pixelfed/pixelfed/commit/8c39ef4))
- Update db, fix sqlite migrations ([#5379](https://github.com/pixelfed/pixelfed/pull/5379))
- Update CatchUnoptimizedMedia command, make 1hr limit opt-in ([99b15b73](https://github.com/pixelfed/pixelfed/commit/99b15b73))
- Update IG, fix Instagram import. Closes #5411 ([fd434aec](https://github.com/pixelfed/pixelfed/commit/fd434aec))
- Update StatusTagsPipeline, fix hashtag bug and formatting ([d516b799](https://github.com/pixelfed/pixelfed/commit/d516b799))
- Update CollectionController, fix showCollection signature ([4e1dd599](https://github.com/pixelfed/pixelfed/commit/4e1dd599))
- Update ApiV1Dot1Controller, fix in-app registration ([56f17b99](https://github.com/pixelfed/pixelfed/commit/56f17b99))
- Update VerifyCsrfToken middleware, add oauth token. Fixes #5426 ([79ebbc2d](https://github.com/pixelfed/pixelfed/commit/79ebbc2d))
- Update AdminSettingsController, increase max photo size limit from 50MB to 1GB ([aa448354](https://github.com/pixelfed/pixelfed/commit/aa448354))
- Update BearerTokenResponse, return scopes in /oauth/token endpoint. Fixes #5286 ([d8f5c302](https://github.com/pixelfed/pixelfed/commit/d8f5c302))
- ([](https://github.com/pixelfed/pixelfed/commit/))
## [v0.12.4 (2024-11-08)](https://github.com/pixelfed/pixelfed/compare/v0.12.4...dev)

View file

@ -4,6 +4,7 @@
<a href="https://circleci.com/gh/pixelfed/pixelfed"><img src="https://circleci.com/gh/pixelfed/pixelfed.svg?style=svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/pixelfed/pixelfed"><img src="https://poser.pugx.org/pixelfed/pixelfed/v/stable.svg" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/pixelfed/pixelfed"><img src="https://poser.pugx.org/pixelfed/pixelfed/license.svg" alt="License"></a>
<a title="Crowdin" target="_blank" href="https://crowdin.com/project/pixelfed"><img src="https://badges.crowdin.net/pixelfed/localized.svg"></a>
</p>
## Introduction

View file

@ -11,13 +11,13 @@ class BearerTokenResponse extends \League\OAuth2\Server\ResponseTypes\BearerToke
* AuthorizationServer::getResponseType() to pull in your version of
* this class rather than the default.
*
* @param AccessTokenEntityInterface $accessToken
*
* @return array
*/
protected function getExtraParams(AccessTokenEntityInterface $accessToken)
{
return [
'scope' => array_map(fn ($scope) => $scope->getIdentifier(), $accessToken->getScopes()),
'created_at' => time(),
];
}

View file

@ -40,10 +40,11 @@ class CatchUnoptimizedMedia extends Command
*/
public function handle()
{
$hasLimit = (bool) config('media.image_optimize.catch_unoptimized_media_hour_limit');
Media::whereNull('processed_at')
->where('created_at', '>', now()->subHours(1))
->where('skip_optimize', '!=', true)
->whereNull('remote_url')
->when($hasLimit, function($q, $hasLimit) {
$q->where('created_at', '>', now()->subHours(1));
})->whereNull('remote_url')
->whereNotNull('status_id')
->whereNotNull('media_path')
->whereIn('mime', [
@ -52,6 +53,7 @@ class CatchUnoptimizedMedia extends Command
])
->chunk(50, function($medias) {
foreach ($medias as $media) {
if ($media->skip_optimize) continue;
ImageOptimize::dispatch($media);
}
});

View file

@ -600,7 +600,7 @@ trait AdminSettingsController
$this->validate($request, [
'image_quality' => 'required|integer|min:1|max:100',
'max_album_length' => 'required|integer|min:1|max:20',
'max_photo_size' => 'required|integer|min:100|max:50000',
'max_photo_size' => 'required|integer|min:100|max:1000000',
'media_types' => 'required',
'optimize_image' => 'required',
'optimize_video' => 'required',

View file

@ -137,7 +137,10 @@ class ApiV1Controller extends Controller
'redirect_uris' => 'required',
]);
$uris = implode(',', explode('\n', $request->redirect_uris));
$uris = collect(explode("\n", $request->redirect_uris))
->map('urldecode')
->filter()
->join(',');
$client = Passport::client()->forceFill([
'user_id' => null,
@ -3509,6 +3512,7 @@ class ApiV1Controller extends Controller
$status = new Status;
$status->caption = $content;
$status->rendered = $defaultCaption;
$status->scope = $visibility;
$status->visibility = $visibility;
$status->profile_id = $user->profile_id;
@ -3533,6 +3537,7 @@ class ApiV1Controller extends Controller
if (! $in_reply_to_id) {
$status = new Status;
$status->caption = $content;
$status->rendered = $defaultCaption;
$status->profile_id = $user->profile_id;
$status->is_nsfw = $cw;
$status->cw_summary = $spoilerText;
@ -3685,7 +3690,10 @@ class ApiV1Controller extends Controller
}
}
$defaultCaption = config_cache('database.default') === 'mysql' ? null : '';
$share = Status::firstOrCreate([
'caption' => $defaultCaption,
'rendered' => $defaultCaption,
'profile_id' => $user->profile_id,
'reblog_of_id' => $status->id,
'type' => 'share',

View file

@ -629,9 +629,6 @@ class ApiV1Dot1Controller extends Controller
abort_if(BouncerService::checkIp($request->ip()), 404);
}
$rl = RateLimiter::attempt('pf:apiv1.1:iarc:'.$request->ip(), config('pixelfed.app_registration_confirm_rate_limit_attempts', 20), function () {}, config('pixelfed.app_registration_confirm_rate_limit_decay', 1800));
abort_if(! $rl, 429, 'Too many requests');
$request->validate([
'user_token' => 'required',
'random_token' => 'required',
@ -658,7 +655,7 @@ class ApiV1Dot1Controller extends Controller
$user->last_active_at = now();
$user->save();
$token = $user->createToken('Pixelfed', ['read', 'write', 'follow', 'admin:read', 'admin:write', 'push']);
$token = $user->createToken('Pixelfed', ['read', 'write', 'follow', 'push']);
return response()->json([
'access_token' => $token->accessToken,
@ -1292,13 +1289,14 @@ class ApiV1Dot1Controller extends Controller
if ($user->last_active_at == null) {
return [];
}
$content = $request->filled('status') ? strip_tags(Purify::clean($request->input('status'))) : null;
$defaultCaption = '';
$content = $request->filled('status') ? strip_tags(Purify::clean($request->input('status'))) : $defaultCaption;
$cw = $user->profile->cw == true ? true : $request->boolean('sensitive', false);
$spoilerText = $cw && $request->filled('spoiler_text') ? $request->input('spoiler_text') : null;
$status = new Status;
$status->caption = $content;
$status->rendered = $defaultCaption;
$status->profile_id = $user->profile_id;
$status->is_nsfw = $cw;
$status->cw_summary = $spoilerText;

View file

@ -29,7 +29,7 @@ class CollectionController extends Controller
return view('collection.create', compact('collection'));
}
public function show(Request $request, int $id)
public function show(Request $request, $id)
{
$user = $request->user();
$collection = CollectionService::getCollection($id);

View file

@ -60,6 +60,7 @@ class CommentController extends Controller
$reply->profile_id = $profile->id;
$reply->is_nsfw = $nsfw;
$reply->caption = Purify::clean($comment);
$reply->rendered = "";
$reply->in_reply_to_id = $status->id;
$reply->in_reply_to_profile_id = $status->profile_id;
$reply->scope = $scope;

View file

@ -30,6 +30,7 @@ use App\Util\Media\License;
use Auth;
use Cache;
use DB;
use Purify;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use League\Fractal;
@ -569,7 +570,9 @@ class ComposeController extends Controller
$status->cw_summary = $request->input('spoiler_text');
}
$status->caption = strip_tags($request->caption);
$defaultCaption = "";
$status->caption = strip_tags($request->input('caption')) ?? $defaultCaption;
$status->rendered = $defaultCaption;
$status->scope = 'draft';
$status->visibility = 'draft';
$status->profile_id = $profile->id;
@ -673,6 +676,7 @@ class ComposeController extends Controller
$place = $request->input('place');
$cw = $request->input('cw');
$tagged = $request->input('tagged');
$defaultCaption = config_cache('database.default') === 'mysql' ? null : "";
if ($place && is_array($place)) {
$status->place_id = $place['id'];
@ -682,7 +686,8 @@ class ComposeController extends Controller
$status->comments_disabled = (bool) $request->input('comments_disabled');
}
$status->caption = strip_tags($request->caption);
$status->caption = $request->filled('caption') ? strip_tags($request->caption) : $defaultCaption;
$status->rendered = $defaultCaption;
$status->profile_id = $profile->id;
$entities = [];
$visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;

View file

@ -0,0 +1,99 @@
<?php
namespace App\Http\Controllers\OAuth;
use Laravel\Passport\Http\Controllers\ApproveAuthorizationController;
use Illuminate\Http\Request;
use League\OAuth2\Server\Exception\OAuthServerException;
use Nyholm\Psr7\Response as Psr7Response;
class OobAuthorizationController extends ApproveAuthorizationController
{
/**
* Approve the authorization request.
*
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function approve(Request $request)
{
$this->assertValidAuthToken($request);
$authRequest = $this->getAuthRequestFromSession($request);
$authRequest->setAuthorizationApproved(true);
return $this->withErrorHandling(function () use ($authRequest) {
$response = $this->server->completeAuthorizationRequest($authRequest, new Psr7Response);
if ($this->isOutOfBandRequest($authRequest)) {
$code = $this->extractAuthorizationCode($response);
return response()->json([
'code' => $code,
'state' => $authRequest->getState()
]);
}
return $this->convertResponse($response);
});
}
/**
* Check if the request is an out-of-band OAuth request.
*
* @param \League\OAuth2\Server\RequestTypes\AuthorizationRequest $authRequest
* @return bool
*/
protected function isOutOfBandRequest($authRequest)
{
return $authRequest->getRedirectUri() === 'urn:ietf:wg:oauth:2.0:oob';
}
/**
* Extract the authorization code from the PSR-7 response.
*
* @param \Psr\Http\Message\ResponseInterface $response
* @return string
* @throws \League\OAuth2\Server\Exception\OAuthServerException
*/
protected function extractAuthorizationCode($response)
{
$location = $response->getHeader('Location')[0] ?? '';
if (empty($location)) {
throw OAuthServerException::serverError('Missing authorization code in response');
}
parse_str(parse_url($location, PHP_URL_QUERY), $params);
if (!isset($params['code'])) {
throw OAuthServerException::serverError('Invalid authorization code format');
}
return $params['code'];
}
/**
* Handle OAuth errors for both redirect and OOB flows.
*
* @param \Closure $callback
* @return \Illuminate\Http\Response
*/
protected function withErrorHandling($callback)
{
try {
return $callback();
} catch (OAuthServerException $e) {
if ($this->isOutOfBandRequest($this->getAuthRequestFromSession(request()))) {
return response()->json([
'error' => $e->getErrorType(),
'message' => $e->getMessage(),
'hint' => $e->getHint()
], $e->getHttpStatusCode());
}
return $this->convertResponse(
$e->generateHttpResponse(new Psr7Response)
);
}
}
}

View file

@ -309,7 +309,7 @@ class StatusController extends Controller
abort_if(! $statusAccount || isset($statusAccount['moved'], $statusAccount['moved']['id']), 422, 'Account moved');
$count = $status->reblogs_count;
$defaultCaption = config_cache('database.default') === 'mysql' ? null : "";
$exists = Status::whereProfileId(Auth::user()->profile->id)
->whereReblogOfId($status->id)
->exists();
@ -324,6 +324,8 @@ class StatusController extends Controller
}
} else {
$share = new Status;
$share->caption = $defaultCaption;
$share->rendered = $defaultCaption;
$share->profile_id = $profile->id;
$share->reblog_of_id = $status->id;
$share->in_reply_to_profile_id = $status->profile_id;

View file

@ -12,6 +12,7 @@ class VerifyCsrfToken extends Middleware
* @var array
*/
protected $except = [
'/api/v1/*'
'/api/v1/*',
'oauth/token'
];
}

View file

@ -2,27 +2,28 @@
namespace App\Jobs\StatusPipeline;
use App\Hashtag;
use App\Jobs\MentionPipeline\MentionPipeline;
use App\Mention;
use App\Services\AccountService;
use App\Services\CustomEmojiService;
use App\Services\StatusService;
use App\Services\TrendingHashtagService;
use App\StatusHashtag;
use App\Util\ActivityPub\Helpers;
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\Services\AccountService;
use App\Services\CustomEmojiService;
use App\Services\StatusService;
use App\Jobs\MentionPipeline\MentionPipeline;
use App\Mention;
use App\Hashtag;
use App\StatusHashtag;
use App\Services\TrendingHashtagService;
use App\Util\ActivityPub\Helpers;
use Illuminate\Support\Facades\DB;
class StatusTagsPipeline implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $activity;
protected $status;
/**
@ -46,92 +47,126 @@ class StatusTagsPipeline implements ShouldQueue
$res = $this->activity;
$status = $this->status;
if(isset($res['tag']['type'], $res['tag']['name'])) {
if (isset($res['tag']['type'], $res['tag']['name'])) {
$res['tag'] = [$res['tag']];
}
$tags = collect($res['tag']);
// Emoji
$tags->filter(function($tag) {
$tags->filter(function ($tag) {
return $tag && isset($tag['id'], $tag['icon'], $tag['name'], $tag['type']) && $tag['type'] == 'Emoji';
})
->map(function($tag) {
CustomEmojiService::import($tag['id'], $this->status->id);
});
->map(function ($tag) {
CustomEmojiService::import($tag['id'], $this->status->id);
});
// Hashtags
$tags->filter(function($tag) {
$tags->filter(function ($tag) {
return $tag && $tag['type'] == 'Hashtag' && isset($tag['href'], $tag['name']);
})
->map(function($tag) use($status) {
$name = substr($tag['name'], 0, 1) == '#' ?
substr($tag['name'], 1) : $tag['name'];
->map(function ($tag) use ($status) {
$name = substr($tag['name'], 0, 1) == '#' ?
substr($tag['name'], 1) : $tag['name'];
$banned = TrendingHashtagService::getBannedHashtagNames();
$banned = TrendingHashtagService::getBannedHashtagNames();
if(count($banned)) {
if(in_array(strtolower($name), array_map('strtolower', $banned))) {
return;
if (count($banned)) {
if (in_array(strtolower($name), array_map('strtolower', $banned))) {
return;
}
}
}
if(config('database.default') === 'pgsql') {
$hashtag = Hashtag::where('name', 'ilike', $name)
->orWhere('slug', 'ilike', str_slug($name, '-', false))
->first();
if (config('database.default') === 'pgsql') {
$hashtag = DB::transaction(function () use ($name) {
$baseSlug = str_slug($name, '-', false);
$slug = $baseSlug;
$counter = 1;
if(!$hashtag) {
$hashtag = Hashtag::updateOrCreate([
'slug' => str_slug($name, '-', false),
'name' => $name
]);
$existing = Hashtag::where('name', $name)
->lockForUpdate()
->first();
if ($existing) {
if ($existing->slug !== $slug) {
while (Hashtag::where('slug', $slug)
->where('name', '!=', $name)
->exists()) {
$slug = $baseSlug.'-'.$counter++;
}
$existing->slug = $slug;
$existing->save();
}
return $existing;
}
while (Hashtag::where('slug', $slug)->exists()) {
$slug = $baseSlug.'-'.$counter++;
}
return Hashtag::create([
'name' => $name,
'slug' => $slug,
]);
});
} else {
$hashtag = DB::transaction(function () use ($name) {
$baseSlug = str_slug($name, '-', false);
$slug = $baseSlug;
$counter = 1;
while (Hashtag::where('slug', $slug)
->where('name', '!=', $name)
->exists()) {
$slug = $baseSlug.'-'.$counter++;
}
return Hashtag::updateOrCreate(
['name' => $name],
['slug' => $slug]
);
});
}
} else {
$hashtag = Hashtag::updateOrCreate([
'slug' => str_slug($name, '-', false),
'name' => $name
StatusHashtag::firstOrCreate([
'status_id' => $status->id,
'hashtag_id' => $hashtag->id,
'profile_id' => $status->profile_id,
'status_visibility' => $status->scope,
]);
}
StatusHashtag::firstOrCreate([
'status_id' => $status->id,
'hashtag_id' => $hashtag->id,
'profile_id' => $status->profile_id,
'status_visibility' => $status->scope
]);
});
});
// Mentions
$tags->filter(function($tag) {
$tags->filter(function ($tag) {
return $tag &&
$tag['type'] == 'Mention' &&
isset($tag['href']) &&
substr($tag['href'], 0, 8) === 'https://';
})
->map(function($tag) use($status) {
if(Helpers::validateLocalUrl($tag['href'])) {
$parts = explode('/', $tag['href']);
if(!$parts) {
return;
->map(function ($tag) use ($status) {
if (Helpers::validateLocalUrl($tag['href'])) {
$parts = explode('/', $tag['href']);
if (! $parts) {
return;
}
$pid = AccountService::usernameToId(end($parts));
if (! $pid) {
return;
}
} else {
$acct = Helpers::profileFetch($tag['href']);
if (! $acct) {
return;
}
$pid = $acct->id;
}
$pid = AccountService::usernameToId(end($parts));
if(!$pid) {
return;
}
} else {
$acct = Helpers::profileFetch($tag['href']);
if(!$acct) {
return;
}
$pid = $acct->id;
}
$mention = new Mention;
$mention->status_id = $status->id;
$mention->profile_id = $pid;
$mention->save();
MentionPipeline::dispatch($status, $mention);
});
$mention = new Mention;
$mention->status_id = $status->id;
$mention->profile_id = $pid;
$mention->save();
MentionPipeline::dispatch($status, $mention);
});
StatusService::refresh($status->id);
}

View file

@ -22,6 +22,7 @@ class Media extends Model
protected $casts = [
'srcset' => 'array',
'deleted_at' => 'datetime',
'skip_optimize' => 'boolean'
];
public function status()

View file

@ -2,81 +2,99 @@
namespace App\Providers;
use App\Observers\{
AvatarObserver,
FollowerObserver,
HashtagFollowObserver,
LikeObserver,
NotificationObserver,
ModLogObserver,
ProfileObserver,
StatusHashtagObserver,
StatusObserver,
UserObserver,
UserFilterObserver,
};
use App\{
Avatar,
Follower,
HashtagFollow,
Like,
Notification,
ModLog,
Profile,
StatusHashtag,
Status,
User,
UserFilter
};
use Auth, Horizon, URL;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Facades\Validator;
use App\Avatar;
use App\Follower;
use App\HashtagFollow;
use App\Like;
use App\ModLog;
use App\Notification;
use App\Observers\AvatarObserver;
use App\Observers\FollowerObserver;
use App\Observers\HashtagFollowObserver;
use App\Observers\LikeObserver;
use App\Observers\ModLogObserver;
use App\Observers\NotificationObserver;
use App\Observers\ProfileObserver;
use App\Observers\StatusHashtagObserver;
use App\Observers\StatusObserver;
use App\Observers\UserFilterObserver;
use App\Observers\UserObserver;
use App\Profile;
use App\Services\AccountService;
use App\Status;
use App\StatusHashtag;
use App\User;
use App\UserFilter;
use Auth;
use Horizon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\ServiceProvider;
use Laravel\Pulse\Facades\Pulse;
use URL;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
if(config('instance.force_https_urls', true)) {
URL::forceScheme('https');
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
if (config('instance.force_https_urls', true)) {
URL::forceScheme('https');
}
Schema::defaultStringLength(191);
Paginator::useBootstrap();
Avatar::observe(AvatarObserver::class);
Follower::observe(FollowerObserver::class);
HashtagFollow::observe(HashtagFollowObserver::class);
Like::observe(LikeObserver::class);
Notification::observe(NotificationObserver::class);
ModLog::observe(ModLogObserver::class);
Profile::observe(ProfileObserver::class);
StatusHashtag::observe(StatusHashtagObserver::class);
User::observe(UserObserver::class);
Schema::defaultStringLength(191);
Paginator::useBootstrap();
Avatar::observe(AvatarObserver::class);
Follower::observe(FollowerObserver::class);
HashtagFollow::observe(HashtagFollowObserver::class);
Like::observe(LikeObserver::class);
Notification::observe(NotificationObserver::class);
ModLog::observe(ModLogObserver::class);
Profile::observe(ProfileObserver::class);
StatusHashtag::observe(StatusHashtagObserver::class);
User::observe(UserObserver::class);
Status::observe(StatusObserver::class);
UserFilter::observe(UserFilterObserver::class);
Horizon::auth(function ($request) {
return Auth::check() && $request->user()->is_admin;
});
Validator::includeUnvalidatedArrayKeys();
UserFilter::observe(UserFilterObserver::class);
Horizon::auth(function ($request) {
return Auth::check() && $request->user()->is_admin;
});
Validator::includeUnvalidatedArrayKeys();
// Model::preventLazyLoading(true);
}
Gate::define('viewPulse', function (User $user) {
return $user->is_admin === 1;
});
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
Pulse::user(function ($user) {
$acct = AccountService::get($user->profile_id, true);
return $acct ? [
'name' => $acct['username'],
'extra' => $user->email,
'avatar' => $acct['avatar'],
] : [
'name' => $user->username,
'extra' => 'DELETED',
'avatar' => '/storage/avatars/default.jpg',
];
});
// Model::preventLazyLoading(true);
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
}

View file

@ -25,6 +25,7 @@ class AuthServiceProvider extends ServiceProvider
public function boot()
{
if(config('pixelfed.oauth_enabled') == true) {
Passport::ignoreRoutes();
Passport::tokensExpireIn(now()->addDays(config('instance.oauth.token_expiration', 356)));
Passport::refreshTokensExpireIn(now()->addDays(config('instance.oauth.refresh_expiration', 400)));
Passport::enableImplicitGrant();

View file

@ -14,7 +14,7 @@ class ImportService
if($userId > 999999) {
return;
}
if($year < 9 || $year > 23) {
if($year < 9 || $year > (int) now()->addYear()->format('y')) {
return;
}
if($month < 1 || $month > 12) {

File diff suppressed because it is too large Load diff

View file

@ -71,9 +71,14 @@ class HttpSignature
public static function instanceActorSign($url, $body = false, $addlHeaders = [], $method = 'post')
{
$keyId = config('app.url').'/i/actor#main-key';
$privateKey = Cache::rememberForever(InstanceActor::PKI_PRIVATE, function () {
return InstanceActor::first()->private_key;
});
if(config_cache('database.default') === 'mysql') {
$privateKey = Cache::rememberForever(InstanceActor::PKI_PRIVATE, function () {
return InstanceActor::first()->private_key;
});
} else {
$privateKey = InstanceActor::first()?->private_key;
}
abort_if(!$privateKey || empty($privateKey), 400, 'Missing instance actor key, please run php artisan instance:actor');
if ($body) {
$digest = self::_digest($body);
}

View file

@ -25,6 +25,7 @@
"laravel/helpers": "^1.1",
"laravel/horizon": "^5.0",
"laravel/passport": "^12.0",
"laravel/pulse": "^1.3",
"laravel/tinker": "^2.9",
"laravel/ui": "^4.2",
"league/flysystem-aws-s3-v3": "^3.0",

1224
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -85,11 +85,29 @@ return [
'database' => env('REDIS_DATABASE', 0),
],
'session' => [
'scheme' => env('REDIS_SCHEME', 'tcp'),
'path' => env('REDIS_PATH'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_DATABASE_SESSION', 1),
],
'pulse' => [
'scheme' => env('REDIS_SCHEME', 'tcp'),
'path' => env('REDIS_PATH'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_DATABASE_PULSE', 2),
],
],
'redis:session' => [
'driver' => 'redis',
'connection' => 'default',
'connection' => 'session',
'prefix' => 'pf_session',
],

View file

@ -143,6 +143,24 @@ return [
'database' => env('REDIS_DATABASE', 0),
],
'session' => [
'scheme' => env('REDIS_SCHEME', 'tcp'),
'path' => env('REDIS_PATH'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_DATABASE_SESSION', 1),
],
'pulse' => [
'scheme' => env('REDIS_SCHEME', 'tcp'),
'path' => env('REDIS_PATH'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_DATABASE_PULSE', 2),
],
],
'dbal' => [

View file

@ -24,6 +24,10 @@ return [
],
],
'image_optimize' => [
'catch_unoptimized_media_hour_limit' => env('PF_CATCHUNOPTIMIZEDMEDIA', false),
],
'hls' => [
/*
|--------------------------------------------------------------------------

236
config/pulse.php Normal file
View file

@ -0,0 +1,236 @@
<?php
use Laravel\Pulse\Http\Middleware\Authorize;
use Laravel\Pulse\Pulse;
use Laravel\Pulse\Recorders;
return [
/*
|--------------------------------------------------------------------------
| Pulse Domain
|--------------------------------------------------------------------------
|
| This is the subdomain which the Pulse dashboard will be accessible from.
| When set to null, the dashboard will reside under the same domain as
| the application. Remember to configure your DNS entries correctly.
|
*/
'domain' => env('PULSE_DOMAIN'),
/*
|--------------------------------------------------------------------------
| Pulse Path
|--------------------------------------------------------------------------
|
| This is the path which the Pulse dashboard will be accessible from. Feel
| free to change this path to anything you'd like. Note that this won't
| affect the path of the internal API that is never exposed to users.
|
*/
'path' => env('PULSE_PATH', 'pulse'),
/*
|--------------------------------------------------------------------------
| Pulse Master Switch
|--------------------------------------------------------------------------
|
| This configuration option may be used to completely disable all Pulse
| data recorders regardless of their individual configurations. This
| provides a single option to quickly disable all Pulse recording.
|
*/
'enabled' => env('PULSE_ENABLED', false),
/*
|--------------------------------------------------------------------------
| Pulse Storage Driver
|--------------------------------------------------------------------------
|
| This configuration option determines which storage driver will be used
| while storing entries from Pulse's recorders. In addition, you also
| may provide any options to configure the selected storage driver.
|
*/
'storage' => [
'driver' => env('PULSE_STORAGE_DRIVER', 'database'),
'trim' => [
'keep' => env('PULSE_STORAGE_KEEP', '7 days'),
],
'database' => [
'connection' => env('PULSE_DB_CONNECTION'),
'chunk' => 1000,
],
],
/*
|--------------------------------------------------------------------------
| Pulse Ingest Driver
|--------------------------------------------------------------------------
|
| This configuration options determines the ingest driver that will be used
| to capture entries from Pulse's recorders. Ingest drivers are great to
| free up your request workers quickly by offloading the data storage.
|
*/
'ingest' => [
'driver' => env('PULSE_INGEST_DRIVER', 'storage'),
'buffer' => env('PULSE_INGEST_BUFFER', 5_000),
'trim' => [
'lottery' => [1, 1_000],
'keep' => env('PULSE_INGEST_KEEP', '7 days'),
],
'redis' => [
'connection' => env('PULSE_REDIS_CONNECTION'),
'chunk' => 1000,
],
],
/*
|--------------------------------------------------------------------------
| Pulse Cache Driver
|--------------------------------------------------------------------------
|
| This configuration option determines the cache driver that will be used
| for various tasks, including caching dashboard results, establishing
| locks for events that should only occur on one server and signals.
|
*/
'cache' => env('PULSE_CACHE_DRIVER'),
/*
|--------------------------------------------------------------------------
| Pulse Route Middleware
|--------------------------------------------------------------------------
|
| These middleware will be assigned to every Pulse route, giving you the
| chance to add your own middleware to this list or change any of the
| existing middleware. Of course, reasonable defaults are provided.
|
*/
'middleware' => [
'web',
Authorize::class,
],
/*
|--------------------------------------------------------------------------
| Pulse Recorders
|--------------------------------------------------------------------------
|
| The following array lists the "recorders" that will be registered with
| Pulse, along with their configuration. Recorders gather application
| event data from requests and tasks to pass to your ingest driver.
|
*/
'recorders' => [
Recorders\CacheInteractions::class => [
'enabled' => env('PULSE_CACHE_INTERACTIONS_ENABLED', true),
'sample_rate' => env('PULSE_CACHE_INTERACTIONS_SAMPLE_RATE', 1),
'ignore' => [
...Pulse::defaultVendorCacheKeys(),
],
'groups' => [
'/^job-exceptions:.*/' => 'job-exceptions:*',
// '/:\d+/' => ':*',
],
],
Recorders\Exceptions::class => [
'enabled' => env('PULSE_EXCEPTIONS_ENABLED', true),
'sample_rate' => env('PULSE_EXCEPTIONS_SAMPLE_RATE', 1),
'location' => env('PULSE_EXCEPTIONS_LOCATION', true),
'ignore' => [
// '/^Package\\\\Exceptions\\\\/',
],
],
Recorders\Queues::class => [
'enabled' => env('PULSE_QUEUES_ENABLED', true),
'sample_rate' => env('PULSE_QUEUES_SAMPLE_RATE', 1),
'ignore' => [
// '/^Package\\\\Jobs\\\\/',
],
],
Recorders\Servers::class => [
'server_name' => env('PULSE_SERVER_NAME', gethostname()),
'directories' => explode(':', env('PULSE_SERVER_DIRECTORIES', '/')),
],
Recorders\SlowJobs::class => [
'enabled' => env('PULSE_SLOW_JOBS_ENABLED', true),
'sample_rate' => env('PULSE_SLOW_JOBS_SAMPLE_RATE', 1),
'threshold' => env('PULSE_SLOW_JOBS_THRESHOLD', 1000),
'ignore' => [
// '/^Package\\\\Jobs\\\\/',
],
],
Recorders\SlowOutgoingRequests::class => [
'enabled' => env('PULSE_SLOW_OUTGOING_REQUESTS_ENABLED', true),
'sample_rate' => env('PULSE_SLOW_OUTGOING_REQUESTS_SAMPLE_RATE', 1),
'threshold' => env('PULSE_SLOW_OUTGOING_REQUESTS_THRESHOLD', 1000),
'ignore' => [
// '#^http://127\.0\.0\.1:13714#', // Inertia SSR...
],
'groups' => [
// '#^https://api\.github\.com/repos/.*$#' => 'api.github.com/repos/*',
// '#^https?://([^/]*).*$#' => '\1',
// '#/\d+#' => '/*',
],
],
Recorders\SlowQueries::class => [
'enabled' => env('PULSE_SLOW_QUERIES_ENABLED', true),
'sample_rate' => env('PULSE_SLOW_QUERIES_SAMPLE_RATE', 1),
'threshold' => env('PULSE_SLOW_QUERIES_THRESHOLD', 1000),
'location' => env('PULSE_SLOW_QUERIES_LOCATION', true),
'max_query_length' => env('PULSE_SLOW_QUERIES_MAX_QUERY_LENGTH'),
'ignore' => [
'/(["`])pulse_[\w]+?\1/', // Pulse tables...
'/(["`])telescope_[\w]+?\1/', // Telescope tables...
],
],
Recorders\SlowRequests::class => [
'enabled' => env('PULSE_SLOW_REQUESTS_ENABLED', true),
'sample_rate' => env('PULSE_SLOW_REQUESTS_SAMPLE_RATE', 1),
'threshold' => env('PULSE_SLOW_REQUESTS_THRESHOLD', 1000),
'ignore' => [
'#^/'.env('PULSE_PATH', 'pulse').'$#', // Pulse dashboard...
'#^/telescope#', // Telescope dashboard...
],
],
Recorders\UserJobs::class => [
'enabled' => env('PULSE_USER_JOBS_ENABLED', true),
'sample_rate' => env('PULSE_USER_JOBS_SAMPLE_RATE', 1),
'ignore' => [
// '/^Package\\\\Jobs\\\\/',
],
],
Recorders\UserRequests::class => [
'enabled' => env('PULSE_USER_REQUESTS_ENABLED', true),
'sample_rate' => env('PULSE_USER_REQUESTS_SAMPLE_RATE', 1),
'ignore' => [
'#^/'.env('PULSE_PATH', 'pulse').'$#', // Pulse dashboard...
'#^/telescope#', // Telescope dashboard...
],
],
],
];

View file

@ -17,6 +17,8 @@ return [
'mailgun' => [
'domain' => env('MAILGUN_DOMAIN'),
'secret' => env('MAILGUN_SECRET'),
'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
'scheme' => 'https',
],
'ses' => [

View file

@ -19,7 +19,7 @@ class CreateDirectMessagesTable extends Migration
$table->bigInteger('from_id')->unsigned()->index();
$table->string('from_profile_ids')->nullable();
$table->boolean('group_message')->default(false);
$table->bigInteger('status_id')->unsigned()->integer();
$table->bigInteger('status_id')->unsigned();
$table->unique(['to_id', 'from_id', 'status_id']);
$table->timestamp('read_at')->nullable();
$table->timestamps();

View file

@ -0,0 +1,84 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Laravel\Pulse\Support\PulseMigration;
return new class extends PulseMigration
{
/**
* Run the migrations.
*/
public function up(): void
{
if (! $this->shouldRun()) {
return;
}
Schema::create('pulse_values', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('timestamp');
$table->string('type');
$table->mediumText('key');
match ($this->driver()) {
'mariadb', 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'),
'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'),
'sqlite' => $table->string('key_hash'),
};
$table->mediumText('value');
$table->index('timestamp'); // For trimming...
$table->index('type'); // For fast lookups and purging...
$table->unique(['type', 'key_hash']); // For data integrity and upserts...
});
Schema::create('pulse_entries', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('timestamp');
$table->string('type');
$table->mediumText('key');
match ($this->driver()) {
'mariadb', 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'),
'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'),
'sqlite' => $table->string('key_hash'),
};
$table->bigInteger('value')->nullable();
$table->index('timestamp'); // For trimming...
$table->index('type'); // For purging...
$table->index('key_hash'); // For mapping...
$table->index(['timestamp', 'type', 'key_hash', 'value']); // For aggregate queries...
});
Schema::create('pulse_aggregates', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('bucket');
$table->unsignedMediumInteger('period');
$table->string('type');
$table->mediumText('key');
match ($this->driver()) {
'mariadb', 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'),
'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'),
'sqlite' => $table->string('key_hash'),
};
$table->string('aggregate');
$table->decimal('value', 20, 2);
$table->unsignedInteger('count')->nullable();
$table->unique(['bucket', 'period', 'type', 'aggregate', 'key_hash']); // Force "on duplicate update"...
$table->index(['period', 'bucket']); // For trimming...
$table->index('type'); // For purging...
$table->index(['period', 'type', 'aggregate', 'bucket']); // For aggregate queries...
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('pulse_values');
Schema::dropIfExists('pulse_entries');
Schema::dropIfExists('pulse_aggregates');
}
};

View file

@ -2,6 +2,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
@ -12,6 +13,9 @@ return new class extends Migration
public function up(): void
{
Schema::table('group_posts', function (Blueprint $table) {
if (DB::getDriverName() === 'sqlite') {
$table->dropUnique(['status_id']);
}
$table->dropColumn('status_id');
$table->dropColumn('reply_child_id');
$table->dropColumn('in_reply_to_id');

View file

@ -17,7 +17,7 @@ services:
#
# See: https://github.com/nginx-proxy/nginx-proxy/tree/main/docs
proxy:
image: nginxproxy/nginx-proxy:1.4
image: nginxproxy/nginx-proxy:1.6.2
container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-proxy"
restart: unless-stopped
profiles:

842
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -71,6 +71,7 @@
"vue-loading-overlay": "^3.3.3",
"vue-timeago": "^5.1.2",
"vue-tribute": "^1.0.7",
"webgl-media-editor": "^0.0.1",
"zuck.js": "^1.6.0"
},
"collective": {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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.

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