Merge branch 'dev' into 2-translate-help-center-pages

This commit is contained in:
Felipe Mateus 2025-01-09 21:22:15 -03:00 committed by GitHub
commit 6662118d4a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
259 changed files with 11601 additions and 7171 deletions

View file

@ -1270,7 +1270,7 @@ DOCKER_WORKER_HEALTHCHECK_INTERVAL="${DOCKER_ALL_DEFAULT_HEALTHCHECK_INTERVAL:?e
# #
# @see https://hub.docker.com/r/nginxproxy/nginx-proxy # @see https://hub.docker.com/r/nginxproxy/nginx-proxy
# @dottie/validate required # @dottie/validate required
DOCKER_PROXY_VERSION="1.4" DOCKER_PROXY_VERSION="1.6"
# How often Docker health check should run for [proxy] service # How often Docker health check should run for [proxy] service
# @dottie/validate required # @dottie/validate required

View file

@ -1,6 +1,39 @@
# Release Notes # Release Notes
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.12.3...dev) ## [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))
- Update Status caption render logic ([fb8dbb95](https://github.com/pixelfed/pixelfed/commit/fb8dbb95))
- Update ApiV1Controller, fix bookmark bug. Closes #5216 ([9f7cc52c](https://github.com/pixelfed/pixelfed/commit/9f7cc52c))
- Update Status caption logic, stop storing duplicate html caption in db and defer to cached StatusService rendering ([9eeb7b67](https://github.com/pixelfed/pixelfed/commit/9eeb7b67))
- Update AutolinkService, optimize lookups ([eac2c196](https://github.com/pixelfed/pixelfed/commit/eac2c196))
- 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))
- Update hashtag component, fix missing video thumbnails ([witten](https://github.com/witten)) ([#5427](https://github.com/pixelfed/pixelfed/pull/5427))
- Update AP Status Transformer, fix inReplyTo. Fixes #5409 ([83cc932f](https://github.com/pixelfed/pixelfed/commit/83cc932f))
- Update Data Export, refactor following/follower and statuses exports to allow accounts of any size with api entity instead of ap ([0d25917c](https://github.com/pixelfed/pixelfed/commit/0d25917c))
- ([](https://github.com/pixelfed/pixelfed/commit/)) - ([](https://github.com/pixelfed/pixelfed/commit/))
## [v0.12.4 (2024-11-08)](https://github.com/pixelfed/pixelfed/compare/v0.12.4...dev) ## [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://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/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 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> </p>
## Introduction ## Introduction

View file

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

View file

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

View file

@ -0,0 +1,85 @@
<?php
namespace App\Console\Commands;
use App\User;
use App\Profile;
use Illuminate\Console\Command;
use function Laravel\Prompts\search;
use function Laravel\Prompts\text;
use function Laravel\Prompts\confirm;
class ReclaimUsername extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:reclaim-username';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Force delete a user and their profile to reclaim a username';
/**
* Execute the console command.
*/
public function handle()
{
$username = search(
label: 'What username would you like to reclaim?',
options: fn (string $search) => strlen($search) > 0 ? $this->getUsernameOptions($search) : [],
required: true
);
$user = User::whereUsername($username)->withTrashed()->first();
$profile = Profile::whereUsername($username)->withTrashed()->first();
if (!$user && !$profile) {
$this->error("No user or profile found with username: {$username}");
return Command::FAILURE;
}
if ($user->delete_after === null || $user->status !== 'deleted') {
$this->error("Cannot reclaim an active account: {$username}");
return Command::FAILURE;
}
$confirm = confirm(
label: "Are you sure you want to force delete user and profile with username: {$username}?",
default: false
);
if (!$confirm) {
$this->info('Operation cancelled.');
return Command::SUCCESS;
}
if ($user) {
$user->forceDelete();
$this->info("User {$username} has been force deleted.");
}
if ($profile) {
$profile->forceDelete();
$this->info("Profile {$username} has been force deleted.");
}
$this->info('Username reclaimed successfully!');
return Command::SUCCESS;
}
private function getUsernameOptions(string $search = ''): array
{
return User::where('username', 'like', "{$search}%")
->withTrashed()
->whereNotNull('delete_after')
->take(10)
->pluck('username')
->toArray();
}
}

View file

@ -2,17 +2,16 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\ImportPost;
use App\Services\ImportService;
use App\Media; use App\Media;
use App\Models\ImportPost;
use App\Profile; use App\Profile;
use App\Status;
use Storage;
use App\Services\AccountService; use App\Services\AccountService;
use App\Services\ImportService;
use App\Services\MediaPathService; use App\Services\MediaPathService;
use App\Status;
use Illuminate\Console\Command;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use App\Util\Lexer\Autolink; use Storage;
class TransformImports extends Command class TransformImports extends Command
{ {
@ -52,6 +51,7 @@ class TransformImports extends Command
if (! $profile) { if (! $profile) {
$ip->skip_missing_media = true; $ip->skip_missing_media = true;
$ip->save(); $ip->save();
continue; continue;
} }
@ -66,6 +66,7 @@ class TransformImports extends Command
if ($exists == true) { if ($exists == true) {
$ip->skip_missing_media = true; $ip->skip_missing_media = true;
$ip->save(); $ip->save();
continue; continue;
} }
@ -73,6 +74,7 @@ class TransformImports extends Command
if (! $idk) { if (! $idk) {
$ip->skip_missing_media = true; $ip->skip_missing_media = true;
$ip->save(); $ip->save();
continue; continue;
} }
@ -81,6 +83,7 @@ class TransformImports extends Command
ImportService::getPostCount($profile->id, true); ImportService::getPostCount($profile->id, true);
$ip->skip_missing_media = true; $ip->skip_missing_media = true;
$ip->save(); $ip->save();
continue; continue;
} }
@ -96,6 +99,7 @@ class TransformImports extends Command
if ($missingMedia === true) { if ($missingMedia === true) {
$ip->skip_missing_media = true; $ip->skip_missing_media = true;
$ip->save(); $ip->save();
continue; continue;
} }
@ -103,7 +107,6 @@ class TransformImports extends Command
$status = new Status; $status = new Status;
$status->profile_id = $pid; $status->profile_id = $pid;
$status->caption = $caption; $status->caption = $caption;
$status->rendered = strlen(trim($caption)) ? Autolink::create()->autolink($ip->caption) : null;
$status->type = $ip->post_type; $status->type = $ip->post_type;
$status->scope = 'unlisted'; $status->scope = 'unlisted';
@ -120,6 +123,7 @@ class TransformImports extends Command
if (! Storage::exists($og)) { if (! Storage::exists($og)) {
$ip->skip_missing_media = true; $ip->skip_missing_media = true;
$ip->save(); $ip->save();
continue; continue;
} }
$size = Storage::size($og); $size = Storage::size($og);

View file

@ -5,8 +5,9 @@ namespace App\Console\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use App\User; use App\User;
use Illuminate\Contracts\Console\PromptsForMissingInput;
class UserVerifyEmail extends Command class UserVerifyEmail extends Command implements PromptsForMissingInput
{ {
/** /**
* The name and signature of the console command. * The name and signature of the console command.
@ -39,13 +40,19 @@ class UserVerifyEmail extends Command
*/ */
public function handle() public function handle()
{ {
$user = User::whereUsername($this->argument('username'))->first(); $username = $this->argument('username');
$user = User::whereUsername($username)->first();
if(!$user) { if(!$user) {
$this->error('Username not found'); $this->error('Username not found');
return; return;
} }
if($user->email_verified_at) {
$this->error('Email already verified ' . $user->email_verified_at->diffForHumans());
return;
}
$user->email_verified_at = now(); $user->email_verified_at = now();
$user->save(); $user->save();
$this->info('Successfully verified email address for ' . $user->username); $this->info('Successfully verified email address for ' . $user->username);

View file

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

View file

@ -137,7 +137,10 @@ class ApiV1Controller extends Controller
'redirect_uris' => 'required', '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([ $client = Passport::client()->forceFill([
'user_id' => null, 'user_id' => null,
@ -1426,6 +1429,8 @@ class ApiV1Controller extends Controller
$status['favourited'] = true; $status['favourited'] = true;
$status['favourites_count'] = $status['favourites_count'] + 1; $status['favourites_count'] = $status['favourites_count'] + 1;
$status['bookmarked'] = BookmarkService::get($user->profile_id, $status['id']);
$status['reblogged'] = ReblogService::get($user->profile_id, $status['id']);
return $this->json($status); return $this->json($status);
} }
@ -1484,6 +1489,8 @@ class ApiV1Controller extends Controller
$status['favourited'] = false; $status['favourited'] = false;
$status['favourites_count'] = isset($ogStatus) ? $ogStatus->likes_count : $status['favourites_count'] - 1; $status['favourites_count'] = isset($ogStatus) ? $ogStatus->likes_count : $status['favourites_count'] - 1;
$status['bookmarked'] = BookmarkService::get($user->profile_id, $status['id']);
$status['reblogged'] = ReblogService::get($user->profile_id, $status['id']);
return $this->json($status); return $this->json($status);
} }
@ -1878,7 +1885,7 @@ class ApiV1Controller extends Controller
$media->original_sha256 = $hash; $media->original_sha256 = $hash;
$media->size = $photo->getSize(); $media->size = $photo->getSize();
$media->mime = $mime; $media->mime = $mime;
$media->caption = $request->input('description') ?? ""; $media->caption = $request->input('description') ?? '';
$media->filter_class = $filterClass; $media->filter_class = $filterClass;
$media->filter_name = $filterName; $media->filter_name = $filterName;
if ($license) { if ($license) {
@ -2106,7 +2113,7 @@ class ApiV1Controller extends Controller
$media->original_sha256 = $hash; $media->original_sha256 = $hash;
$media->size = $photo->getSize(); $media->size = $photo->getSize();
$media->mime = $mime; $media->mime = $mime;
$media->caption = $request->input('description') ?? ""; $media->caption = $request->input('description') ?? '';
$media->filter_class = $filterClass; $media->filter_class = $filterClass;
$media->filter_name = $filterName; $media->filter_name = $filterName;
if ($license) { if ($license) {
@ -3490,8 +3497,8 @@ class ApiV1Controller extends Controller
return []; return [];
} }
$content = strip_tags($request->input('status')); $defaultCaption = "";
$rendered = Autolink::create()->autolink($content); $content = $request->filled('status') ? strip_tags($request->input('status')) : $defaultCaption;
$cw = $user->profile->cw == true ? true : $request->boolean('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;
@ -3505,7 +3512,7 @@ class ApiV1Controller extends Controller
$status = new Status; $status = new Status;
$status->caption = $content; $status->caption = $content;
$status->rendered = $rendered; $status->rendered = $defaultCaption;
$status->scope = $visibility; $status->scope = $visibility;
$status->visibility = $visibility; $status->visibility = $visibility;
$status->profile_id = $user->profile_id; $status->profile_id = $user->profile_id;
@ -3530,7 +3537,7 @@ class ApiV1Controller extends Controller
if (! $in_reply_to_id) { if (! $in_reply_to_id) {
$status = new Status; $status = new Status;
$status->caption = $content; $status->caption = $content;
$status->rendered = $rendered; $status->rendered = $defaultCaption;
$status->profile_id = $user->profile_id; $status->profile_id = $user->profile_id;
$status->is_nsfw = $cw; $status->is_nsfw = $cw;
$status->cw_summary = $spoilerText; $status->cw_summary = $spoilerText;
@ -3683,7 +3690,10 @@ class ApiV1Controller extends Controller
} }
} }
$defaultCaption = config_cache('database.default') === 'mysql' ? null : '';
$share = Status::firstOrCreate([ $share = Status::firstOrCreate([
'caption' => $defaultCaption,
'rendered' => $defaultCaption,
'profile_id' => $user->profile_id, 'profile_id' => $user->profile_id,
'reblog_of_id' => $status->id, 'reblog_of_id' => $status->id,
'type' => 'share', 'type' => 'share',
@ -3698,6 +3708,8 @@ class ApiV1Controller extends Controller
ReblogService::add($user->profile_id, $status->id); ReblogService::add($user->profile_id, $status->id);
$res = StatusService::getMastodon($status->id); $res = StatusService::getMastodon($status->id);
$res['reblogged'] = true; $res['reblogged'] = true;
$res['favourited'] = LikeService::liked($user->profile_id, $status->id);
$res['bookmarked'] = BookmarkService::get($user->profile_id, $status->id);
return $this->json($res); return $this->json($res);
} }
@ -3744,6 +3756,8 @@ class ApiV1Controller extends Controller
$res = StatusService::getMastodon($status->id); $res = StatusService::getMastodon($status->id);
$res['reblogged'] = false; $res['reblogged'] = false;
$res['favourited'] = LikeService::liked($user->profile_id, $status->id);
$res['bookmarked'] = BookmarkService::get($user->profile_id, $status->id);
return $this->json($res); return $this->json($res);
} }
@ -3951,6 +3965,7 @@ class ApiV1Controller extends Controller
abort_unless($request->user()->tokenCan('write'), 403); abort_unless($request->user()->tokenCan('write'), 403);
$status = Status::findOrFail($id); $status = Status::findOrFail($id);
$user = $request->user();
$pid = $request->user()->profile_id; $pid = $request->user()->profile_id;
$account = AccountService::get($status->profile_id); $account = AccountService::get($status->profile_id);
abort_if(isset($account['moved'], $account['moved']['id']), 422, 'Cannot bookmark a post from an account that has migrated'); abort_if(isset($account['moved'], $account['moved']['id']), 422, 'Cannot bookmark a post from an account that has migrated');
@ -3994,6 +4009,7 @@ class ApiV1Controller extends Controller
$status = Status::findOrFail($id); $status = Status::findOrFail($id);
$pid = $request->user()->profile_id; $pid = $request->user()->profile_id;
$user = $request->user();
abort_if($user->has_roles && ! UserRoleService::can('can-bookmark', $user->id), 403, 'Invalid permissions for this action'); abort_if($user->has_roles && ! UserRoleService::can('can-bookmark', $user->id), 403, 'Invalid permissions for this action');
abort_if($status->in_reply_to_id || $status->reblog_of_id, 404); abort_if($status->in_reply_to_id || $status->reblog_of_id, 404);

View file

@ -37,7 +37,6 @@ use App\Status;
use App\StatusArchived; use App\StatusArchived;
use App\User; use App\User;
use App\UserSetting; use App\UserSetting;
use App\Util\Lexer\Autolink;
use App\Util\Lexer\RestrictedNames; use App\Util\Lexer\RestrictedNames;
use Cache; use Cache;
use DB; use DB;
@ -49,6 +48,7 @@ use Jenssegers\Agent\Agent;
use League\Fractal; use League\Fractal;
use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Serializer\ArraySerializer;
use Mail; use Mail;
use Purify;
class ApiV1Dot1Controller extends Controller class ApiV1Dot1Controller extends Controller
{ {
@ -629,9 +629,6 @@ class ApiV1Dot1Controller extends Controller
abort_if(BouncerService::checkIp($request->ip()), 404); 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([ $request->validate([
'user_token' => 'required', 'user_token' => 'required',
'random_token' => 'required', 'random_token' => 'required',
@ -658,7 +655,7 @@ class ApiV1Dot1Controller extends Controller
$user->last_active_at = now(); $user->last_active_at = now();
$user->save(); $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([ return response()->json([
'access_token' => $token->accessToken, 'access_token' => $token->accessToken,
@ -1292,15 +1289,14 @@ class ApiV1Dot1Controller extends Controller
if ($user->last_active_at == null) { if ($user->last_active_at == null) {
return []; return [];
} }
$defaultCaption = '';
$content = strip_tags($request->input('status')); $content = $request->filled('status') ? strip_tags(Purify::clean($request->input('status'))) : $defaultCaption;
$rendered = Autolink::create()->autolink($content);
$cw = $user->profile->cw == true ? true : $request->boolean('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;
$status = new Status; $status = new Status;
$status->caption = $content; $status->caption = $content;
$status->rendered = $rendered; $status->rendered = $defaultCaption;
$status->profile_id = $user->profile_id; $status->profile_id = $user->profile_id;
$status->is_nsfw = $cw; $status->is_nsfw = $cw;
$status->cw_summary = $spoilerText; $status->cw_summary = $spoilerText;

View file

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

View file

@ -8,12 +8,12 @@ use App\Services\StatusService;
use App\Status; use App\Status;
use App\Transformer\Api\StatusTransformer; use App\Transformer\Api\StatusTransformer;
use App\UserFilter; use App\UserFilter;
use App\Util\Lexer\Autolink;
use Auth; use Auth;
use DB; use DB;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use League\Fractal; use League\Fractal;
use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Serializer\ArraySerializer;
use Purify;
class CommentController extends Controller class CommentController extends Controller
{ {
@ -56,12 +56,11 @@ class CommentController extends Controller
$reply = DB::transaction(function () use ($comment, $status, $profile, $nsfw) { $reply = DB::transaction(function () use ($comment, $status, $profile, $nsfw) {
$scope = $profile->is_private == true ? 'private' : 'public'; $scope = $profile->is_private == true ? 'private' : 'public';
$autolink = Autolink::create()->autolink($comment); $reply = new Status;
$reply = new Status();
$reply->profile_id = $profile->id; $reply->profile_id = $profile->id;
$reply->is_nsfw = $nsfw; $reply->is_nsfw = $nsfw;
$reply->caption = e($comment); $reply->caption = Purify::clean($comment);
$reply->rendered = $autolink; $reply->rendered = "";
$reply->in_reply_to_id = $status->id; $reply->in_reply_to_id = $status->id;
$reply->in_reply_to_profile_id = $status->profile_id; $reply->in_reply_to_profile_id = $status->profile_id;
$reply->scope = $scope; $reply->scope = $scope;
@ -76,9 +75,9 @@ class CommentController extends Controller
CommentPipeline::dispatch($status, $reply); CommentPipeline::dispatch($status, $reply);
if ($request->ajax()) { if ($request->ajax()) {
$fractal = new Fractal\Manager(); $fractal = new Fractal\Manager;
$fractal->setSerializer(new ArraySerializer()); $fractal->setSerializer(new ArraySerializer);
$entity = new Fractal\Resource\Item($reply, new StatusTransformer()); $entity = new Fractal\Resource\Item($reply, new StatusTransformer);
$entity = $fractal->createData($entity)->toArray(); $entity = $fractal->createData($entity)->toArray();
$response = [ $response = [
'code' => 200, 'code' => 200,

View file

@ -25,12 +25,12 @@ use App\Services\UserStorageService;
use App\Status; use App\Status;
use App\Transformer\Api\MediaTransformer; use App\Transformer\Api\MediaTransformer;
use App\UserFilter; use App\UserFilter;
use App\Util\Lexer\Autolink;
use App\Util\Media\Filter; use App\Util\Media\Filter;
use App\Util\Media\License; use App\Util\Media\License;
use Auth; use Auth;
use Cache; use Cache;
use DB; use DB;
use Purify;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use League\Fractal; use League\Fractal;
@ -43,8 +43,8 @@ class ComposeController extends Controller
public function __construct() public function __construct()
{ {
$this->middleware('auth'); $this->middleware('auth');
$this->fractal = new Fractal\Manager(); $this->fractal = new Fractal\Manager;
$this->fractal->setSerializer(new ArraySerializer()); $this->fractal->setSerializer(new ArraySerializer);
} }
public function show(Request $request) public function show(Request $request)
@ -112,14 +112,14 @@ class ComposeController extends Controller
abort_if(MediaBlocklistService::exists($hash) == true, 451); abort_if(MediaBlocklistService::exists($hash) == true, 451);
$media = new Media(); $media = new Media;
$media->status_id = null; $media->status_id = null;
$media->profile_id = $profile->id; $media->profile_id = $profile->id;
$media->user_id = $user->id; $media->user_id = $user->id;
$media->media_path = $path; $media->media_path = $path;
$media->original_sha256 = $hash; $media->original_sha256 = $hash;
$media->size = $photo->getSize(); $media->size = $photo->getSize();
$media->caption = ""; $media->caption = '';
$media->mime = $mime; $media->mime = $mime;
$media->filter_class = $filterClass; $media->filter_class = $filterClass;
$media->filter_name = $filterName; $media->filter_name = $filterName;
@ -151,7 +151,7 @@ class ComposeController extends Controller
$user->save(); $user->save();
Cache::forget($limitKey); Cache::forget($limitKey);
$resource = new Fractal\Resource\Item($media, new MediaTransformer()); $resource = new Fractal\Resource\Item($media, new MediaTransformer);
$res = $this->fractal->createData($resource)->toArray(); $res = $this->fractal->createData($resource)->toArray();
$res['preview_url'] = $preview_url; $res['preview_url'] = $preview_url;
$res['url'] = $url; $res['url'] = $url;
@ -570,8 +570,9 @@ class ComposeController extends Controller
$status->cw_summary = $request->input('spoiler_text'); $status->cw_summary = $request->input('spoiler_text');
} }
$status->caption = strip_tags($request->caption); $defaultCaption = "";
$status->rendered = Autolink::create()->autolink($status->caption); $status->caption = strip_tags($request->input('caption')) ?? $defaultCaption;
$status->rendered = $defaultCaption;
$status->scope = 'draft'; $status->scope = 'draft';
$status->visibility = 'draft'; $status->visibility = 'draft';
$status->profile_id = $profile->id; $status->profile_id = $profile->id;
@ -675,6 +676,7 @@ class ComposeController extends Controller
$place = $request->input('place'); $place = $request->input('place');
$cw = $request->input('cw'); $cw = $request->input('cw');
$tagged = $request->input('tagged'); $tagged = $request->input('tagged');
$defaultCaption = config_cache('database.default') === 'mysql' ? null : "";
if ($place && is_array($place)) { if ($place && is_array($place)) {
$status->place_id = $place['id']; $status->place_id = $place['id'];
@ -684,7 +686,8 @@ class ComposeController extends Controller
$status->comments_disabled = (bool) $request->input('comments_disabled'); $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; $status->profile_id = $profile->id;
$entities = []; $entities = [];
$visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility; $visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;
@ -693,7 +696,6 @@ class ComposeController extends Controller
$status->visibility = $visibility; $status->visibility = $visibility;
$status->scope = $visibility; $status->scope = $visibility;
$status->type = 'text'; $status->type = 'text';
$status->rendered = Autolink::create()->autolink($status->caption);
$status->entities = json_encode(array_merge([ $status->entities = json_encode(array_merge([
'timg' => [ 'timg' => [
'version' => 0, 'version' => 0,
@ -806,7 +808,6 @@ class ComposeController extends Controller
$status = new Status; $status = new Status;
$status->profile_id = $request->user()->profile_id; $status->profile_id = $request->user()->profile_id;
$status->caption = $request->input('caption'); $status->caption = $request->input('caption');
$status->rendered = Autolink::create()->autolink($status->caption);
$status->visibility = 'draft'; $status->visibility = 'draft';
$status->scope = 'draft'; $status->scope = 'draft';
$status->type = 'poll'; $status->type = 'poll';

View file

@ -22,6 +22,7 @@ use App\Services\WebfingerService;
use App\Status; use App\Status;
use App\UserFilter; use App\UserFilter;
use App\Util\ActivityPub\Helpers; use App\Util\ActivityPub\Helpers;
use App\Util\Lexer\Autolink;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -306,7 +307,9 @@ class DirectMessageController extends Controller
$user = $request->user(); $user = $request->user();
abort_if($user->has_roles && ! UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action'); abort_if($user->has_roles && ! UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action');
if (! $user->is_admin) {
abort_if($user->created_at->gt(now()->subHours(72)), 400, 'You need to wait a bit before you can DM another account'); abort_if($user->created_at->gt(now()->subHours(72)), 400, 'You need to wait a bit before you can DM another account');
}
$profile = $user->profile; $profile = $user->profile;
$recipient = Profile::where('id', '!=', $profile->id)->findOrFail($request->input('to_id')); $recipient = Profile::where('id', '!=', $profile->id)->findOrFail($request->input('to_id'));
@ -326,7 +329,6 @@ class DirectMessageController extends Controller
$status = new Status; $status = new Status;
$status->profile_id = $profile->id; $status->profile_id = $profile->id;
$status->caption = $msg; $status->caption = $msg;
$status->rendered = $msg;
$status->visibility = 'direct'; $status->visibility = 'direct';
$status->scope = 'direct'; $status->scope = 'direct';
$status->in_reply_to_profile_id = $recipient->id; $status->in_reply_to_profile_id = $recipient->id;
@ -372,7 +374,7 @@ class DirectMessageController extends Controller
->exists(); ->exists();
if ($recipient->domain == null && $hidden == false && ! $nf) { if ($recipient->domain == null && $hidden == false && ! $nf) {
$notification = new Notification(); $notification = new Notification;
$notification->profile_id = $recipient->id; $notification->profile_id = $recipient->id;
$notification->actor_id = $profile->id; $notification->actor_id = $profile->id;
$notification->action = 'dm'; $notification->action = 'dm';
@ -405,6 +407,8 @@ class DirectMessageController extends Controller
{ {
$this->validate($request, [ $this->validate($request, [
'pid' => 'required', 'pid' => 'required',
'max_id' => 'sometimes|integer',
'min_id' => 'sometimes|integer',
]); ]);
$user = $request->user(); $user = $request->user();
abort_if($user->has_roles && ! UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action'); abort_if($user->has_roles && ! UserRoleService::can('can-direct-message', $user->id), 403, 'Invalid permissions for this action');
@ -419,29 +423,33 @@ class DirectMessageController extends Controller
if ($min_id) { if ($min_id) {
$res = DirectMessage::select('*') $res = DirectMessage::select('*')
->where('id', '>', $min_id) ->where('id', '>', $min_id)
->where(function ($q) use ($pid, $uid) { ->where(function ($query) use ($pid, $uid) {
return $q->where([['from_id', $pid], ['to_id', $uid], $query->where('from_id', $pid)->where('to_id', $uid);
])->orWhere([['from_id', $uid], ['to_id', $pid]]); })->orWhere(function ($query) use ($pid, $uid) {
$query->where('from_id', $uid)->where('to_id', $pid);
}) })
->latest() ->orderBy('id', 'asc')
->take(8) ->take(8)
->get(); ->get()
->reverse();
} elseif ($max_id) { } elseif ($max_id) {
$res = DirectMessage::select('*') $res = DirectMessage::select('*')
->where('id', '<', $max_id) ->where('id', '<', $max_id)
->where(function ($q) use ($pid, $uid) { ->where(function ($query) use ($pid, $uid) {
return $q->where([['from_id', $pid], ['to_id', $uid], $query->where('from_id', $pid)->where('to_id', $uid);
])->orWhere([['from_id', $uid], ['to_id', $pid]]); })->orWhere(function ($query) use ($pid, $uid) {
$query->where('from_id', $uid)->where('to_id', $pid);
}) })
->latest() ->orderBy('id', 'desc')
->take(8) ->take(8)
->get(); ->get();
} else { } else {
$res = DirectMessage::where(function ($q) use ($pid, $uid) { $res = DirectMessage::where(function ($query) use ($pid, $uid) {
return $q->where([['from_id', $pid], ['to_id', $uid], $query->where('from_id', $pid)->where('to_id', $uid);
])->orWhere([['from_id', $uid], ['to_id', $pid]]); })->orWhere(function ($query) use ($pid, $uid) {
$query->where('from_id', $uid)->where('to_id', $pid);
}) })
->latest() ->orderBy('id', 'desc')
->take(8) ->take(8)
->get(); ->get();
} }
@ -630,13 +638,12 @@ class DirectMessageController extends Controller
$status = new Status; $status = new Status;
$status->profile_id = $profile->id; $status->profile_id = $profile->id;
$status->caption = null; $status->caption = null;
$status->rendered = null;
$status->visibility = 'direct'; $status->visibility = 'direct';
$status->scope = 'direct'; $status->scope = 'direct';
$status->in_reply_to_profile_id = $recipient->id; $status->in_reply_to_profile_id = $recipient->id;
$status->save(); $status->save();
$media = new Media(); $media = new Media;
$media->status_id = $status->id; $media->status_id = $status->id;
$media->profile_id = $profile->id; $media->profile_id = $profile->id;
$media->user_id = $user->id; $media->user_id = $user->id;
@ -824,6 +831,11 @@ class DirectMessageController extends Controller
{ {
$profile = $dm->author; $profile = $dm->author;
$url = $dm->recipient->sharedInbox ?? $dm->recipient->inbox_url; $url = $dm->recipient->sharedInbox ?? $dm->recipient->inbox_url;
$status = $dm->status;
if (! $status) {
return;
}
$tags = [ $tags = [
[ [
@ -833,6 +845,8 @@ class DirectMessageController extends Controller
], ],
]; ];
$content = $status->caption ? Autolink::create()->autolink($status->caption) : null;
$body = [ $body = [
'@context' => [ '@context' => [
'https://w3id.org/security/v1', 'https://w3id.org/security/v1',
@ -848,7 +862,7 @@ class DirectMessageController extends Controller
'id' => $dm->status->url(), 'id' => $dm->status->url(),
'type' => 'Note', 'type' => 'Note',
'summary' => null, 'summary' => null,
'content' => $dm->status->rendered ?? $dm->status->caption, 'content' => $content,
'inReplyTo' => null, 'inReplyTo' => null,
'published' => $dm->status->created_at->toAtomString(), 'published' => $dm->status->created_at->toAtomString(),
'url' => $dm->status->url(), 'url' => $dm->status->url(),

View file

@ -2,13 +2,14 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use App\Models\Group; use App\Models\Group;
use App\Models\GroupPost; use App\Models\GroupPost;
use App\Status;
use App\Models\InstanceActor; use App\Models\InstanceActor;
use App\Services\MediaService; use App\Services\MediaService;
use App\Status;
use App\Util\Lexer\Autolink;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class GroupFederationController extends Controller class GroupFederationController extends Controller
{ {
@ -16,6 +17,7 @@ class GroupFederationController extends Controller
{ {
$group = Group::whereLocal(true)->whereActivitypub(true)->findOrFail($id); $group = Group::whereLocal(true)->whereActivitypub(true)->findOrFail($id);
$res = $this->showGroupObject($group); $res = $this->showGroupObject($group);
return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
} }
@ -32,7 +34,7 @@ class GroupFederationController extends Controller
'type' => 'Group', 'type' => 'Group',
'attributedTo' => [ 'attributedTo' => [
'type' => 'Person', 'type' => 'Person',
'id' => $group->admin->permalink() 'id' => $group->admin->permalink(),
], ],
// 'endpoints' => [ // 'endpoints' => [
// 'sharedInbox' => config('app.url') . '/f/inbox' // 'sharedInbox' => config('app.url') . '/f/inbox'
@ -43,23 +45,24 @@ class GroupFederationController extends Controller
'owner' => $group->permalink(), 'owner' => $group->permalink(),
'publicKeyPem' => InstanceActor::first()->public_key, 'publicKeyPem' => InstanceActor::first()->public_key,
], ],
'url' => $group->permalink() 'url' => $group->permalink(),
]; ];
if ($group->metadata && isset($group->metadata['avatar'])) { if ($group->metadata && isset($group->metadata['avatar'])) {
$res['icon'] = [ $res['icon'] = [
'type' => 'Image', 'type' => 'Image',
'url' => $group->metadata['avatar']['url'] 'url' => $group->metadata['avatar']['url'],
]; ];
} }
if ($group->metadata && isset($group->metadata['header'])) { if ($group->metadata && isset($group->metadata['header'])) {
$res['image'] = [ $res['image'] = [
'type' => 'Image', 'type' => 'Image',
'url' => $group->metadata['header']['url'] 'url' => $group->metadata['header']['url'],
]; ];
} }
ksort($res); ksort($res);
return $res; return $res;
}); });
} }
@ -70,7 +73,7 @@ class GroupFederationController extends Controller
$gp = GroupPost::whereGroupId($gid)->findOrFail($sid); $gp = GroupPost::whereGroupId($gid)->findOrFail($sid);
$status = Status::findOrFail($gp->status_id); $status = Status::findOrFail($gp->status_id);
// permission check // permission check
$content = $status->caption ? Autolink::create()->autolink($status->caption) : null;
$res = [ $res = [
'@context' => 'https://www.w3.org/ns/activitystreams', '@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $gp->url(), 'id' => $gp->url(),
@ -78,7 +81,7 @@ class GroupFederationController extends Controller
'type' => 'Note', 'type' => 'Note',
'summary' => null, 'summary' => null,
'content' => $status->rendered ?? $status->caption, 'content' => $content,
'inReplyTo' => null, 'inReplyTo' => null,
'published' => $status->created_at->toAtomString(), 'published' => $status->created_at->toAtomString(),
@ -94,9 +97,10 @@ class GroupFederationController extends Controller
'target' => [ 'target' => [
'type' => 'Collection', 'type' => 'Collection',
'id' => $group->permalink('/wall'), 'id' => $group->permalink('/wall'),
'attributedTo' => $group->permalink() 'attributedTo' => $group->permalink(),
] ],
]; ];
// ksort($res); // ksort($res);
return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
} }

View file

@ -2,48 +2,29 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\Request; use App\AccountInterstitial;
use App\{ use App\Bookmark;
AccountInterstitial, use App\DirectMessage;
Bookmark, use App\DiscoverCategory;
DirectMessage, use App\Follower;
DiscoverCategory,
Hashtag,
Follower,
Like,
Media,
MediaTag,
Notification,
Profile,
StatusHashtag,
Status,
User,
UserFilter,
};
use Auth,Cache;
use Illuminate\Support\Facades\Redis;
use Carbon\Carbon;
use League\Fractal;
use App\Transformer\Api\{
AccountTransformer,
StatusTransformer,
// StatusMediaContainerTransformer,
};
use App\Util\Media\Filter;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Jobs\ModPipeline\HandleSpammerPipeline; use App\Jobs\ModPipeline\HandleSpammerPipeline;
use League\Fractal\Serializer\ArraySerializer; use App\Profile;
use League\Fractal\Pagination\IlluminatePaginatorAdapter; use App\Services\BookmarkService;
use Illuminate\Validation\Rule; use App\Services\DiscoverService;
use Illuminate\Support\Str;
use App\Services\MediaTagService;
use App\Services\ModLogService; use App\Services\ModLogService;
use App\Services\PublicTimelineService; use App\Services\PublicTimelineService;
use App\Services\SnowflakeService;
use App\Services\StatusService; use App\Services\StatusService;
use App\Services\UserFilterService; use App\Services\UserFilterService;
use App\Services\DiscoverService; use App\Status; // StatusMediaContainerTransformer,
use App\Services\BookmarkService; use App\Transformer\Api\StatusTransformer;
use App\User;
use Auth;
use Cache;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redis;
use Illuminate\Validation\Rule;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
class InternalApiController extends Controller class InternalApiController extends Controller
{ {
@ -52,8 +33,8 @@ class InternalApiController extends Controller
public function __construct() public function __construct()
{ {
$this->middleware('auth'); $this->middleware('auth');
$this->fractal = new Fractal\Manager(); $this->fractal = new Fractal\Manager;
$this->fractal->setSerializer(new ArraySerializer()); $this->fractal->setSerializer(new ArraySerializer);
} }
// deprecated v2 compose api // deprecated v2 compose api
@ -63,10 +44,7 @@ class InternalApiController extends Controller
} }
// deprecated // deprecated
public function discover(Request $request) public function discover(Request $request) {}
{
return;
}
public function discoverPosts(Request $request) public function discoverPosts(Request $request)
{ {
@ -84,6 +62,7 @@ class InternalApiController extends Controller
}) })
->take(12) ->take(12)
->values(); ->values();
return response()->json(compact('posts')); return response()->json(compact('posts'));
} }
@ -110,7 +89,7 @@ class InternalApiController extends Controller
public function statusReplies(Request $request, int $id) public function statusReplies(Request $request, int $id)
{ {
$this->validate($request, [ $this->validate($request, [
'limit' => 'nullable|int|min:1|max:6' 'limit' => 'nullable|int|min:1|max:6',
]); ]);
$parent = Status::whereScope('public')->findOrFail($id); $parent = Status::whereScope('public')->findOrFail($id);
$limit = $request->input('limit') ?? 3; $limit = $request->input('limit') ?? 3;
@ -118,16 +97,13 @@ class InternalApiController extends Controller
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')
->take($limit) ->take($limit)
->get(); ->get();
$resource = new Fractal\Resource\Collection($children, new StatusTransformer()); $resource = new Fractal\Resource\Collection($children, new StatusTransformer);
$res = $this->fractal->createData($resource)->toArray(); $res = $this->fractal->createData($resource)->toArray();
return response()->json($res); return response()->json($res);
} }
public function stories(Request $request) public function stories(Request $request) {}
{
}
public function discoverCategories(Request $request) public function discoverCategories(Request $request)
{ {
@ -136,9 +112,10 @@ class InternalApiController extends Controller
return [ return [
'name' => $item->name, 'name' => $item->name,
'url' => $item->url(), 'url' => $item->url(),
'thumb' => $item->thumb() 'thumb' => $item->thumb(),
]; ];
}); });
return response()->json($res); return response()->json($res);
} }
@ -153,15 +130,15 @@ class InternalApiController extends Controller
'addcw', 'addcw',
'remcw', 'remcw',
'unlist', 'unlist',
'spammer' 'spammer',
]) ]),
], ],
'item_id' => 'required|integer|min:1', 'item_id' => 'required|integer|min:1',
'item_type' => [ 'item_type' => [
'required', 'required',
'string', 'string',
Rule::in(['profile', 'status']) Rule::in(['profile', 'status']),
] ],
]); ]);
$action = $request->input('action'); $action = $request->input('action');
@ -184,7 +161,7 @@ class InternalApiController extends Controller
->action('admin.status.moderate') ->action('admin.status.moderate')
->metadata([ ->metadata([
'action' => 'cw', 'action' => 'cw',
'message' => 'Success!' 'message' => 'Success!',
]) ])
->accessLevel('admin') ->accessLevel('admin')
->save(); ->save();
@ -229,7 +206,7 @@ class InternalApiController extends Controller
->action('admin.status.moderate') ->action('admin.status.moderate')
->metadata([ ->metadata([
'action' => 'remove_cw', 'action' => 'remove_cw',
'message' => 'Success!' 'message' => 'Success!',
]) ])
->accessLevel('admin') ->accessLevel('admin')
->save(); ->save();
@ -255,7 +232,7 @@ class InternalApiController extends Controller
->action('admin.status.moderate') ->action('admin.status.moderate')
->metadata([ ->metadata([
'action' => 'unlist', 'action' => 'unlist',
'message' => 'Success!' 'message' => 'Success!',
]) ])
->accessLevel('admin') ->accessLevel('admin')
->save(); ->save();
@ -299,7 +276,7 @@ class InternalApiController extends Controller
->action('admin.status.moderate') ->action('admin.status.moderate')
->metadata([ ->metadata([
'action' => 'spammer', 'action' => 'spammer',
'message' => 'Success!' 'message' => 'Success!',
]) ])
->accessLevel('admin') ->accessLevel('admin')
->save(); ->save();
@ -307,6 +284,7 @@ class InternalApiController extends Controller
} }
StatusService::del($status->id, true); StatusService::del($status->id, true);
return ['msg' => 200]; return ['msg' => 200];
} }
@ -331,6 +309,7 @@ class InternalApiController extends Controller
if ($status) { if ($status) {
BookmarkService::add($pid, $status['id']); BookmarkService::add($pid, $status['id']);
} }
return $status; return $status;
}) })
->filter(function ($bookmark) { ->filter(function ($bookmark) {
@ -350,7 +329,7 @@ class InternalApiController extends Controller
'max_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, 'max_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
'since_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, 'since_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
'min_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX, 'min_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
'limit' => 'nullable|integer|min:1|max:24' 'limit' => 'nullable|integer|min:1|max:24',
]); ]);
$profile = Profile::whereNull('status')->findOrFail($id); $profile = Profile::whereNull('status')->findOrFail($id);
@ -369,17 +348,19 @@ class InternalApiController extends Controller
$pid = Auth::user()->profile->id; $pid = Auth::user()->profile->id;
$following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function () use ($pid) { $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function () use ($pid) {
$following = Follower::whereProfileId($pid)->pluck('following_id'); $following = Follower::whereProfileId($pid)->pluck('following_id');
return $following->push($pid)->toArray(); return $following->push($pid)->toArray();
}); });
$visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : []; $visibility = in_array($profile->id, $following) == true ? ['public', 'unlisted', 'private'] : [];
} else { } else {
if (Auth::check()) { if (Auth::check()) {
$pid = Auth::user()->profile->id; $pid = Auth::user()->profile->id;
$following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function () use ($pid) { $following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function () use ($pid) {
$following = Follower::whereProfileId($pid)->pluck('following_id'); $following = Follower::whereProfileId($pid)->pluck('following_id');
return $following->push($pid)->toArray(); return $following->push($pid)->toArray();
}); });
$visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : ['public', 'unlisted']; $visibility = in_array($profile->id, $following) == true ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
} else { } else {
$visibility = ['public', 'unlisted']; $visibility = ['public', 'unlisted'];
} }
@ -391,7 +372,6 @@ class InternalApiController extends Controller
'id', 'id',
'uri', 'uri',
'caption', 'caption',
'rendered',
'profile_id', 'profile_id',
'type', 'type',
'in_reply_to_id', 'in_reply_to_id',
@ -411,7 +391,7 @@ class InternalApiController extends Controller
->limit($limit) ->limit($limit)
->get(); ->get();
$resource = new Fractal\Resource\Collection($timeline, new StatusTransformer()); $resource = new Fractal\Resource\Collection($timeline, new StatusTransformer);
$res = $this->fractal->createData($resource)->toArray(); $res = $this->fractal->createData($resource)->toArray();
return response()->json($res); return response()->json($res);
@ -431,6 +411,7 @@ class InternalApiController extends Controller
{ {
$pid = $request->user()->profile_id; $pid = $request->user()->profile_id;
$exists = Redis::sismember('email:manual', $pid); $exists = Redis::sismember('email:manual', $pid);
return view('account.email.request_verification', compact('exists')); return view('account.email.request_verification', compact('exists'));
} }
@ -438,6 +419,7 @@ class InternalApiController extends Controller
{ {
$pid = $request->user()->profile_id; $pid = $request->user()->profile_id;
Redis::sadd('email:manual', $pid); Redis::sadd('email:manual', $pid);
return redirect('/i/verify-email')->with(['status' => 'Successfully sent manual verification request!']); return redirect('/i/verify-email')->with(['status' => 'Successfully sent manual verification request!']);
} }
} }

View file

@ -2,12 +2,10 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Status;
use Auth;
use DB;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\{
Profile,
Status,
};
use Auth, DB, Purify;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
class MicroController extends Controller class MicroController extends Controller
@ -23,7 +21,7 @@ class MicroController extends Controller
'type' => [ 'type' => [
'required', 'required',
'string', 'string',
Rule::in(['text']) Rule::in(['text']),
], ],
'title' => 'nullable|string|max:140', 'title' => 'nullable|string|max:140',
'content' => 'required|string|max:500', 'content' => 'required|string|max:500',
@ -34,9 +32,9 @@ class MicroController extends Controller
'public', 'public',
'unlisted', 'unlisted',
'private', 'private',
'draft' 'draft',
]) ]),
] ],
]); ]);
$profile = Auth::user()->profile; $profile = Auth::user()->profile;
$title = $request->input('title'); $title = $request->input('title');
@ -48,7 +46,6 @@ class MicroController extends Controller
$status->type = 'text'; $status->type = 'text';
$status->profile_id = $profile->id; $status->profile_id = $profile->id;
$status->caption = strip_tags($content); $status->caption = strip_tags($content);
$status->rendered = Purify::clean($content);
$status->is_nsfw = false; $status->is_nsfw = false;
// TODO: remove deprecated visibility in favor of scope // TODO: remove deprecated visibility in favor of scope
@ -56,12 +53,14 @@ class MicroController extends Controller
$status->scope = $visibility; $status->scope = $visibility;
$status->entities = json_encode(['title' => $title]); $status->entities = json_encode(['title' => $title]);
$status->save(); $status->save();
return $status; return $status;
}); });
$fractal = new \League\Fractal\Manager(); $fractal = new \League\Fractal\Manager;
$fractal->setSerializer(new \League\Fractal\Serializer\ArraySerializer()); $fractal->setSerializer(new \League\Fractal\Serializer\ArraySerializer);
$s = new \League\Fractal\Resource\Item($status, new \App\Transformer\Api\StatusTransformer()); $s = new \League\Fractal\Resource\Item($status, new \App\Transformer\Api\StatusTransformer);
return $fractal->createData($s)->toArray(); return $fractal->createData($s)->toArray();
} }
} }

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

@ -31,8 +31,8 @@ class PublicApiController extends Controller
public function __construct() public function __construct()
{ {
$this->fractal = new Fractal\Manager(); $this->fractal = new Fractal\Manager;
$this->fractal->setSerializer(new ArraySerializer()); $this->fractal->setSerializer(new ArraySerializer);
} }
protected function getUserData($user) protected function getUserData($user)
@ -74,7 +74,7 @@ class PublicApiController extends Controller
abort_if(! in_array($cached['visibility'], ['public', 'unlisted']), 403); abort_if(! in_array($cached['visibility'], ['public', 'unlisted']), 403);
$res = ['status' => $cached]; $res = ['status' => $cached];
} else { } else {
$item = new Fractal\Resource\Item($status, new StatusStatelessTransformer()); $item = new Fractal\Resource\Item($status, new StatusStatelessTransformer);
$res = [ $res = [
'status' => $this->fractal->createData($item)->toArray(), 'status' => $this->fractal->createData($item)->toArray(),
]; ];
@ -141,7 +141,7 @@ class PublicApiController extends Controller
$replies = $status->comments() $replies = $status->comments()
->whereNull('reblog_of_id') ->whereNull('reblog_of_id')
->whereIn('scope', $scope) ->whereIn('scope', $scope)
->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at') ->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at')
->where('id', '>=', $request->min_id) ->where('id', '>=', $request->min_id)
->orderBy('id', 'desc') ->orderBy('id', 'desc')
->paginate($limit); ->paginate($limit);
@ -150,7 +150,7 @@ class PublicApiController extends Controller
$replies = $status->comments() $replies = $status->comments()
->whereNull('reblog_of_id') ->whereNull('reblog_of_id')
->whereIn('scope', $scope) ->whereIn('scope', $scope)
->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at') ->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at')
->where('id', '<=', $request->max_id) ->where('id', '<=', $request->max_id)
->orderBy('id', 'desc') ->orderBy('id', 'desc')
->paginate($limit); ->paginate($limit);
@ -159,12 +159,12 @@ class PublicApiController extends Controller
$replies = Status::whereInReplyToId($status->id) $replies = Status::whereInReplyToId($status->id)
->whereNull('reblog_of_id') ->whereNull('reblog_of_id')
->whereIn('scope', $scope) ->whereIn('scope', $scope)
->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'rendered', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at') ->select('id', 'caption', 'local', 'visibility', 'scope', 'is_nsfw', 'profile_id', 'in_reply_to_id', 'type', 'reply_count', 'created_at')
->orderBy('id', 'desc') ->orderBy('id', 'desc')
->paginate($limit); ->paginate($limit);
} }
$resource = new Fractal\Resource\Collection($replies, new StatusStatelessTransformer(), 'data'); $resource = new Fractal\Resource\Collection($replies, new StatusStatelessTransformer, 'data');
$resource->setPaginator(new IlluminatePaginatorAdapter($replies)); $resource->setPaginator(new IlluminatePaginatorAdapter($replies));
$res = $this->fractal->createData($resource)->toArray(); $res = $this->fractal->createData($resource)->toArray();
@ -271,7 +271,6 @@ class PublicApiController extends Controller
'id', 'id',
'uri', 'uri',
'caption', 'caption',
'rendered',
'profile_id', 'profile_id',
'type', 'type',
'in_reply_to_id', 'in_reply_to_id',
@ -405,7 +404,6 @@ class PublicApiController extends Controller
'id', 'id',
'uri', 'uri',
'caption', 'caption',
'rendered',
'profile_id', 'profile_id',
'type', 'type',
'in_reply_to_id', 'in_reply_to_id',
@ -456,7 +454,6 @@ class PublicApiController extends Controller
'id', 'id',
'uri', 'uri',
'caption', 'caption',
'rendered',
'profile_id', 'profile_id',
'type', 'type',
'in_reply_to_id', 'in_reply_to_id',

View file

@ -8,6 +8,7 @@ use App\Profile;
use App\Services\WebfingerService; use App\Services\WebfingerService;
use App\Status; use App\Status;
use App\Util\ActivityPub\Helpers; use App\Util\ActivityPub\Helpers;
use App\Util\Lexer\Autolink;
use Auth; use Auth;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
@ -320,17 +321,21 @@ class SearchController extends Controller
if (Status::whereUri($tag)->whereLocal(false)->exists()) { if (Status::whereUri($tag)->whereLocal(false)->exists()) {
$item = Status::whereUri($tag)->first(); $item = Status::whereUri($tag)->first();
if (! $item) {
return;
}
$media = $item->firstMedia(); $media = $item->firstMedia();
$url = null; $url = null;
if ($media) { if ($media) {
$url = $media->remote_url; $url = $media->remote_url;
} }
$content = $item->caption ? Autolink::create()->autolink($item->caption) : null;
$this->tokens['posts'] = [[ $this->tokens['posts'] = [[
'count' => 0, 'count' => 0,
'url' => "/i/web/post/_/$item->profile_id/$item->id", 'url' => "/i/web/post/_/$item->profile_id/$item->id",
'type' => 'status', 'type' => 'status',
'username' => $item->profile->username, 'username' => $item->profile->username,
'caption' => $item->rendered ?? $item->caption, 'caption' => $content,
'thumb' => $url, 'thumb' => $url,
'timestamp' => $item->created_at->diffForHumans(), 'timestamp' => $item->created_at->diffForHumans(),
]]; ]];
@ -340,17 +345,21 @@ class SearchController extends Controller
if (isset($remote['type']) && $remote['type'] == 'Note') { if (isset($remote['type']) && $remote['type'] == 'Note') {
$item = Helpers::statusFetch($tag); $item = Helpers::statusFetch($tag);
if (! $item) {
return;
}
$media = $item->firstMedia(); $media = $item->firstMedia();
$url = null; $url = null;
if ($media) { if ($media) {
$url = $media->remote_url; $url = $media->remote_url;
} }
$content = $item->caption ? Autolink::create()->autolink($item->caption) : null;
$this->tokens['posts'] = [[ $this->tokens['posts'] = [[
'count' => 0, 'count' => 0,
'url' => "/i/web/post/_/$item->profile_id/$item->id", 'url' => "/i/web/post/_/$item->profile_id/$item->id",
'type' => 'status', 'type' => 'status',
'username' => $item->profile->username, 'username' => $item->profile->username,
'caption' => $item->rendered ?? $item->caption, 'caption' => $content,
'thumb' => $url, 'thumb' => $url,
'timestamp' => $item->created_at->diffForHumans(), 'timestamp' => $item->created_at->diffForHumans(),
]]; ]];

View file

@ -2,25 +2,23 @@
namespace App\Http\Controllers\Settings; namespace App\Http\Controllers\Settings;
use App\AccountLog;
use App\Following;
use App\Report;
use App\Status; use App\Status;
use App\UserFilter; use App\Transformer\ActivityPub\ProfileTransformer;
use Auth, Cookie, DB, Cache, Purify;
use Carbon\Carbon;
use Illuminate\Http\Request;
use App\Transformer\ActivityPub\{
ProfileTransformer,
StatusTransformer
};
use App\Transformer\Api\StatusTransformer as StatusApiTransformer; use App\Transformer\Api\StatusTransformer as StatusApiTransformer;
use App\UserFilter;
use Auth;
use Cache;
use Illuminate\Http\Request;
use League\Fractal; use League\Fractal;
use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter; use Storage;
trait ExportSettings trait ExportSettings
{ {
private const CHUNK_SIZE = 1000;
private const STORAGE_BASE = 'user_exports';
public function __construct() public function __construct()
{ {
$this->middleware('auth'); $this->middleware('auth');
@ -33,47 +31,146 @@ trait ExportSettings
public function exportAccount() public function exportAccount()
{ {
$data = Cache::remember('account:export:profile:actor:'.Auth::user()->profile->id, now()->addMinutes(60), function() {
$profile = Auth::user()->profile; $profile = Auth::user()->profile;
$fractal = new Fractal\Manager(); $fractal = new Fractal\Manager;
$fractal->setSerializer(new ArraySerializer()); $fractal->setSerializer(new ArraySerializer);
$resource = new Fractal\Resource\Item($profile, new ProfileTransformer()); $resource = new Fractal\Resource\Item($profile, new ProfileTransformer);
return $fractal->createData($resource)->toArray();
}); $data = $fractal->createData($resource)->toArray();
return response()->streamDownload(function () use ($data) { return response()->streamDownload(function () use ($data) {
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}, 'account.json', [ }, 'account.json', [
'Content-Type' => 'application/json' 'Content-Type' => 'application/json',
]); ]);
} }
public function exportFollowing() public function exportFollowing()
{ {
$data = Cache::remember('account:export:profile:following:'.Auth::user()->profile->id, now()->addMinutes(60), function() { $profile = Auth::user()->profile;
return Auth::user()->profile->following()->get()->map(function($i) { $userId = Auth::id();
return $i->url();
$userExportPath = 'user_exports/'.$userId;
$filename = 'pixelfed-following.json';
$tempPath = $userExportPath.'/'.$filename;
if (! Storage::exists($userExportPath)) {
Storage::makeDirectory($userExportPath);
}
try {
Storage::put($tempPath, '[');
$profile->following()
->chunk(1000, function ($following) use ($tempPath) {
$urls = $following->map(function ($follow) {
return $follow->url();
}); });
$json = json_encode($urls,
JSON_PRETTY_PRINT |
JSON_UNESCAPED_SLASHES |
JSON_UNESCAPED_UNICODE
);
$json = trim($json, '[]');
if (Storage::size($tempPath) > 1) {
$json = ','.$json;
}
Storage::append($tempPath, $json);
}); });
return response()->streamDownload(function () use($data) {
echo $data; Storage::append($tempPath, ']');
}, 'following.json', [
'Content-Type' => 'application/json' return response()->stream(
]); function () use ($tempPath) {
$handle = fopen(Storage::path($tempPath), 'rb');
while (! feof($handle)) {
echo fread($handle, 8192);
flush();
}
fclose($handle);
Storage::delete($tempPath);
},
200,
[
'Content-Type' => 'application/json',
'Content-Disposition' => 'attachment; filename="pixelfed-following.json"',
]
);
} catch (\Exception $e) {
if (Storage::exists($tempPath)) {
Storage::delete($tempPath);
}
throw $e;
}
} }
public function exportFollowers() public function exportFollowers()
{ {
$data = Cache::remember('account:export:profile:followers:'.Auth::user()->profile->id, now()->addMinutes(60), function() { $profile = Auth::user()->profile;
return Auth::user()->profile->followers()->get()->map(function($i) { $userId = Auth::id();
return $i->url();
$userExportPath = 'user_exports/'.$userId;
$filename = 'pixelfed-followers.json';
$tempPath = $userExportPath.'/'.$filename;
if (! Storage::exists($userExportPath)) {
Storage::makeDirectory($userExportPath);
}
try {
Storage::put($tempPath, '[');
$profile->followers()
->chunk(1000, function ($followers) use ($tempPath) {
$urls = $followers->map(function ($follower) {
return $follower->url();
}); });
$json = json_encode($urls,
JSON_PRETTY_PRINT |
JSON_UNESCAPED_SLASHES |
JSON_UNESCAPED_UNICODE
);
$json = trim($json, '[]');
if (Storage::size($tempPath) > 1) {
$json = ','.$json;
}
Storage::append($tempPath, $json);
}); });
return response()->streamDownload(function () use($data) {
echo $data; Storage::append($tempPath, ']');
}, 'followers.json', [
'Content-Type' => 'application/json' return response()->stream(
]); function () use ($tempPath) {
$handle = fopen(Storage::path($tempPath), 'rb');
while (! feof($handle)) {
echo fread($handle, 8192);
flush();
}
fclose($handle);
Storage::delete($tempPath);
},
200,
[
'Content-Type' => 'application/json',
'Content-Disposition' => 'attachment; filename="pixelfed-followers.json"',
]
);
} catch (\Exception $e) {
if (Storage::exists($tempPath)) {
Storage::delete($tempPath);
}
throw $e;
}
} }
public function exportMuteBlockList() public function exportMuteBlockList()
@ -88,57 +185,77 @@ trait ExportSettings
$data = Cache::remember('account:export:profile:muteblocklist:'.Auth::user()->profile->id, now()->addMinutes(60), function () use ($profile) { $data = Cache::remember('account:export:profile:muteblocklist:'.Auth::user()->profile->id, now()->addMinutes(60), function () use ($profile) {
return json_encode([ return json_encode([
'muted' => $profile->mutedProfileUrls(), 'muted' => $profile->mutedProfileUrls(),
'blocked' => $profile->blockedProfileUrls() 'blocked' => $profile->blockedProfileUrls(),
], JSON_PRETTY_PRINT); ], JSON_PRETTY_PRINT);
}); });
return response()->streamDownload(function () use ($data) { return response()->streamDownload(function () use ($data) {
echo $data; echo $data;
}, 'muted-and-blocked-accounts.json', [ }, 'muted-and-blocked-accounts.json', [
'Content-Type' => 'application/json' 'Content-Type' => 'application/json',
]); ]);
} }
public function exportStatuses(Request $request) public function exportStatuses(Request $request)
{ {
$this->validate($request, [
'type' => 'required|string|in:ap,api'
]);
$limit = 500;
$profile = Auth::user()->profile; $profile = Auth::user()->profile;
$type = 'ap'; $userId = Auth::id();
$userExportPath = self::STORAGE_BASE.'/'.$userId;
$filename = 'pixelfed-statuses.json';
$tempPath = $userExportPath.'/'.$filename;
$count = Status::select('id')->whereProfileId($profile->id)->count(); if (! Storage::exists($userExportPath)) {
if($count > $limit) { Storage::makeDirectory($userExportPath);
// fire background job
return redirect('/settings/data-export')->with(['status' => 'You have more than '.$limit.' statuses, we do not support full account export yet.']);
} }
$filename = 'outbox.json'; Storage::put($tempPath, '[');
if($type == 'ap') { $fractal = new Fractal\Manager;
$data = Cache::remember('account:export:profile:statuses:ap:'.Auth::user()->profile->id, now()->addHours(1), function() { $fractal->setSerializer(new ArraySerializer);
$profile = Auth::user()->profile->statuses;
$fractal = new Fractal\Manager(); try {
$fractal->setSerializer(new ArraySerializer()); Status::whereProfileId($profile->id)
$resource = new Fractal\Resource\Collection($profile, new StatusTransformer()); ->chunk(self::CHUNK_SIZE, function ($statuses) use ($fractal, $tempPath) {
return $fractal->createData($resource)->toArray(); $resource = new Fractal\Resource\Collection($statuses, new StatusApiTransformer);
$data = $fractal->createData($resource)->toArray();
$json = json_encode($data,
JSON_PRETTY_PRINT |
JSON_UNESCAPED_SLASHES |
JSON_UNESCAPED_UNICODE
);
$json = trim($json, '[]');
if (Storage::size($tempPath) > 1) {
$json = ','.$json;
}
Storage::append($tempPath, $json);
}); });
} else {
$filename = 'api-statuses.json';
$data = Cache::remember('account:export:profile:statuses:api:'.Auth::user()->profile->id, now()->addHours(1), function() {
$profile = Auth::user()->profile->statuses;
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Collection($profile, new StatusApiTransformer());
return $fractal->createData($resource)->toArray();
});
}
return response()->streamDownload(function () use ($data, $filename) { Storage::append($tempPath, ']');
echo json_encode($data, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);
}, $filename, [
'Content-Type' => 'application/json'
]);
}
return response()->stream(
function () use ($tempPath) {
$handle = fopen(Storage::path($tempPath), 'rb');
while (! feof($handle)) {
echo fread($handle, 8192);
flush();
}
fclose($handle);
Storage::delete($tempPath);
},
200,
[
'Content-Type' => 'application/json',
'Content-Disposition' => 'attachment; filename="pixelfed-statuses.json"',
]
);
} catch (\Exception $e) {
if (Storage::exists($tempPath)) {
Storage::delete($tempPath);
}
throw $e;
}
}
} }

View file

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

View file

@ -281,7 +281,7 @@ class StoryApiV1Controller extends Controller
$photo = $request->file('file'); $photo = $request->file('file');
$path = $this->storeMedia($photo, $user); $path = $this->storeMedia($photo, $user);
$story = new Story(); $story = new Story;
$story->duration = $request->input('duration', 3); $story->duration = $request->input('duration', 3);
$story->profile_id = $user->profile_id; $story->profile_id = $user->profile_id;
$story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' : 'photo'; $story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' : 'photo';
@ -418,7 +418,6 @@ class StoryApiV1Controller extends Controller
$status->type = 'story:reply'; $status->type = 'story:reply';
$status->profile_id = $pid; $status->profile_id = $pid;
$status->caption = $text; $status->caption = $text;
$status->rendered = $text;
$status->scope = 'direct'; $status->scope = 'direct';
$status->visibility = 'direct'; $status->visibility = 'direct';
$status->in_reply_to_profile_id = $story->profile_id; $status->in_reply_to_profile_id = $story->profile_id;

View file

@ -54,7 +54,7 @@ class StoryComposeController extends Controller
$photo = $request->file('file'); $photo = $request->file('file');
$path = $this->storePhoto($photo, $user); $path = $this->storePhoto($photo, $user);
$story = new Story(); $story = new Story;
$story->duration = 3; $story->duration = 3;
$story->profile_id = $user->profile_id; $story->profile_id = $user->profile_id;
$story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' : 'photo'; $story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' : 'photo';
@ -403,7 +403,6 @@ class StoryComposeController extends Controller
$status->profile_id = $pid; $status->profile_id = $pid;
$status->type = 'story:reaction'; $status->type = 'story:reaction';
$status->caption = $text; $status->caption = $text;
$status->rendered = $text;
$status->scope = 'direct'; $status->scope = 'direct';
$status->visibility = 'direct'; $status->visibility = 'direct';
$status->in_reply_to_profile_id = $story->profile_id; $status->in_reply_to_profile_id = $story->profile_id;
@ -477,7 +476,6 @@ class StoryComposeController extends Controller
$status->type = 'story:reply'; $status->type = 'story:reply';
$status->profile_id = $pid; $status->profile_id = $pid;
$status->caption = $text; $status->caption = $text;
$status->rendered = $text;
$status->scope = 'direct'; $status->scope = 'direct';
$status->visibility = 'direct'; $status->visibility = 'direct';
$status->in_reply_to_profile_id = $story->profile_id; $status->in_reply_to_profile_id = $story->profile_id;

View file

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

View file

@ -2,18 +2,17 @@
namespace App\Http\Resources; namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource; use App\Models\CustomEmoji;
use Cache;
use App\Services\AccountService; use App\Services\AccountService;
use App\Services\HashidService; use App\Services\HashidService;
use App\Services\LikeService; use App\Services\LikeService;
use App\Services\MediaService; use App\Services\MediaService;
use App\Services\MediaTagService; use App\Services\MediaTagService;
use App\Services\StatusHashtagService;
use App\Services\StatusLabelService;
use App\Services\StatusMentionService;
use App\Services\PollService; use App\Services\PollService;
use App\Models\CustomEmoji; use App\Services\StatusHashtagService;
use App\Services\StatusMentionService;
use App\Util\Lexer\Autolink;
use Illuminate\Http\Resources\Json\JsonResource;
class StatusStateless extends JsonResource class StatusStateless extends JsonResource
{ {
@ -28,6 +27,7 @@ class StatusStateless extends JsonResource
$status = $this; $status = $this;
$taggedPeople = MediaTagService::get($status->id); $taggedPeople = MediaTagService::get($status->id);
$poll = $status->type === 'poll' ? PollService::get($status->id) : null; $poll = $status->type === 'poll' ? PollService::get($status->id) : null;
$autoLink = $status->caption ? Autolink::create()->autolink($status->caption) : null;
return [ return [
'_v' => 1, '_v' => 1,
@ -39,7 +39,7 @@ class StatusStateless extends JsonResource
'in_reply_to_id' => $status->in_reply_to_id ? (string) $status->in_reply_to_id : null, 'in_reply_to_id' => $status->in_reply_to_id ? (string) $status->in_reply_to_id : null,
'in_reply_to_account_id' => $status->in_reply_to_profile_id ? (string) $status->in_reply_to_profile_id : null, 'in_reply_to_account_id' => $status->in_reply_to_profile_id ? (string) $status->in_reply_to_profile_id : null,
'reblog' => null, 'reblog' => null,
'content' => $status->rendered ?? $status->caption, 'content' => $autoLink,
'content_text' => $status->caption, 'content_text' => $status->caption,
'created_at' => str_replace('+00:00', 'Z', $status->created_at->format(DATE_RFC3339_EXTENDED)), 'created_at' => str_replace('+00:00', 'Z', $status->created_at->format(DATE_RFC3339_EXTENDED)),
'emojis' => CustomEmoji::scan($status->caption), 'emojis' => CustomEmoji::scan($status->caption),
@ -53,7 +53,7 @@ class StatusStateless extends JsonResource
'visibility' => $status->scope ?? $status->visibility, 'visibility' => $status->scope ?? $status->visibility,
'application' => [ 'application' => [
'name' => 'web', 'name' => 'web',
'website' => null 'website' => null,
], ],
'language' => null, 'language' => null,
'mentions' => StatusMentionService::get($status->id), 'mentions' => StatusMentionService::get($status->id),
@ -70,7 +70,7 @@ class StatusStateless extends JsonResource
'media_attachments' => MediaService::get($status->id), 'media_attachments' => MediaService::get($status->id),
'account' => AccountService::get($status->profile_id, true), 'account' => AccountService::get($status->profile_id, true),
'tags' => StatusHashtagService::statusTags($status->id), 'tags' => StatusHashtagService::statusTags($status->id),
'poll' => $poll 'poll' => $poll,
]; ];
} }
} }

View file

@ -2,35 +2,32 @@
namespace App\Jobs\GroupPipeline; namespace App\Jobs\GroupPipeline;
use App\Notification;
use App\Hashtag; use App\Hashtag;
use App\Mention; use App\Mention;
use App\Profile;
use App\Status;
use App\StatusHashtag;
use App\Models\GroupPostHashtag;
use App\Models\GroupPost; use App\Models\GroupPost;
use Cache; use App\Models\GroupPostHashtag;
use App\Profile;
use App\Services\StatusService;
use App\Status;
use App\Util\Lexer\Autolink;
use App\Util\Lexer\Extractor;
use DB; use DB;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
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;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Redis;
use App\Services\MediaStorageService;
use App\Services\NotificationService;
use App\Services\StatusService;
use App\Util\Lexer\Autolink;
use App\Util\Lexer\Extractor;
class NewStatusPipeline implements ShouldQueue class NewStatusPipeline implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status; protected $status;
protected $gp; protected $gp;
protected $tags; protected $tags;
protected $mentions; protected $mentions;
public function __construct(Status $status, GroupPost $gp) public function __construct(Status $status, GroupPost $gp)
@ -50,10 +47,6 @@ class NewStatusPipeline implements ShouldQueue
->autolink($status->caption); ->autolink($status->caption);
$entities = Extractor::create()->extract($status->caption); $entities = Extractor::create()->extract($status->caption);
$autolink = str_replace('/discover/tags/', '/groups/' . $status->group_id . '/topics/', $autolink);
$status->rendered = nl2br($autolink);
$status->entities = null; $status->entities = null;
$status->save(); $status->save();
@ -117,7 +110,7 @@ class NewStatusPipeline implements ShouldQueue
} }
DB::transaction(function () use ($status, $mentioned) { DB::transaction(function () use ($status, $mentioned) {
$m = new Mention(); $m = new Mention;
$m->status_id = $status->id; $m->status_id = $status->id;
$m->profile_id = $mentioned->id; $m->profile_id = $mentioned->id;
$m->save(); $m->save();

View file

@ -91,11 +91,6 @@ class StatusEntityLexer implements ShouldQueue
public function storeEntities() public function storeEntities()
{ {
$this->storeHashtags(); $this->storeHashtags();
DB::transaction(function () {
$status = $this->status;
$status->rendered = nl2br($this->autolink);
$status->save();
});
} }
public function storeHashtags() public function storeHashtags()
@ -146,7 +141,7 @@ class StatusEntityLexer implements ShouldQueue
} }
DB::transaction(function () use ($status, $mentioned) { DB::transaction(function () use ($status, $mentioned) {
$m = new Mention(); $m = new Mention;
$m->status_id = $status->id; $m->status_id = $status->id;
$m->profile_id = $mentioned->id; $m->profile_id = $mentioned->id;
$m->save(); $m->save();

View file

@ -120,8 +120,7 @@ class StatusRemoteUpdatePipeline implements ShouldQueue
protected function updateImmediateAttributes($status, $activity) protected function updateImmediateAttributes($status, $activity)
{ {
if (isset($activity['content'])) { if (isset($activity['content'])) {
$status->caption = strip_tags($activity['content']); $status->caption = strip_tags(Purify::clean($activity['content']));
$status->rendered = Purify::clean($activity['content']);
} }
if (isset($activity['sensitive'])) { if (isset($activity['sensitive'])) {

View file

@ -2,27 +2,28 @@
namespace App\Jobs\StatusPipeline; 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\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
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;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use App\Services\AccountService; use Illuminate\Support\Facades\DB;
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;
class StatusTagsPipeline implements ShouldQueue class StatusTagsPipeline implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $activity; protected $activity;
protected $status; protected $status;
/** /**
@ -77,28 +78,62 @@ class StatusTagsPipeline implements ShouldQueue
} }
if (config('database.default') === 'pgsql') { if (config('database.default') === 'pgsql') {
$hashtag = Hashtag::where('name', 'ilike', $name) $hashtag = DB::transaction(function () use ($name) {
->orWhere('slug', 'ilike', str_slug($name, '-', false)) $baseSlug = str_slug($name, '-', false);
$slug = $baseSlug;
$counter = 1;
$existing = Hashtag::where('name', $name)
->lockForUpdate()
->first(); ->first();
if(!$hashtag) { if ($existing) {
$hashtag = Hashtag::updateOrCreate([ if ($existing->slug !== $slug) {
'slug' => str_slug($name, '-', false), while (Hashtag::where('slug', $slug)
'name' => $name ->where('name', '!=', $name)
]); ->exists()) {
$slug = $baseSlug.'-'.$counter++;
} }
} else { $existing->slug = $slug;
$hashtag = Hashtag::updateOrCreate([ $existing->save();
'slug' => str_slug($name, '-', false), }
'name' => $name
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]
);
});
} }
StatusHashtag::firstOrCreate([ StatusHashtag::firstOrCreate([
'status_id' => $status->id, 'status_id' => $status->id,
'hashtag_id' => $hashtag->id, 'hashtag_id' => $hashtag->id,
'profile_id' => $status->profile_id, 'profile_id' => $status->profile_id,
'status_visibility' => $status->scope 'status_visibility' => $status->scope,
]); ]);
}); });

View file

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

View file

@ -2,39 +2,39 @@
namespace App\Providers; namespace App\Providers;
use App\Observers\{ use App\Avatar;
AvatarObserver, use App\Follower;
FollowerObserver, use App\HashtagFollow;
HashtagFollowObserver, use App\Like;
LikeObserver, use App\ModLog;
NotificationObserver, use App\Notification;
ModLogObserver, use App\Observers\AvatarObserver;
ProfileObserver, use App\Observers\FollowerObserver;
StatusHashtagObserver, use App\Observers\HashtagFollowObserver;
StatusObserver, use App\Observers\LikeObserver;
UserObserver, use App\Observers\ModLogObserver;
UserFilterObserver, use App\Observers\NotificationObserver;
}; use App\Observers\ProfileObserver;
use App\{ use App\Observers\StatusHashtagObserver;
Avatar, use App\Observers\StatusObserver;
Follower, use App\Observers\UserFilterObserver;
HashtagFollow, use App\Observers\UserObserver;
Like, use App\Profile;
Notification, use App\Services\AccountService;
ModLog, use App\Status;
Profile, use App\StatusHashtag;
StatusHashtag, use App\User;
Status, use App\UserFilter;
User, use Auth;
UserFilter use Horizon;
};
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 Illuminate\Database\Eloquent\Model; 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 class AppServiceProvider extends ServiceProvider
{ {
@ -67,6 +67,24 @@ class AppServiceProvider extends ServiceProvider
}); });
Validator::includeUnvalidatedArrayKeys(); Validator::includeUnvalidatedArrayKeys();
Gate::define('viewPulse', function (User $user) {
return $user->is_admin === 1;
});
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); // Model::preventLazyLoading(true);
} }

View file

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

View file

@ -2,53 +2,25 @@
namespace App\Services; namespace App\Services;
use Cache;
use App\Profile; use App\Profile;
use Illuminate\Support\Str; use Cache;
use Illuminate\Support\Facades\Http; use Purify;
use App\Util\Webfinger\WebfingerUrl;
class AutolinkService class AutolinkService
{ {
const CACHE_KEY = 'pf:services:autolink:'; const CACHE_KEY = 'pf:services:autolink:mue:';
public static function mentionedUsernameExists($username) public static function mentionedUsernameExists($username)
{ {
$key = 'pf:services:autolink:userexists:' . hash('sha256', $username); if (str_starts_with($username, '@')) {
if (substr_count($username, '@') === 1) {
$username = substr($username, 1);
}
}
$name = Purify::clean(strtolower($username));
return Cache::remember($key, 3600, function() use($username) { return Cache::remember(self::CACHE_KEY.base64_encode($name), 7200, function () use ($name) {
$remote = Str::of($username)->contains('@'); return Profile::where('username', $name)->exists();
$profile = Profile::whereUsername($username)->first();
if($profile) {
if($profile->domain != null) {
$instance = InstanceService::getByDomain($profile->domain);
if($instance && $instance->banned == true) {
return false;
}
}
return true;
} else {
if($remote) {
$parts = explode('@', $username);
$domain = last($parts);
$instance = InstanceService::getByDomain($domain);
if($instance) {
if($instance->banned == true) {
return false;
} else {
$wf = WebfingerUrl::generateWebfingerUrl($username);
$res = Http::head($wf);
return $res->ok();
}
} else {
$wf = WebfingerUrl::generateWebfingerUrl($username);
$res = Http::head($wf);
return $res->ok();
}
}
}
return false;
}); });
} }
} }

View file

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

View file

@ -3,14 +3,13 @@
namespace App\Services\Status; namespace App\Services\Status;
use App\Media; use App\Media;
use App\ModLog;
use App\Status;
use App\Models\StatusEdit; use App\Models\StatusEdit;
use Purify; use App\ModLog;
use App\Util\Lexer\Autolink;
use App\Services\MediaService; use App\Services\MediaService;
use App\Services\MediaStorageService; use App\Services\MediaStorageService;
use App\Services\StatusService; use App\Services\StatusService;
use App\Status;
use Purify;
class UpdateStatusService class UpdateStatusService
{ {
@ -31,7 +30,9 @@ class UpdateStatusService
return; return;
} }
$oids = $status->media()->orderBy('order')->pluck('id')->map(function($m) { return (string) $m; }); $oids = $status->media()->orderBy('order')->pluck('id')->map(function ($m) {
return (string) $m;
});
$nids = collect($attributes['media_ids']); $nids = collect($attributes['media_ids']);
if ($oids->toArray() === $nids->toArray()) { if ($oids->toArray() === $nids->toArray()) {
@ -64,15 +65,12 @@ class UpdateStatusService
if (isset($attributes['status'])) { if (isset($attributes['status'])) {
$cleaned = Purify::clean($attributes['status']); $cleaned = Purify::clean($attributes['status']);
$status->caption = $cleaned; $status->caption = $cleaned;
$status->rendered = nl2br(Autolink::create()->autolink($cleaned));
} else { } else {
$status->caption = null; $status->caption = null;
$status->rendered = null;
} }
if (isset($attributes['sensitive'])) { if (isset($attributes['sensitive'])) {
if ($status->is_nsfw != (bool) $attributes['sensitive'] && if ($status->is_nsfw != (bool) $attributes['sensitive'] &&
(bool) $attributes['sensitive'] == false) (bool) $attributes['sensitive'] == false) {
{
$exists = ModLog::whereObjectType('App\Status::class') $exists = ModLog::whereObjectType('App\Status::class')
->whereObjectId($status->id) ->whereObjectId($status->id)
->whereAction('admin.status.moderate') ->whereAction('admin.status.moderate')
@ -114,7 +112,7 @@ class UpdateStatusService
'spoiler_text' => $status->cw_summary, 'spoiler_text' => $status->cw_summary,
'is_nsfw' => $status->is_nsfw, 'is_nsfw' => $status->is_nsfw,
'ordered_media_attachment_ids' => $status->media()->orderBy('order')->pluck('id')->toArray(), 'ordered_media_attachment_ids' => $status->media()->orderBy('order')->pluck('id')->toArray(),
'created_at' => $status->created_at 'created_at' => $status->created_at,
]); ]);
} }
} }
@ -131,7 +129,7 @@ class UpdateStatusService
'caption' => $cleaned, 'caption' => $cleaned,
'spoiler_text' => $spoiler_text, 'spoiler_text' => $spoiler_text,
'is_nsfw' => $sensitive, 'is_nsfw' => $sensitive,
'ordered_media_attachment_ids' => $mids 'ordered_media_attachment_ids' => $mids,
]); ]);
} }
} }

View file

@ -10,7 +10,7 @@ use League\Fractal\Serializer\ArraySerializer;
class StatusService class StatusService
{ {
const CACHE_KEY = 'pf:services:status:'; const CACHE_KEY = 'pf:services:status:v1.1:';
public static function key($id, $publicOnly = true) public static function key($id, $publicOnly = true)
{ {

View file

@ -308,46 +308,6 @@ class Status extends Model
return $this->comments()->orderBy('created_at', 'desc')->take(3); return $this->comments()->orderBy('created_at', 'desc')->take(3);
} }
public function toActivityPubObject()
{
if($this->local == false) {
return;
}
$profile = $this->profile;
$to = $this->scopeToAudience('to');
$cc = $this->scopeToAudience('cc');
return [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $this->permalink(),
'type' => 'Create',
'actor' => $profile->permalink(),
'published' => str_replace('+00:00', 'Z', $this->created_at->format(DATE_RFC3339_EXTENDED)),
'to' => $to,
'cc' => $cc,
'object' => [
'id' => $this->url(),
'type' => 'Note',
'summary' => null,
'inReplyTo' => null,
'published' => str_replace('+00:00', 'Z', $this->created_at->format(DATE_RFC3339_EXTENDED)),
'url' => $this->url(),
'attributedTo' => $this->profile->url(),
'to' => $to,
'cc' => $cc,
'sensitive' => (bool) $this->is_nsfw,
'content' => $this->rendered,
'attachment' => $this->media->map(function($media) {
return [
'type' => 'Document',
'mediaType' => $media->mime,
'url' => $media->url(),
'name' => null
];
})->toArray()
]
];
}
public function scopeToAudience($audience) public function scopeToAudience($audience)
{ {
if(!in_array($audience, ['to', 'cc']) || $this->local == false) { if(!in_array($audience, ['to', 'cc']) || $this->local == false) {

View file

@ -2,14 +2,27 @@
namespace App\Transformer\ActivityPub; namespace App\Transformer\ActivityPub;
use App\Status;
use League\Fractal;
use App\Services\MediaService; use App\Services\MediaService;
use App\Services\StatusService;
use App\Status;
use App\Util\Lexer\Autolink;
use League\Fractal;
class StatusTransformer extends Fractal\TransformerAbstract class StatusTransformer extends Fractal\TransformerAbstract
{ {
public function transform(Status $status) public function transform(Status $status)
{ {
$content = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : '';
$inReplyTo = null;
if ($status->in_reply_to_id) {
$reply = StatusService::get($status->in_reply_to_id, true);
if ($reply && isset($reply['url'])) {
$inReplyTo = $reply['url'];
}
}
return [ return [
'@context' => [ '@context' => [
'https://www.w3.org/ns/activitystreams', 'https://www.w3.org/ns/activitystreams',
@ -22,30 +35,20 @@ class StatusTransformer extends Fractal\TransformerAbstract
], ],
], ],
'id' => $status->url(), 'id' => $status->url(),
// TODO: handle other types
'type' => 'Note', 'type' => 'Note',
// XXX: CW Title
'summary' => null, 'summary' => null,
'content' => $status->rendered ?? $status->caption, 'content' => $content,
'inReplyTo' => null, 'inReplyTo' => $inReplyTo,
// TODO: fix date format
'published' => $status->created_at->toAtomString(), 'published' => $status->created_at->toAtomString(),
'url' => $status->url(), 'url' => $status->url(),
'attributedTo' => $status->profile->permalink(), 'attributedTo' => $status->profile->permalink(),
'to' => [ 'to' => [
// TODO: handle proper scope
'https://www.w3.org/ns/activitystreams#Public', 'https://www.w3.org/ns/activitystreams#Public',
], ],
'cc' => [ 'cc' => [
// TODO: add cc's
$status->profile->permalink('/followers'), $status->profile->permalink('/followers'),
], ],
'sensitive' => (bool) $status->is_nsfw, 'sensitive' => (bool) $status->is_nsfw,
'atomUri' => $status->url(),
'inReplyToAtomUri' => null,
'attachment' => MediaService::activitypub($status->id), 'attachment' => MediaService::activitypub($status->id),
'tag' => [], 'tag' => [],
'location' => $status->place_id ? [ 'location' => $status->place_id ? [
@ -53,7 +56,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
'name' => $status->place->name, 'name' => $status->place->name,
'longitude' => $status->place->long, 'longitude' => $status->place->long,
'latitude' => $status->place->lat, 'latitude' => $status->place->lat,
'country' => $status->place->country 'country' => $status->place->country,
] : null, ] : null,
]; ];
} }

View file

@ -2,10 +2,11 @@
namespace App\Transformer\ActivityPub\Verb; namespace App\Transformer\ActivityPub\Verb;
use App\Status;
use League\Fractal;
use App\Models\CustomEmoji; use App\Models\CustomEmoji;
use App\Status;
use App\Util\Lexer\Autolink;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use League\Fractal;
class CreateNote extends Fractal\TransformerAbstract class CreateNote extends Fractal\TransformerAbstract
{ {
@ -16,10 +17,11 @@ class CreateNote extends Fractal\TransformerAbstract
$name = Str::startsWith($webfinger, '@') ? $name = Str::startsWith($webfinger, '@') ?
$webfinger : $webfinger :
'@'.$webfinger; '@'.$webfinger;
return [ return [
'type' => 'Mention', 'type' => 'Mention',
'href' => $mention->permalink(), 'href' => $mention->permalink(),
'name' => $name 'name' => $name,
]; ];
})->toArray(); })->toArray();
@ -33,7 +35,7 @@ class CreateNote extends Fractal\TransformerAbstract
$reply = [ $reply = [
'type' => 'Mention', 'type' => 'Mention',
'href' => $parent->permalink(), 'href' => $parent->permalink(),
'name' => $name 'name' => $name,
]; ];
$mentions = array_merge($reply, $mentions); $mentions = array_merge($reply, $mentions);
} }
@ -50,6 +52,7 @@ class CreateNote extends Fractal\TransformerAbstract
$emojis = CustomEmoji::scan($status->caption, true) ?? []; $emojis = CustomEmoji::scan($status->caption, true) ?? [];
$emoji = array_merge($emojis, $mentions); $emoji = array_merge($emojis, $mentions);
$tags = array_merge($emoji, $hashtags); $tags = array_merge($emoji, $hashtags);
$content = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : "";
return [ return [
'@context' => [ '@context' => [
@ -62,28 +65,28 @@ class CreateNote extends Fractal\TransformerAbstract
'pixelfed' => 'http://pixelfed.org/ns#', 'pixelfed' => 'http://pixelfed.org/ns#',
'commentsEnabled' => [ 'commentsEnabled' => [
'@id' => 'pixelfed:commentsEnabled', '@id' => 'pixelfed:commentsEnabled',
'@type' => 'schema:Boolean' '@type' => 'schema:Boolean',
], ],
'capabilities' => [ 'capabilities' => [
'@id' => 'pixelfed:capabilities', '@id' => 'pixelfed:capabilities',
'@container' => '@set' '@container' => '@set',
], ],
'announce' => [ 'announce' => [
'@id' => 'pixelfed:canAnnounce', '@id' => 'pixelfed:canAnnounce',
'@type' => '@id' '@type' => '@id',
], ],
'like' => [ 'like' => [
'@id' => 'pixelfed:canLike', '@id' => 'pixelfed:canLike',
'@type' => '@id' '@type' => '@id',
], ],
'reply' => [ 'reply' => [
'@id' => 'pixelfed:canReply', '@id' => 'pixelfed:canReply',
'@type' => '@id' '@type' => '@id',
], ],
'toot' => 'http://joinmastodon.org/ns#', 'toot' => 'http://joinmastodon.org/ns#',
'Emoji' => 'toot:Emoji', 'Emoji' => 'toot:Emoji',
'blurhash' => 'toot:blurhash', 'blurhash' => 'toot:blurhash',
] ],
], ],
'id' => $status->permalink(), 'id' => $status->permalink(),
'type' => 'Create', 'type' => 'Create',
@ -95,7 +98,7 @@ class CreateNote extends Fractal\TransformerAbstract
'id' => $status->url(), 'id' => $status->url(),
'type' => 'Note', 'type' => 'Note',
'summary' => $status->is_nsfw ? $status->cw_summary : null, 'summary' => $status->is_nsfw ? $status->cw_summary : null,
'content' => $status->rendered ?? $status->caption, 'content' => $content,
'inReplyTo' => $status->in_reply_to_id ? $status->parent()->url() : null, 'inReplyTo' => $status->in_reply_to_id ? $status->parent()->url() : null,
'published' => $status->created_at->toAtomString(), 'published' => $status->created_at->toAtomString(),
'url' => $status->url(), 'url' => $status->url(),
@ -119,6 +122,7 @@ class CreateNote extends Fractal\TransformerAbstract
if ($media->height) { if ($media->height) {
$res['height'] = $media->height; $res['height'] = $media->height;
} }
return $res; return $res;
})->toArray(), })->toArray(),
'tag' => $tags, 'tag' => $tags,
@ -126,16 +130,16 @@ class CreateNote extends Fractal\TransformerAbstract
'capabilities' => [ 'capabilities' => [
'announce' => 'https://www.w3.org/ns/activitystreams#Public', 'announce' => 'https://www.w3.org/ns/activitystreams#Public',
'like' => 'https://www.w3.org/ns/activitystreams#Public', 'like' => 'https://www.w3.org/ns/activitystreams#Public',
'reply' => $status->comments_disabled == true ? '[]' : 'https://www.w3.org/ns/activitystreams#Public' 'reply' => $status->comments_disabled == true ? '[]' : 'https://www.w3.org/ns/activitystreams#Public',
], ],
'location' => $status->place_id ? [ 'location' => $status->place_id ? [
'type' => 'Place', 'type' => 'Place',
'name' => $status->place->name, 'name' => $status->place->name,
'longitude' => $status->place->long, 'longitude' => $status->place->long,
'latitude' => $status->place->lat, 'latitude' => $status->place->lat,
'country' => $status->place->country 'country' => $status->place->country,
] : null, ] : null,
] ],
]; ];
} }
} }

View file

@ -2,10 +2,11 @@
namespace App\Transformer\ActivityPub\Verb; namespace App\Transformer\ActivityPub\Verb;
use App\Status;
use League\Fractal;
use App\Models\CustomEmoji; use App\Models\CustomEmoji;
use App\Status;
use App\Util\Lexer\Autolink;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use League\Fractal;
class Note extends Fractal\TransformerAbstract class Note extends Fractal\TransformerAbstract
{ {
@ -17,10 +18,11 @@ class Note extends Fractal\TransformerAbstract
$name = Str::startsWith($webfinger, '@') ? $name = Str::startsWith($webfinger, '@') ?
$webfinger : $webfinger :
'@'.$webfinger; '@'.$webfinger;
return [ return [
'type' => 'Mention', 'type' => 'Mention',
'href' => $mention->permalink(), 'href' => $mention->permalink(),
'name' => $name 'name' => $name,
]; ];
})->toArray(); })->toArray();
@ -34,7 +36,7 @@ class Note extends Fractal\TransformerAbstract
$reply = [ $reply = [
'type' => 'Mention', 'type' => 'Mention',
'href' => $parent->permalink(), 'href' => $parent->permalink(),
'name' => $name 'name' => $name,
]; ];
array_push($mentions, $reply); array_push($mentions, $reply);
} }
@ -51,6 +53,7 @@ class Note extends Fractal\TransformerAbstract
$emojis = CustomEmoji::scan($status->caption, true) ?? []; $emojis = CustomEmoji::scan($status->caption, true) ?? [];
$emoji = array_merge($emojis, $mentions); $emoji = array_merge($emojis, $mentions);
$tags = array_merge($emoji, $hashtags); $tags = array_merge($emoji, $hashtags);
$content = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : "";
return [ return [
'@context' => [ '@context' => [
@ -63,33 +66,33 @@ class Note extends Fractal\TransformerAbstract
'pixelfed' => 'http://pixelfed.org/ns#', 'pixelfed' => 'http://pixelfed.org/ns#',
'commentsEnabled' => [ 'commentsEnabled' => [
'@id' => 'pixelfed:commentsEnabled', '@id' => 'pixelfed:commentsEnabled',
'@type' => 'schema:Boolean' '@type' => 'schema:Boolean',
], ],
'capabilities' => [ 'capabilities' => [
'@id' => 'pixelfed:capabilities', '@id' => 'pixelfed:capabilities',
'@container' => '@set' '@container' => '@set',
], ],
'announce' => [ 'announce' => [
'@id' => 'pixelfed:canAnnounce', '@id' => 'pixelfed:canAnnounce',
'@type' => '@id' '@type' => '@id',
], ],
'like' => [ 'like' => [
'@id' => 'pixelfed:canLike', '@id' => 'pixelfed:canLike',
'@type' => '@id' '@type' => '@id',
], ],
'reply' => [ 'reply' => [
'@id' => 'pixelfed:canReply', '@id' => 'pixelfed:canReply',
'@type' => '@id' '@type' => '@id',
], ],
'toot' => 'http://joinmastodon.org/ns#', 'toot' => 'http://joinmastodon.org/ns#',
'Emoji' => 'toot:Emoji', 'Emoji' => 'toot:Emoji',
'blurhash' => 'toot:blurhash', 'blurhash' => 'toot:blurhash',
] ],
], ],
'id' => $status->url(), 'id' => $status->url(),
'type' => 'Note', 'type' => 'Note',
'summary' => $status->is_nsfw ? $status->cw_summary : null, 'summary' => $status->is_nsfw ? $status->cw_summary : null,
'content' => $status->rendered ?? $status->caption, 'content' => $content,
'inReplyTo' => $status->in_reply_to_id ? $status->parent()->url() : null, 'inReplyTo' => $status->in_reply_to_id ? $status->parent()->url() : null,
'published' => $status->created_at->toAtomString(), 'published' => $status->created_at->toAtomString(),
'url' => $status->url(), 'url' => $status->url(),
@ -113,6 +116,7 @@ class Note extends Fractal\TransformerAbstract
if ($media->height) { if ($media->height) {
$res['height'] = $media->height; $res['height'] = $media->height;
} }
return $res; return $res;
})->toArray(), })->toArray(),
'tag' => $tags, 'tag' => $tags,
@ -120,14 +124,14 @@ class Note extends Fractal\TransformerAbstract
'capabilities' => [ 'capabilities' => [
'announce' => 'https://www.w3.org/ns/activitystreams#Public', 'announce' => 'https://www.w3.org/ns/activitystreams#Public',
'like' => 'https://www.w3.org/ns/activitystreams#Public', 'like' => 'https://www.w3.org/ns/activitystreams#Public',
'reply' => $status->comments_disabled == true ? '[]' : 'https://www.w3.org/ns/activitystreams#Public' 'reply' => $status->comments_disabled == true ? '[]' : 'https://www.w3.org/ns/activitystreams#Public',
], ],
'location' => $status->place_id ? [ 'location' => $status->place_id ? [
'type' => 'Place', 'type' => 'Place',
'name' => $status->place->name, 'name' => $status->place->name,
'longitude' => $status->place->long, 'longitude' => $status->place->long,
'latitude' => $status->place->lat, 'latitude' => $status->place->lat,
'country' => $status->place->country 'country' => $status->place->country,
] : null, ] : null,
]; ];
} }

View file

@ -3,8 +3,9 @@
namespace App\Transformer\ActivityPub\Verb; namespace App\Transformer\ActivityPub\Verb;
use App\Status; use App\Status;
use League\Fractal; use App\Util\Lexer\Autolink;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use League\Fractal;
class Question extends Fractal\TransformerAbstract class Question extends Fractal\TransformerAbstract
{ {
@ -15,10 +16,11 @@ class Question extends Fractal\TransformerAbstract
$name = Str::startsWith($webfinger, '@') ? $name = Str::startsWith($webfinger, '@') ?
$webfinger : $webfinger :
'@'.$webfinger; '@'.$webfinger;
return [ return [
'type' => 'Mention', 'type' => 'Mention',
'href' => $mention->permalink(), 'href' => $mention->permalink(),
'name' => $name 'name' => $name,
]; ];
})->toArray(); })->toArray();
@ -30,6 +32,7 @@ class Question extends Fractal\TransformerAbstract
]; ];
})->toArray(); })->toArray();
$tags = array_merge($mentions, $hashtags); $tags = array_merge($mentions, $hashtags);
$content = $status->caption ? Autolink::create()->autolink($status->caption) : null;
return [ return [
'@context' => [ '@context' => [
@ -42,32 +45,32 @@ class Question extends Fractal\TransformerAbstract
'pixelfed' => 'http://pixelfed.org/ns#', 'pixelfed' => 'http://pixelfed.org/ns#',
'commentsEnabled' => [ 'commentsEnabled' => [
'@id' => 'pixelfed:commentsEnabled', '@id' => 'pixelfed:commentsEnabled',
'@type' => 'schema:Boolean' '@type' => 'schema:Boolean',
], ],
'capabilities' => [ 'capabilities' => [
'@id' => 'pixelfed:capabilities', '@id' => 'pixelfed:capabilities',
'@container' => '@set' '@container' => '@set',
], ],
'announce' => [ 'announce' => [
'@id' => 'pixelfed:canAnnounce', '@id' => 'pixelfed:canAnnounce',
'@type' => '@id' '@type' => '@id',
], ],
'like' => [ 'like' => [
'@id' => 'pixelfed:canLike', '@id' => 'pixelfed:canLike',
'@type' => '@id' '@type' => '@id',
], ],
'reply' => [ 'reply' => [
'@id' => 'pixelfed:canReply', '@id' => 'pixelfed:canReply',
'@type' => '@id' '@type' => '@id',
], ],
'toot' => 'http://joinmastodon.org/ns#', 'toot' => 'http://joinmastodon.org/ns#',
'Emoji' => 'toot:Emoji' 'Emoji' => 'toot:Emoji',
] ],
], ],
'id' => $status->url(), 'id' => $status->url(),
'type' => 'Question', 'type' => 'Question',
'summary' => null, 'summary' => null,
'content' => $status->rendered ?? $status->caption, 'content' => $content,
'inReplyTo' => $status->in_reply_to_id ? $status->parent()->url() : null, 'inReplyTo' => $status->in_reply_to_id ? $status->parent()->url() : null,
'published' => $status->created_at->toAtomString(), 'published' => $status->created_at->toAtomString(),
'url' => $status->url(), 'url' => $status->url(),
@ -81,14 +84,14 @@ class Question extends Fractal\TransformerAbstract
'capabilities' => [ 'capabilities' => [
'announce' => 'https://www.w3.org/ns/activitystreams#Public', 'announce' => 'https://www.w3.org/ns/activitystreams#Public',
'like' => 'https://www.w3.org/ns/activitystreams#Public', 'like' => 'https://www.w3.org/ns/activitystreams#Public',
'reply' => $status->comments_disabled == true ? '[]' : 'https://www.w3.org/ns/activitystreams#Public' 'reply' => $status->comments_disabled == true ? '[]' : 'https://www.w3.org/ns/activitystreams#Public',
], ],
'location' => $status->place_id ? [ 'location' => $status->place_id ? [
'type' => 'Place', 'type' => 'Place',
'name' => $status->place->name, 'name' => $status->place->name,
'longitude' => $status->place->long, 'longitude' => $status->place->long,
'latitude' => $status->place->lat, 'latitude' => $status->place->lat,
'country' => $status->place->country 'country' => $status->place->country,
] : null, ] : null,
'endTime' => $status->poll->expires_at->toAtomString(), 'endTime' => $status->poll->expires_at->toAtomString(),
'oneOf' => collect($status->poll->poll_options)->map(function ($option, $index) use ($status) { 'oneOf' => collect($status->poll->poll_options)->map(function ($option, $index) use ($status) {
@ -97,10 +100,10 @@ class Question extends Fractal\TransformerAbstract
'name' => $option, 'name' => $option,
'replies' => [ 'replies' => [
'type' => 'Collection', 'type' => 'Collection',
'totalItems' => $status->poll->cached_tallies[$index] 'totalItems' => $status->poll->cached_tallies[$index],
] ],
]; ];
}) }),
]; ];
} }
} }

View file

@ -2,10 +2,11 @@
namespace App\Transformer\ActivityPub\Verb; namespace App\Transformer\ActivityPub\Verb;
use App\Status;
use League\Fractal;
use App\Models\CustomEmoji; use App\Models\CustomEmoji;
use App\Status;
use App\Util\Lexer\Autolink;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use League\Fractal;
class UpdateNote extends Fractal\TransformerAbstract class UpdateNote extends Fractal\TransformerAbstract
{ {
@ -16,10 +17,11 @@ class UpdateNote extends Fractal\TransformerAbstract
$name = Str::startsWith($webfinger, '@') ? $name = Str::startsWith($webfinger, '@') ?
$webfinger : $webfinger :
'@'.$webfinger; '@'.$webfinger;
return [ return [
'type' => 'Mention', 'type' => 'Mention',
'href' => $mention->permalink(), 'href' => $mention->permalink(),
'name' => $name 'name' => $name,
]; ];
})->toArray(); })->toArray();
@ -33,7 +35,7 @@ class UpdateNote extends Fractal\TransformerAbstract
$reply = [ $reply = [
'type' => 'Mention', 'type' => 'Mention',
'href' => $parent->permalink(), 'href' => $parent->permalink(),
'name' => $name 'name' => $name,
]; ];
$mentions = array_merge($reply, $mentions); $mentions = array_merge($reply, $mentions);
} }
@ -51,6 +53,7 @@ class UpdateNote extends Fractal\TransformerAbstract
$emoji = array_merge($emojis, $mentions); $emoji = array_merge($emojis, $mentions);
$tags = array_merge($emoji, $hashtags); $tags = array_merge($emoji, $hashtags);
$content = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : "";
$latestEdit = $status->edits()->latest()->first(); $latestEdit = $status->edits()->latest()->first();
return [ return [
@ -64,27 +67,27 @@ class UpdateNote extends Fractal\TransformerAbstract
'pixelfed' => 'http://pixelfed.org/ns#', 'pixelfed' => 'http://pixelfed.org/ns#',
'commentsEnabled' => [ 'commentsEnabled' => [
'@id' => 'pixelfed:commentsEnabled', '@id' => 'pixelfed:commentsEnabled',
'@type' => 'schema:Boolean' '@type' => 'schema:Boolean',
], ],
'capabilities' => [ 'capabilities' => [
'@id' => 'pixelfed:capabilities', '@id' => 'pixelfed:capabilities',
'@container' => '@set' '@container' => '@set',
], ],
'announce' => [ 'announce' => [
'@id' => 'pixelfed:canAnnounce', '@id' => 'pixelfed:canAnnounce',
'@type' => '@id' '@type' => '@id',
], ],
'like' => [ 'like' => [
'@id' => 'pixelfed:canLike', '@id' => 'pixelfed:canLike',
'@type' => '@id' '@type' => '@id',
], ],
'reply' => [ 'reply' => [
'@id' => 'pixelfed:canReply', '@id' => 'pixelfed:canReply',
'@type' => '@id' '@type' => '@id',
], ],
'toot' => 'http://joinmastodon.org/ns#', 'toot' => 'http://joinmastodon.org/ns#',
'Emoji' => 'toot:Emoji' 'Emoji' => 'toot:Emoji',
] ],
], ],
'id' => $status->permalink('#updates/'.$latestEdit->id), 'id' => $status->permalink('#updates/'.$latestEdit->id),
'type' => 'Update', 'type' => 'Update',
@ -96,7 +99,7 @@ class UpdateNote extends Fractal\TransformerAbstract
'id' => $status->url(), 'id' => $status->url(),
'type' => 'Note', 'type' => 'Note',
'summary' => $status->is_nsfw ? $status->cw_summary : null, 'summary' => $status->is_nsfw ? $status->cw_summary : null,
'content' => $status->rendered ?? $status->caption, 'content' => $content,
'inReplyTo' => $status->in_reply_to_id ? $status->parent()->url() : null, 'inReplyTo' => $status->in_reply_to_id ? $status->parent()->url() : null,
'published' => $status->created_at->toAtomString(), 'published' => $status->created_at->toAtomString(),
'url' => $status->url(), 'url' => $status->url(),
@ -118,16 +121,16 @@ class UpdateNote extends Fractal\TransformerAbstract
'capabilities' => [ 'capabilities' => [
'announce' => 'https://www.w3.org/ns/activitystreams#Public', 'announce' => 'https://www.w3.org/ns/activitystreams#Public',
'like' => 'https://www.w3.org/ns/activitystreams#Public', 'like' => 'https://www.w3.org/ns/activitystreams#Public',
'reply' => $status->comments_disabled == true ? '[]' : 'https://www.w3.org/ns/activitystreams#Public' 'reply' => $status->comments_disabled == true ? '[]' : 'https://www.w3.org/ns/activitystreams#Public',
], ],
'location' => $status->place_id ? [ 'location' => $status->place_id ? [
'type' => 'Place', 'type' => 'Place',
'name' => $status->place->name, 'name' => $status->place->name,
'longitude' => $status->place->long, 'longitude' => $status->place->long,
'latitude' => $status->place->lat, 'latitude' => $status->place->lat,
'country' => $status->place->country 'country' => $status->place->country,
] : null, ] : null,
] ],
]; ];
} }
} }

View file

@ -2,17 +2,19 @@
namespace App\Transformer\Api\Mastodon\v1; namespace App\Transformer\Api\Mastodon\v1;
use App\Status;
use League\Fractal;
use Cache;
use App\Services\MediaService; use App\Services\MediaService;
use App\Services\ProfileService; use App\Services\ProfileService;
use App\Services\StatusHashtagService; use App\Services\StatusHashtagService;
use App\Status;
use App\Util\Lexer\Autolink;
use League\Fractal;
class StatusTransformer extends Fractal\TransformerAbstract class StatusTransformer extends Fractal\TransformerAbstract
{ {
public function transform(Status $status) public function transform(Status $status)
{ {
$content = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : "";
return [ return [
'id' => (string) $status->id, 'id' => (string) $status->id,
'created_at' => $status->created_at->toJSON(), 'created_at' => $status->created_at->toJSON(),
@ -31,11 +33,11 @@ class StatusTransformer extends Fractal\TransformerAbstract
'favourited' => $status->liked(), 'favourited' => $status->liked(),
'muted' => false, 'muted' => false,
'bookmarked' => false, 'bookmarked' => false,
'content' => $status->rendered ?? $status->caption ?? '', 'content' => $content,
'reblog' => null, 'reblog' => null,
'application' => [ 'application' => [
'name' => 'web', 'name' => 'web',
'website' => null 'website' => null,
], ],
'mentions' => [], 'mentions' => [],
'emojis' => [], 'emojis' => [],

View file

@ -2,21 +2,20 @@
namespace App\Transformer\Api; namespace App\Transformer\Api;
use App\Status; use App\Models\CustomEmoji;
use League\Fractal;
use Cache;
use App\Services\AccountService; use App\Services\AccountService;
use App\Services\HashidService; use App\Services\HashidService;
use App\Services\LikeService; use App\Services\LikeService;
use App\Services\MediaService; use App\Services\MediaService;
use App\Services\MediaTagService; use App\Services\MediaTagService;
use App\Services\StatusService; use App\Services\PollService;
use App\Services\StatusHashtagService; use App\Services\StatusHashtagService;
use App\Services\StatusLabelService; use App\Services\StatusLabelService;
use App\Services\StatusMentionService; use App\Services\StatusMentionService;
use App\Services\PollService; use App\Services\StatusService;
use App\Models\CustomEmoji; use App\Status;
use App\Util\Lexer\Autolink; use App\Util\Lexer\Autolink;
use League\Fractal;
class StatusStatelessTransformer extends Fractal\TransformerAbstract class StatusStatelessTransformer extends Fractal\TransformerAbstract
{ {
@ -24,9 +23,7 @@ class StatusStatelessTransformer extends Fractal\TransformerAbstract
{ {
$taggedPeople = MediaTagService::get($status->id); $taggedPeople = MediaTagService::get($status->id);
$poll = $status->type === 'poll' ? PollService::get($status->id) : null; $poll = $status->type === 'poll' ? PollService::get($status->id) : null;
$rendered = config('exp.autolink') ? $rendered = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : "";
( $status->caption ? Autolink::create()->autolink($status->caption) : '' ) :
( $status->rendered ?? $status->caption );
return [ return [
'_v' => 1, '_v' => 1,
@ -52,7 +49,7 @@ class StatusStatelessTransformer extends Fractal\TransformerAbstract
'visibility' => $status->scope ?? $status->visibility, 'visibility' => $status->scope ?? $status->visibility,
'application' => [ 'application' => [
'name' => 'web', 'name' => 'web',
'website' => null 'website' => null,
], ],
'language' => null, 'language' => null,
'mentions' => StatusMentionService::get($status->id), 'mentions' => StatusMentionService::get($status->id),

View file

@ -2,24 +2,21 @@
namespace App\Transformer\Api; namespace App\Transformer\Api;
use App\Like; use App\Models\CustomEmoji;
use App\Status; use App\Services\BookmarkService;
use League\Fractal;
use Cache;
use App\Services\HashidService; use App\Services\HashidService;
use App\Services\LikeService; use App\Services\LikeService;
use App\Services\MediaService; use App\Services\MediaService;
use App\Services\MediaTagService; use App\Services\MediaTagService;
use App\Services\StatusService; use App\Services\PollService;
use App\Services\ProfileService;
use App\Services\StatusHashtagService; use App\Services\StatusHashtagService;
use App\Services\StatusLabelService; use App\Services\StatusLabelService;
use App\Services\StatusMentionService; use App\Services\StatusMentionService;
use App\Services\ProfileService; use App\Services\StatusService;
use Illuminate\Support\Str; use App\Status;
use App\Services\PollService;
use App\Models\CustomEmoji;
use App\Services\BookmarkService;
use App\Util\Lexer\Autolink; use App\Util\Lexer\Autolink;
use League\Fractal;
class StatusTransformer extends Fractal\TransformerAbstract class StatusTransformer extends Fractal\TransformerAbstract
{ {
@ -28,9 +25,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
$pid = request()->user()->profile_id; $pid = request()->user()->profile_id;
$taggedPeople = MediaTagService::get($status->id); $taggedPeople = MediaTagService::get($status->id);
$poll = $status->type === 'poll' ? PollService::get($status->id, $pid) : null; $poll = $status->type === 'poll' ? PollService::get($status->id, $pid) : null;
$rendered = config('exp.autolink') ? $content = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : '';
( $status->caption ? Autolink::create()->autolink($status->caption) : '' ) :
( $status->rendered ?? $status->caption );
return [ return [
'_v' => 1, '_v' => 1,
@ -38,10 +33,10 @@ class StatusTransformer extends Fractal\TransformerAbstract
'shortcode' => HashidService::encode($status->id), 'shortcode' => HashidService::encode($status->id),
'uri' => $status->url(), 'uri' => $status->url(),
'url' => $status->url(), 'url' => $status->url(),
'in_reply_to_id' => (string) $status->in_reply_to_id, 'in_reply_to_id' => $status->in_reply_to_id ? (string) $status->in_reply_to_id : null,
'in_reply_to_account_id' => (string) $status->in_reply_to_profile_id, 'in_reply_to_account_id' => $status->in_reply_to_profile_id ? (string) $status->in_reply_to_profile_id : null,
'reblog' => $status->reblog_of_id ? StatusService::get($status->reblog_of_id) : null, 'reblog' => $status->reblog_of_id ? StatusService::get($status->reblog_of_id) : null,
'content' => $rendered, 'content' => $content,
'content_text' => $status->caption, 'content_text' => $status->caption,
'created_at' => str_replace('+00:00', 'Z', $status->created_at->format(DATE_RFC3339_EXTENDED)), 'created_at' => str_replace('+00:00', 'Z', $status->created_at->format(DATE_RFC3339_EXTENDED)),
'emojis' => CustomEmoji::scan($status->caption), 'emojis' => CustomEmoji::scan($status->caption),
@ -55,7 +50,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
'visibility' => $status->scope ?? $status->visibility, 'visibility' => $status->scope ?? $status->visibility,
'application' => [ 'application' => [
'name' => 'web', 'name' => 'web',
'website' => null 'website' => null,
], ],
'language' => null, 'language' => null,
'mentions' => StatusMentionService::get($status->id), 'mentions' => StatusMentionService::get($status->id),

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') public static function instanceActorSign($url, $body = false, $addlHeaders = [], $method = 'post')
{ {
$keyId = config('app.url').'/i/actor#main-key'; $keyId = config('app.url').'/i/actor#main-key';
if(config_cache('database.default') === 'mysql') {
$privateKey = Cache::rememberForever(InstanceActor::PKI_PRIVATE, function () { $privateKey = Cache::rememberForever(InstanceActor::PKI_PRIVATE, function () {
return InstanceActor::first()->private_key; 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) { if ($body) {
$digest = self::_digest($body); $digest = self::_digest($body);
} }

View file

@ -417,8 +417,8 @@ class Inbox
return; return;
} }
$msg = $activity['content']; $msg = Purify::clean($activity['content']);
$msgText = strip_tags($activity['content']); $msgText = strip_tags($msg);
if (Str::startsWith($msgText, '@'.$profile->username)) { if (Str::startsWith($msgText, '@'.$profile->username)) {
$len = strlen('@'.$profile->username); $len = strlen('@'.$profile->username);
@ -438,7 +438,6 @@ class Inbox
$status = new Status; $status = new Status;
$status->profile_id = $actor->id; $status->profile_id = $actor->id;
$status->caption = $msgText; $status->caption = $msgText;
$status->rendered = $msg;
$status->visibility = 'direct'; $status->visibility = 'direct';
$status->scope = 'direct'; $status->scope = 'direct';
$status->url = $activity['id']; $status->url = $activity['id'];
@ -1081,7 +1080,6 @@ class Inbox
$status->uri = $url; $status->uri = $url;
$status->object_url = $url; $status->object_url = $url;
$status->caption = $text; $status->caption = $text;
$status->rendered = $text;
$status->scope = 'direct'; $status->scope = 'direct';
$status->visibility = 'direct'; $status->visibility = 'direct';
$status->in_reply_to_profile_id = $story->profile_id; $status->in_reply_to_profile_id = $story->profile_id;
@ -1199,7 +1197,6 @@ class Inbox
$status->profile_id = $actorProfile->id; $status->profile_id = $actorProfile->id;
$status->type = 'story:reply'; $status->type = 'story:reply';
$status->caption = $text; $status->caption = $text;
$status->rendered = $text;
$status->url = $url; $status->url = $url;
$status->uri = $url; $status->uri = $url;
$status->object_url = $url; $status->object_url = $url;

View file

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

1294
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), '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' => [ 'redis:session' => [
'driver' => 'redis', 'driver' => 'redis',
'connection' => 'default', 'connection' => 'session',
'prefix' => 'pf_session', 'prefix' => 'pf_session',
], ],

View file

@ -143,6 +143,24 @@ return [
'database' => env('REDIS_DATABASE', 0), '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' => [ 'dbal' => [

View file

@ -24,6 +24,10 @@ return [
], ],
], ],
'image_optimize' => [
'catch_unoptimized_media_hour_limit' => env('PF_CATCHUNOPTIMIZEDMEDIA', false),
],
'hls' => [ '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' => [ 'mailgun' => [
'domain' => env('MAILGUN_DOMAIN'), 'domain' => env('MAILGUN_DOMAIN'),
'secret' => env('MAILGUN_SECRET'), 'secret' => env('MAILGUN_SECRET'),
'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
'scheme' => 'https',
], ],
'ses' => [ 'ses' => [

View file

@ -19,7 +19,7 @@ class CreateDirectMessagesTable extends Migration
$table->bigInteger('from_id')->unsigned()->index(); $table->bigInteger('from_id')->unsigned()->index();
$table->string('from_profile_ids')->nullable(); $table->string('from_profile_ids')->nullable();
$table->boolean('group_message')->default(false); $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->unique(['to_id', 'from_id', 'status_id']);
$table->timestamp('read_at')->nullable(); $table->timestamp('read_at')->nullable();
$table->timestamps(); $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\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
return new class extends Migration return new class extends Migration
@ -12,6 +13,9 @@ return new class extends Migration
public function up(): void public function up(): void
{ {
Schema::table('group_posts', function (Blueprint $table) { Schema::table('group_posts', function (Blueprint $table) {
if (DB::getDriverName() === 'sqlite') {
$table->dropUnique(['status_id']);
}
$table->dropColumn('status_id'); $table->dropColumn('status_id');
$table->dropColumn('reply_child_id'); $table->dropColumn('reply_child_id');
$table->dropColumn('in_reply_to_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 # See: https://github.com/nginx-proxy/nginx-proxy/tree/main/docs
proxy: proxy:
image: nginxproxy/nginx-proxy:1.4 image: "nginxproxy/nginx-proxy:${DOCKER_PROXY_VERSION}"
container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-proxy" container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-proxy"
restart: unless-stopped restart: unless-stopped
profiles: profiles:

68
funding.json Normal file
View file

@ -0,0 +1,68 @@
{
"version": "v1.0.0",
"entity": {
"type": "individual",
"role": "owner",
"name": "Daniel Supernault",
"email": "danielsupernault@gmail.com",
"phone": "",
"description": "I'm the developer behind Pixelfed, an open-source, federated photo-sharing platform that prioritizes privacy, community, and creativity. With a passion for building ethical alternatives to mainstream social networks, I have championed decentralized technologies and empowered users to share their stories without compromising their digital autonomy. As the driving force behind Pixelfed, I combine innovative development with a thoughtful approach to user experience, fostering an online space that feels personal, authentic, and inclusive.",
"webpageUrl": {
"url": "https://github.com/pixelfed/pixelfed"
}
},
"projects": [
{
"guid": "pixelfed",
"name": "Pixelfed",
"description": "Pixelfed is a free, open-source photo-sharing platform designed to put users in control of their content. Built on the principles of decentralization, Pixelfed offers a refreshing alternative to traditional social media, prioritizing privacy, community, and ethical design. Whether you're an artist, photographer, or someone who loves sharing moments, Pixelfed lets you connect and create without intrusive ads, algorithms, or data exploitation. Fully federated and part of the Fediverse, Pixelfed empowers users to join or host their own instances while still connecting with a global network of creatives and communities. It's not just a platform—it's a movement toward a better, more user-centered internet.",
"webpageUrl": {
"url": "https://github.com/pixelfed/pixelfed"
},
"repositoryUrl": {
"url": "https://github.com/pixelfed/pixelfed"
},
"licenses": ["spdx:AGPL-3.0"],
"tags": ["activitypub", "fediverse", "laravel", "pixelfed"]
}
],
"funding": {
"channels": [
{
"guid": "github-sponsors",
"type": "payment-provider",
"address": "https://github.com/sponsors/dansup",
"description": "Sponsor me through Github."
},
{
"guid": "paypal-sponsors",
"type": "payment-provider",
"address": "https://www.paypal.com/paypalme/dansup",
"description": "Sponsor me through Paypal."
}
],
"plans": [
{
"guid": "developer-time",
"status": "active",
"name": "Developer compensation",
"description": "This will cover the cost of one developer working part-time on the projects.",
"amount": 0,
"currency": "USD",
"frequency": "monthly",
"channels": ["github-sponsors", "paypal-sponsors"]
},
{
"guid": "support-plan",
"status": "active",
"name": "Support plan",
"description": "Pay anything you wish/can to show your support for the projects.",
"amount": 0,
"currency": "USD",
"frequency": "one-time",
"channels": ["github-sponsors", "paypal-sponsors"]
}
],
"history": []
}
}

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-loading-overlay": "^3.3.3",
"vue-timeago": "^5.1.2", "vue-timeago": "^5.1.2",
"vue-tribute": "^1.0.7", "vue-tribute": "^1.0.7",
"webgl-media-editor": "^0.0.1",
"zuck.js": "^1.6.0" "zuck.js": "^1.6.0"
}, },
"collective": { "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.

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