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
# @dottie/validate required
DOCKER_PROXY_VERSION="1.4"
DOCKER_PROXY_VERSION="1.6"
# How often Docker health check should run for [proxy] service
# @dottie/validate required

View file

@ -1,6 +1,39 @@
# Release Notes
## [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/))
## [v0.12.4 (2024-11-08)](https://github.com/pixelfed/pixelfed/compare/v0.12.4...dev)

View file

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

View file

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

View file

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

View file

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

View file

@ -5,8 +5,9 @@ namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
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.
@ -39,13 +40,19 @@ class UserVerifyEmail extends Command
*/
public function handle()
{
$user = User::whereUsername($this->argument('username'))->first();
$username = $this->argument('username');
$user = User::whereUsername($username)->first();
if(!$user) {
$this->error('Username not found');
return;
}
if($user->email_verified_at) {
$this->error('Email already verified ' . $user->email_verified_at->diffForHumans());
return;
}
$user->email_verified_at = now();
$user->save();
$this->info('Successfully verified email address for ' . $user->username);

View file

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

View file

@ -137,7 +137,10 @@ class ApiV1Controller extends Controller
'redirect_uris' => 'required',
]);
$uris = implode(',', explode('\n', $request->redirect_uris));
$uris = collect(explode("\n", $request->redirect_uris))
->map('urldecode')
->filter()
->join(',');
$client = Passport::client()->forceFill([
'user_id' => null,
@ -1426,6 +1429,8 @@ class ApiV1Controller extends Controller
$status['favourited'] = true;
$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);
}
@ -1484,6 +1489,8 @@ class ApiV1Controller extends Controller
$status['favourited'] = false;
$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);
}
@ -1878,7 +1885,7 @@ class ApiV1Controller extends Controller
$media->original_sha256 = $hash;
$media->size = $photo->getSize();
$media->mime = $mime;
$media->caption = $request->input('description') ?? "";
$media->caption = $request->input('description') ?? '';
$media->filter_class = $filterClass;
$media->filter_name = $filterName;
if ($license) {
@ -2106,7 +2113,7 @@ class ApiV1Controller extends Controller
$media->original_sha256 = $hash;
$media->size = $photo->getSize();
$media->mime = $mime;
$media->caption = $request->input('description') ?? "";
$media->caption = $request->input('description') ?? '';
$media->filter_class = $filterClass;
$media->filter_name = $filterName;
if ($license) {
@ -3490,8 +3497,8 @@ class ApiV1Controller extends Controller
return [];
}
$content = strip_tags($request->input('status'));
$rendered = Autolink::create()->autolink($content);
$defaultCaption = "";
$content = $request->filled('status') ? strip_tags($request->input('status')) : $defaultCaption;
$cw = $user->profile->cw == true ? true : $request->boolean('sensitive', false);
$spoilerText = $cw && $request->filled('spoiler_text') ? $request->input('spoiler_text') : null;
@ -3505,7 +3512,7 @@ class ApiV1Controller extends Controller
$status = new Status;
$status->caption = $content;
$status->rendered = $rendered;
$status->rendered = $defaultCaption;
$status->scope = $visibility;
$status->visibility = $visibility;
$status->profile_id = $user->profile_id;
@ -3530,7 +3537,7 @@ class ApiV1Controller extends Controller
if (! $in_reply_to_id) {
$status = new Status;
$status->caption = $content;
$status->rendered = $rendered;
$status->rendered = $defaultCaption;
$status->profile_id = $user->profile_id;
$status->is_nsfw = $cw;
$status->cw_summary = $spoilerText;
@ -3683,7 +3690,10 @@ class ApiV1Controller extends Controller
}
}
$defaultCaption = config_cache('database.default') === 'mysql' ? null : '';
$share = Status::firstOrCreate([
'caption' => $defaultCaption,
'rendered' => $defaultCaption,
'profile_id' => $user->profile_id,
'reblog_of_id' => $status->id,
'type' => 'share',
@ -3698,6 +3708,8 @@ class ApiV1Controller extends Controller
ReblogService::add($user->profile_id, $status->id);
$res = StatusService::getMastodon($status->id);
$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);
}
@ -3744,6 +3756,8 @@ class ApiV1Controller extends Controller
$res = StatusService::getMastodon($status->id);
$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);
}
@ -3951,6 +3965,7 @@ class ApiV1Controller extends Controller
abort_unless($request->user()->tokenCan('write'), 403);
$status = Status::findOrFail($id);
$user = $request->user();
$pid = $request->user()->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');
@ -3994,6 +4009,7 @@ class ApiV1Controller extends Controller
$status = Status::findOrFail($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($status->in_reply_to_id || $status->reblog_of_id, 404);

View file

@ -37,7 +37,6 @@ use App\Status;
use App\StatusArchived;
use App\User;
use App\UserSetting;
use App\Util\Lexer\Autolink;
use App\Util\Lexer\RestrictedNames;
use Cache;
use DB;
@ -49,6 +48,7 @@ use Jenssegers\Agent\Agent;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use Mail;
use Purify;
class ApiV1Dot1Controller extends Controller
{
@ -629,9 +629,6 @@ class ApiV1Dot1Controller extends Controller
abort_if(BouncerService::checkIp($request->ip()), 404);
}
$rl = RateLimiter::attempt('pf:apiv1.1:iarc:'.$request->ip(), config('pixelfed.app_registration_confirm_rate_limit_attempts', 20), function () {}, config('pixelfed.app_registration_confirm_rate_limit_decay', 1800));
abort_if(! $rl, 429, 'Too many requests');
$request->validate([
'user_token' => 'required',
'random_token' => 'required',
@ -658,7 +655,7 @@ class ApiV1Dot1Controller extends Controller
$user->last_active_at = now();
$user->save();
$token = $user->createToken('Pixelfed', ['read', 'write', 'follow', 'admin:read', 'admin:write', 'push']);
$token = $user->createToken('Pixelfed', ['read', 'write', 'follow', 'push']);
return response()->json([
'access_token' => $token->accessToken,
@ -1292,15 +1289,14 @@ class ApiV1Dot1Controller extends Controller
if ($user->last_active_at == null) {
return [];
}
$content = strip_tags($request->input('status'));
$rendered = Autolink::create()->autolink($content);
$defaultCaption = '';
$content = $request->filled('status') ? strip_tags(Purify::clean($request->input('status'))) : $defaultCaption;
$cw = $user->profile->cw == true ? true : $request->boolean('sensitive', false);
$spoilerText = $cw && $request->filled('spoiler_text') ? $request->input('spoiler_text') : null;
$status = new Status;
$status->caption = $content;
$status->rendered = $rendered;
$status->rendered = $defaultCaption;
$status->profile_id = $user->profile_id;
$status->is_nsfw = $cw;
$status->cw_summary = $spoilerText;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,12 +2,10 @@
namespace App\Http\Controllers;
use App\Status;
use Auth;
use DB;
use Illuminate\Http\Request;
use App\{
Profile,
Status,
};
use Auth, DB, Purify;
use Illuminate\Validation\Rule;
class MicroController extends Controller
@ -23,7 +21,7 @@ class MicroController extends Controller
'type' => [
'required',
'string',
Rule::in(['text'])
Rule::in(['text']),
],
'title' => 'nullable|string|max:140',
'content' => 'required|string|max:500',
@ -34,9 +32,9 @@ class MicroController extends Controller
'public',
'unlisted',
'private',
'draft'
])
]
'draft',
]),
],
]);
$profile = Auth::user()->profile;
$title = $request->input('title');
@ -48,7 +46,6 @@ class MicroController extends Controller
$status->type = 'text';
$status->profile_id = $profile->id;
$status->caption = strip_tags($content);
$status->rendered = Purify::clean($content);
$status->is_nsfw = false;
// TODO: remove deprecated visibility in favor of scope
@ -56,12 +53,14 @@ class MicroController extends Controller
$status->scope = $visibility;
$status->entities = json_encode(['title' => $title]);
$status->save();
return $status;
});
$fractal = new \League\Fractal\Manager();
$fractal->setSerializer(new \League\Fractal\Serializer\ArraySerializer());
$s = new \League\Fractal\Resource\Item($status, new \App\Transformer\Api\StatusTransformer());
$fractal = new \League\Fractal\Manager;
$fractal->setSerializer(new \League\Fractal\Serializer\ArraySerializer);
$s = new \League\Fractal\Resource\Item($status, new \App\Transformer\Api\StatusTransformer);
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()
{
$this->fractal = new Fractal\Manager();
$this->fractal->setSerializer(new ArraySerializer());
$this->fractal = new Fractal\Manager;
$this->fractal->setSerializer(new ArraySerializer);
}
protected function getUserData($user)
@ -74,7 +74,7 @@ class PublicApiController extends Controller
abort_if(! in_array($cached['visibility'], ['public', 'unlisted']), 403);
$res = ['status' => $cached];
} else {
$item = new Fractal\Resource\Item($status, new StatusStatelessTransformer());
$item = new Fractal\Resource\Item($status, new StatusStatelessTransformer);
$res = [
'status' => $this->fractal->createData($item)->toArray(),
];
@ -141,7 +141,7 @@ class PublicApiController extends Controller
$replies = $status->comments()
->whereNull('reblog_of_id')
->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)
->orderBy('id', 'desc')
->paginate($limit);
@ -150,7 +150,7 @@ class PublicApiController extends Controller
$replies = $status->comments()
->whereNull('reblog_of_id')
->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)
->orderBy('id', 'desc')
->paginate($limit);
@ -159,12 +159,12 @@ class PublicApiController extends Controller
$replies = Status::whereInReplyToId($status->id)
->whereNull('reblog_of_id')
->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')
->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));
$res = $this->fractal->createData($resource)->toArray();
@ -271,7 +271,6 @@ class PublicApiController extends Controller
'id',
'uri',
'caption',
'rendered',
'profile_id',
'type',
'in_reply_to_id',
@ -405,7 +404,6 @@ class PublicApiController extends Controller
'id',
'uri',
'caption',
'rendered',
'profile_id',
'type',
'in_reply_to_id',
@ -456,7 +454,6 @@ class PublicApiController extends Controller
'id',
'uri',
'caption',
'rendered',
'profile_id',
'type',
'in_reply_to_id',

View file

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

View file

@ -2,25 +2,23 @@
namespace App\Http\Controllers\Settings;
use App\AccountLog;
use App\Following;
use App\Report;
use App\Status;
use App\UserFilter;
use Auth, Cookie, DB, Cache, Purify;
use Carbon\Carbon;
use Illuminate\Http\Request;
use App\Transformer\ActivityPub\{
ProfileTransformer,
StatusTransformer
};
use App\Transformer\ActivityPub\ProfileTransformer;
use App\Transformer\Api\StatusTransformer as StatusApiTransformer;
use App\UserFilter;
use Auth;
use Cache;
use Illuminate\Http\Request;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use Storage;
trait ExportSettings
{
private const CHUNK_SIZE = 1000;
private const STORAGE_BASE = 'user_exports';
public function __construct()
{
$this->middleware('auth');
@ -33,47 +31,146 @@ trait ExportSettings
public function exportAccount()
{
$data = Cache::remember('account:export:profile:actor:'.Auth::user()->profile->id, now()->addMinutes(60), function() {
$profile = Auth::user()->profile;
$fractal = new Fractal\Manager();
$fractal->setSerializer(new ArraySerializer());
$resource = new Fractal\Resource\Item($profile, new ProfileTransformer());
return $fractal->createData($resource)->toArray();
});
$fractal = new Fractal\Manager;
$fractal->setSerializer(new ArraySerializer);
$resource = new Fractal\Resource\Item($profile, new ProfileTransformer);
$data = $fractal->createData($resource)->toArray();
return response()->streamDownload(function () use ($data) {
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}, 'account.json', [
'Content-Type' => 'application/json'
'Content-Type' => 'application/json',
]);
}
public function exportFollowing()
{
$data = Cache::remember('account:export:profile:following:'.Auth::user()->profile->id, now()->addMinutes(60), function() {
return Auth::user()->profile->following()->get()->map(function($i) {
return $i->url();
$profile = Auth::user()->profile;
$userId = Auth::id();
$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;
}, 'following.json', [
'Content-Type' => 'application/json'
]);
Storage::append($tempPath, ']');
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()
{
$data = Cache::remember('account:export:profile:followers:'.Auth::user()->profile->id, now()->addMinutes(60), function() {
return Auth::user()->profile->followers()->get()->map(function($i) {
return $i->url();
$profile = Auth::user()->profile;
$userId = Auth::id();
$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;
}, 'followers.json', [
'Content-Type' => 'application/json'
]);
Storage::append($tempPath, ']');
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()
@ -88,57 +185,77 @@ trait ExportSettings
$data = Cache::remember('account:export:profile:muteblocklist:'.Auth::user()->profile->id, now()->addMinutes(60), function () use ($profile) {
return json_encode([
'muted' => $profile->mutedProfileUrls(),
'blocked' => $profile->blockedProfileUrls()
'blocked' => $profile->blockedProfileUrls(),
], JSON_PRETTY_PRINT);
});
return response()->streamDownload(function () use ($data) {
echo $data;
}, 'muted-and-blocked-accounts.json', [
'Content-Type' => 'application/json'
'Content-Type' => 'application/json',
]);
}
public function exportStatuses(Request $request)
{
$this->validate($request, [
'type' => 'required|string|in:ap,api'
]);
$limit = 500;
$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($count > $limit) {
// fire background job
return redirect('/settings/data-export')->with(['status' => 'You have more than '.$limit.' statuses, we do not support full account export yet.']);
if (! Storage::exists($userExportPath)) {
Storage::makeDirectory($userExportPath);
}
$filename = 'outbox.json';
if($type == 'ap') {
$data = Cache::remember('account:export:profile:statuses:ap:'.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 StatusTransformer());
return $fractal->createData($resource)->toArray();
Storage::put($tempPath, '[');
$fractal = new Fractal\Manager;
$fractal->setSerializer(new ArraySerializer);
try {
Status::whereProfileId($profile->id)
->chunk(self::CHUNK_SIZE, function ($statuses) use ($fractal, $tempPath) {
$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) {
echo json_encode($data, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);
}, $filename, [
'Content-Type' => 'application/json'
]);
}
Storage::append($tempPath, ']');
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');
$count = $status->reblogs_count;
$defaultCaption = config_cache('database.default') === 'mysql' ? null : "";
$exists = Status::whereProfileId(Auth::user()->profile->id)
->whereReblogOfId($status->id)
->exists();
@ -324,6 +324,8 @@ class StatusController extends Controller
}
} else {
$share = new Status;
$share->caption = $defaultCaption;
$share->rendered = $defaultCaption;
$share->profile_id = $profile->id;
$share->reblog_of_id = $status->id;
$share->in_reply_to_profile_id = $status->profile_id;

View file

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

View file

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

View file

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

View file

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

View file

@ -2,35 +2,32 @@
namespace App\Jobs\GroupPipeline;
use App\Notification;
use App\Hashtag;
use App\Mention;
use App\Profile;
use App\Status;
use App\StatusHashtag;
use App\Models\GroupPostHashtag;
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 Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
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
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status;
protected $gp;
protected $tags;
protected $mentions;
public function __construct(Status $status, GroupPost $gp)
@ -50,10 +47,6 @@ class NewStatusPipeline implements ShouldQueue
->autolink($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->save();
@ -117,7 +110,7 @@ class NewStatusPipeline implements ShouldQueue
}
DB::transaction(function () use ($status, $mentioned) {
$m = new Mention();
$m = new Mention;
$m->status_id = $status->id;
$m->profile_id = $mentioned->id;
$m->save();

View file

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

View file

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

View file

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

View file

@ -2,39 +2,39 @@
namespace App\Providers;
use App\Observers\{
AvatarObserver,
FollowerObserver,
HashtagFollowObserver,
LikeObserver,
NotificationObserver,
ModLogObserver,
ProfileObserver,
StatusHashtagObserver,
StatusObserver,
UserObserver,
UserFilterObserver,
};
use App\{
Avatar,
Follower,
HashtagFollow,
Like,
Notification,
ModLog,
Profile,
StatusHashtag,
Status,
User,
UserFilter
};
use Auth, Horizon, URL;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Facades\Validator;
use App\Avatar;
use App\Follower;
use App\HashtagFollow;
use App\Like;
use App\ModLog;
use App\Notification;
use App\Observers\AvatarObserver;
use App\Observers\FollowerObserver;
use App\Observers\HashtagFollowObserver;
use App\Observers\LikeObserver;
use App\Observers\ModLogObserver;
use App\Observers\NotificationObserver;
use App\Observers\ProfileObserver;
use App\Observers\StatusHashtagObserver;
use App\Observers\StatusObserver;
use App\Observers\UserFilterObserver;
use App\Observers\UserObserver;
use App\Profile;
use App\Services\AccountService;
use App\Status;
use App\StatusHashtag;
use App\User;
use App\UserFilter;
use Auth;
use Horizon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\ServiceProvider;
use Laravel\Pulse\Facades\Pulse;
use URL;
class AppServiceProvider extends ServiceProvider
{
@ -67,6 +67,24 @@ class AppServiceProvider extends ServiceProvider
});
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);
}

View file

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

View file

@ -2,53 +2,25 @@
namespace App\Services;
use Cache;
use App\Profile;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Http;
use App\Util\Webfinger\WebfingerUrl;
use Cache;
use Purify;
class AutolinkService
{
const CACHE_KEY = 'pf:services:autolink:';
const CACHE_KEY = 'pf:services:autolink:mue:';
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) {
$remote = Str::of($username)->contains('@');
$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;
return Cache::remember(self::CACHE_KEY.base64_encode($name), 7200, function () use ($name) {
return Profile::where('username', $name)->exists();
});
}
}

View file

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

View file

@ -3,14 +3,13 @@
namespace App\Services\Status;
use App\Media;
use App\ModLog;
use App\Status;
use App\Models\StatusEdit;
use Purify;
use App\Util\Lexer\Autolink;
use App\ModLog;
use App\Services\MediaService;
use App\Services\MediaStorageService;
use App\Services\StatusService;
use App\Status;
use Purify;
class UpdateStatusService
{
@ -31,7 +30,9 @@ class UpdateStatusService
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']);
if ($oids->toArray() === $nids->toArray()) {
@ -64,15 +65,12 @@ class UpdateStatusService
if (isset($attributes['status'])) {
$cleaned = Purify::clean($attributes['status']);
$status->caption = $cleaned;
$status->rendered = nl2br(Autolink::create()->autolink($cleaned));
} else {
$status->caption = null;
$status->rendered = null;
}
if (isset($attributes['sensitive'])) {
if ($status->is_nsfw != (bool) $attributes['sensitive'] &&
(bool) $attributes['sensitive'] == false)
{
(bool) $attributes['sensitive'] == false) {
$exists = ModLog::whereObjectType('App\Status::class')
->whereObjectId($status->id)
->whereAction('admin.status.moderate')
@ -114,7 +112,7 @@ class UpdateStatusService
'spoiler_text' => $status->cw_summary,
'is_nsfw' => $status->is_nsfw,
'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,
'spoiler_text' => $spoiler_text,
'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
{
const CACHE_KEY = 'pf:services:status:';
const CACHE_KEY = 'pf:services:status:v1.1:';
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);
}
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)
{
if(!in_array($audience, ['to', 'cc']) || $this->local == false) {

View file

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

View file

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

View file

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

View file

@ -3,8 +3,9 @@
namespace App\Transformer\ActivityPub\Verb;
use App\Status;
use League\Fractal;
use App\Util\Lexer\Autolink;
use Illuminate\Support\Str;
use League\Fractal;
class Question extends Fractal\TransformerAbstract
{
@ -15,10 +16,11 @@ class Question extends Fractal\TransformerAbstract
$name = Str::startsWith($webfinger, '@') ?
$webfinger :
'@'.$webfinger;
return [
'type' => 'Mention',
'href' => $mention->permalink(),
'name' => $name
'name' => $name,
];
})->toArray();
@ -30,6 +32,7 @@ class Question extends Fractal\TransformerAbstract
];
})->toArray();
$tags = array_merge($mentions, $hashtags);
$content = $status->caption ? Autolink::create()->autolink($status->caption) : null;
return [
'@context' => [
@ -42,32 +45,32 @@ class Question extends Fractal\TransformerAbstract
'pixelfed' => 'http://pixelfed.org/ns#',
'commentsEnabled' => [
'@id' => 'pixelfed:commentsEnabled',
'@type' => 'schema:Boolean'
'@type' => 'schema:Boolean',
],
'capabilities' => [
'@id' => 'pixelfed:capabilities',
'@container' => '@set'
'@container' => '@set',
],
'announce' => [
'@id' => 'pixelfed:canAnnounce',
'@type' => '@id'
'@type' => '@id',
],
'like' => [
'@id' => 'pixelfed:canLike',
'@type' => '@id'
'@type' => '@id',
],
'reply' => [
'@id' => 'pixelfed:canReply',
'@type' => '@id'
'@type' => '@id',
],
'toot' => 'http://joinmastodon.org/ns#',
'Emoji' => 'toot:Emoji'
]
'Emoji' => 'toot:Emoji',
],
],
'id' => $status->url(),
'type' => 'Question',
'summary' => null,
'content' => $status->rendered ?? $status->caption,
'content' => $content,
'inReplyTo' => $status->in_reply_to_id ? $status->parent()->url() : null,
'published' => $status->created_at->toAtomString(),
'url' => $status->url(),
@ -81,14 +84,14 @@ class Question extends Fractal\TransformerAbstract
'capabilities' => [
'announce' => '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 ? [
'type' => 'Place',
'name' => $status->place->name,
'longitude' => $status->place->long,
'latitude' => $status->place->lat,
'country' => $status->place->country
'country' => $status->place->country,
] : null,
'endTime' => $status->poll->expires_at->toAtomString(),
'oneOf' => collect($status->poll->poll_options)->map(function ($option, $index) use ($status) {
@ -97,10 +100,10 @@ class Question extends Fractal\TransformerAbstract
'name' => $option,
'replies' => [
'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;
use App\Status;
use League\Fractal;
use App\Models\CustomEmoji;
use App\Status;
use App\Util\Lexer\Autolink;
use Illuminate\Support\Str;
use League\Fractal;
class UpdateNote extends Fractal\TransformerAbstract
{
@ -16,10 +17,11 @@ class UpdateNote extends Fractal\TransformerAbstract
$name = Str::startsWith($webfinger, '@') ?
$webfinger :
'@'.$webfinger;
return [
'type' => 'Mention',
'href' => $mention->permalink(),
'name' => $name
'name' => $name,
];
})->toArray();
@ -33,7 +35,7 @@ class UpdateNote extends Fractal\TransformerAbstract
$reply = [
'type' => 'Mention',
'href' => $parent->permalink(),
'name' => $name
'name' => $name,
];
$mentions = array_merge($reply, $mentions);
}
@ -51,6 +53,7 @@ class UpdateNote extends Fractal\TransformerAbstract
$emoji = array_merge($emojis, $mentions);
$tags = array_merge($emoji, $hashtags);
$content = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : "";
$latestEdit = $status->edits()->latest()->first();
return [
@ -64,27 +67,27 @@ class UpdateNote extends Fractal\TransformerAbstract
'pixelfed' => 'http://pixelfed.org/ns#',
'commentsEnabled' => [
'@id' => 'pixelfed:commentsEnabled',
'@type' => 'schema:Boolean'
'@type' => 'schema:Boolean',
],
'capabilities' => [
'@id' => 'pixelfed:capabilities',
'@container' => '@set'
'@container' => '@set',
],
'announce' => [
'@id' => 'pixelfed:canAnnounce',
'@type' => '@id'
'@type' => '@id',
],
'like' => [
'@id' => 'pixelfed:canLike',
'@type' => '@id'
'@type' => '@id',
],
'reply' => [
'@id' => 'pixelfed:canReply',
'@type' => '@id'
'@type' => '@id',
],
'toot' => 'http://joinmastodon.org/ns#',
'Emoji' => 'toot:Emoji'
]
'Emoji' => 'toot:Emoji',
],
],
'id' => $status->permalink('#updates/'.$latestEdit->id),
'type' => 'Update',
@ -96,7 +99,7 @@ class UpdateNote extends Fractal\TransformerAbstract
'id' => $status->url(),
'type' => 'Note',
'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,
'published' => $status->created_at->toAtomString(),
'url' => $status->url(),
@ -118,16 +121,16 @@ class UpdateNote extends Fractal\TransformerAbstract
'capabilities' => [
'announce' => '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 ? [
'type' => 'Place',
'name' => $status->place->name,
'longitude' => $status->place->long,
'latitude' => $status->place->lat,
'country' => $status->place->country
'country' => $status->place->country,
] : null,
]
],
];
}
}

View file

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

View file

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

View file

@ -2,24 +2,21 @@
namespace App\Transformer\Api;
use App\Like;
use App\Status;
use League\Fractal;
use Cache;
use App\Models\CustomEmoji;
use App\Services\BookmarkService;
use App\Services\HashidService;
use App\Services\LikeService;
use App\Services\MediaService;
use App\Services\MediaTagService;
use App\Services\StatusService;
use App\Services\PollService;
use App\Services\ProfileService;
use App\Services\StatusHashtagService;
use App\Services\StatusLabelService;
use App\Services\StatusMentionService;
use App\Services\ProfileService;
use Illuminate\Support\Str;
use App\Services\PollService;
use App\Models\CustomEmoji;
use App\Services\BookmarkService;
use App\Services\StatusService;
use App\Status;
use App\Util\Lexer\Autolink;
use League\Fractal;
class StatusTransformer extends Fractal\TransformerAbstract
{
@ -28,9 +25,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
$pid = request()->user()->profile_id;
$taggedPeople = MediaTagService::get($status->id);
$poll = $status->type === 'poll' ? PollService::get($status->id, $pid) : null;
$rendered = config('exp.autolink') ?
( $status->caption ? Autolink::create()->autolink($status->caption) : '' ) :
( $status->rendered ?? $status->caption );
$content = $status->caption ? nl2br(Autolink::create()->autolink($status->caption)) : '';
return [
'_v' => 1,
@ -38,10 +33,10 @@ class StatusTransformer extends Fractal\TransformerAbstract
'shortcode' => HashidService::encode($status->id),
'uri' => $status->url(),
'url' => $status->url(),
'in_reply_to_id' => (string) $status->in_reply_to_id,
'in_reply_to_account_id' => (string) $status->in_reply_to_profile_id,
'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,
'reblog' => $status->reblog_of_id ? StatusService::get($status->reblog_of_id) : null,
'content' => $rendered,
'content' => $content,
'content_text' => $status->caption,
'created_at' => str_replace('+00:00', 'Z', $status->created_at->format(DATE_RFC3339_EXTENDED)),
'emojis' => CustomEmoji::scan($status->caption),
@ -55,7 +50,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
'visibility' => $status->scope ?? $status->visibility,
'application' => [
'name' => 'web',
'website' => null
'website' => null,
],
'language' => null,
'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')
{
$keyId = config('app.url').'/i/actor#main-key';
if(config_cache('database.default') === 'mysql') {
$privateKey = Cache::rememberForever(InstanceActor::PKI_PRIVATE, function () {
return InstanceActor::first()->private_key;
});
} else {
$privateKey = InstanceActor::first()?->private_key;
}
abort_if(!$privateKey || empty($privateKey), 400, 'Missing instance actor key, please run php artisan instance:actor');
if ($body) {
$digest = self::_digest($body);
}

View file

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

View file

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

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),
],
'session' => [
'scheme' => env('REDIS_SCHEME', 'tcp'),
'path' => env('REDIS_PATH'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_DATABASE_SESSION', 1),
],
'pulse' => [
'scheme' => env('REDIS_SCHEME', 'tcp'),
'path' => env('REDIS_PATH'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_DATABASE_PULSE', 2),
],
],
'redis:session' => [
'driver' => 'redis',
'connection' => 'default',
'connection' => 'session',
'prefix' => 'pf_session',
],

View file

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

View file

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

236
config/pulse.php Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,7 +17,7 @@ services:
#
# See: https://github.com/nginx-proxy/nginx-proxy/tree/main/docs
proxy:
image: nginxproxy/nginx-proxy:1.4
image: "nginxproxy/nginx-proxy:${DOCKER_PROXY_VERSION}"
container_name: "${DOCKER_ALL_CONTAINER_NAME_PREFIX}-proxy"
restart: unless-stopped
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-timeago": "^5.1.2",
"vue-tribute": "^1.0.7",
"webgl-media-editor": "^0.0.1",
"zuck.js": "^1.6.0"
},
"collective": {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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