Merge pull request #5356 from pixelfed/staging

Staging
This commit is contained in:
daniel 2024-11-19 04:08:47 -07:00 committed by GitHub
commit abe8298999
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 795 additions and 848 deletions

View file

@ -1,10 +1,14 @@
# Release Notes # Release Notes
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.12.3...dev) ## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.12.3...dev)
### Updates
- Update AP helpers, reject statuses with invalid dates ([960f3849](https://github.com/pixelfed/pixelfed/commit/960f3849)) - 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 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 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 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))
- ([](https://github.com/pixelfed/pixelfed/commit/)) - ([](https://github.com/pixelfed/pixelfed/commit/))
## [v0.12.4 (2024-11-08)](https://github.com/pixelfed/pixelfed/compare/v0.12.4...dev) ## [v0.12.4 (2024-11-08)](https://github.com/pixelfed/pixelfed/compare/v0.12.4...dev)

View file

@ -2,17 +2,16 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\ImportPost;
use App\Services\ImportService;
use App\Media; use App\Media;
use App\Models\ImportPost;
use App\Profile; use App\Profile;
use App\Status;
use Storage;
use App\Services\AccountService; use App\Services\AccountService;
use App\Services\ImportService;
use App\Services\MediaPathService; use App\Services\MediaPathService;
use App\Status;
use Illuminate\Console\Command;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use App\Util\Lexer\Autolink; use Storage;
class TransformImports extends Command class TransformImports extends Command
{ {
@ -35,23 +34,24 @@ class TransformImports extends Command
*/ */
public function handle() public function handle()
{ {
if(!config('import.instagram.enabled')) { if (! config('import.instagram.enabled')) {
return; return;
} }
$ips = ImportPost::whereNull('status_id')->where('skip_missing_media', '!=', true)->take(500)->get(); $ips = ImportPost::whereNull('status_id')->where('skip_missing_media', '!=', true)->take(500)->get();
if(!$ips->count()) { if (! $ips->count()) {
return; return;
} }
foreach($ips as $ip) { foreach ($ips as $ip) {
$id = $ip->user_id; $id = $ip->user_id;
$pid = $ip->profile_id; $pid = $ip->profile_id;
$profile = Profile::find($pid); $profile = Profile::find($pid);
if(!$profile) { if (! $profile) {
$ip->skip_missing_media = true; $ip->skip_missing_media = true;
$ip->save(); $ip->save();
continue; continue;
} }
@ -63,39 +63,43 @@ class TransformImports extends Command
->where('creation_day', $ip->creation_day) ->where('creation_day', $ip->creation_day)
->exists(); ->exists();
if($exists == true) { if ($exists == true) {
$ip->skip_missing_media = true; $ip->skip_missing_media = true;
$ip->save(); $ip->save();
continue; continue;
} }
$idk = ImportService::getId($ip->user_id, $ip->creation_year, $ip->creation_month, $ip->creation_day); $idk = ImportService::getId($ip->user_id, $ip->creation_year, $ip->creation_month, $ip->creation_day);
if(!$idk) { if (! $idk) {
$ip->skip_missing_media = true; $ip->skip_missing_media = true;
$ip->save(); $ip->save();
continue; continue;
} }
if(Storage::exists('imports/' . $id . '/' . $ip->filename) === false) { if (Storage::exists('imports/'.$id.'/'.$ip->filename) === false) {
ImportService::clearAttempts($profile->id); ImportService::clearAttempts($profile->id);
ImportService::getPostCount($profile->id, true); ImportService::getPostCount($profile->id, true);
$ip->skip_missing_media = true; $ip->skip_missing_media = true;
$ip->save(); $ip->save();
continue; continue;
} }
$missingMedia = false; $missingMedia = false;
foreach($ip->media as $ipm) { foreach ($ip->media as $ipm) {
$fileName = last(explode('/', $ipm['uri'])); $fileName = last(explode('/', $ipm['uri']));
$og = 'imports/' . $id . '/' . $fileName; $og = 'imports/'.$id.'/'.$fileName;
if(!Storage::exists($og)) { if (! Storage::exists($og)) {
$missingMedia = true; $missingMedia = true;
} }
} }
if($missingMedia === true) { if ($missingMedia === true) {
$ip->skip_missing_media = true; $ip->skip_missing_media = true;
$ip->save(); $ip->save();
continue; continue;
} }
@ -103,7 +107,6 @@ class TransformImports extends Command
$status = new Status; $status = new Status;
$status->profile_id = $pid; $status->profile_id = $pid;
$status->caption = $caption; $status->caption = $caption;
$status->rendered = strlen(trim($caption)) ? Autolink::create()->autolink($ip->caption) : null;
$status->type = $ip->post_type; $status->type = $ip->post_type;
$status->scope = 'unlisted'; $status->scope = 'unlisted';
@ -112,20 +115,21 @@ class TransformImports extends Command
$status->created_at = now()->parse($ip->creation_date); $status->created_at = now()->parse($ip->creation_date);
$status->save(); $status->save();
foreach($ip->media as $ipm) { foreach ($ip->media as $ipm) {
$fileName = last(explode('/', $ipm['uri'])); $fileName = last(explode('/', $ipm['uri']));
$ext = last(explode('.', $fileName)); $ext = last(explode('.', $fileName));
$basePath = MediaPathService::get($profile); $basePath = MediaPathService::get($profile);
$og = 'imports/' . $id . '/' . $fileName; $og = 'imports/'.$id.'/'.$fileName;
if(!Storage::exists($og)) { if (! Storage::exists($og)) {
$ip->skip_missing_media = true; $ip->skip_missing_media = true;
$ip->save(); $ip->save();
continue; continue;
} }
$size = Storage::size($og); $size = Storage::size($og);
$mime = Storage::mimeType($og); $mime = Storage::mimeType($og);
$newFile = Str::random(40) . '.' . $ext; $newFile = Str::random(40).'.'.$ext;
$np = $basePath . '/' . $newFile; $np = $basePath.'/'.$newFile;
Storage::move($og, $np); Storage::move($og, $np);
$media = new Media; $media = new Media;
$media->profile_id = $pid; $media->profile_id = $pid;

View file

@ -3490,8 +3490,7 @@ class ApiV1Controller extends Controller
return []; return [];
} }
$content = strip_tags($request->input('status')); $content = $request->filled('status') ? strip_tags(Purify::clean($request->input('status'))) : null;
$rendered = Autolink::create()->autolink($content);
$cw = $user->profile->cw == true ? true : $request->boolean('sensitive', false); $cw = $user->profile->cw == true ? true : $request->boolean('sensitive', false);
$spoilerText = $cw && $request->filled('spoiler_text') ? $request->input('spoiler_text') : null; $spoilerText = $cw && $request->filled('spoiler_text') ? $request->input('spoiler_text') : null;
@ -3505,7 +3504,6 @@ class ApiV1Controller extends Controller
$status = new Status; $status = new Status;
$status->caption = $content; $status->caption = $content;
$status->rendered = $rendered;
$status->scope = $visibility; $status->scope = $visibility;
$status->visibility = $visibility; $status->visibility = $visibility;
$status->profile_id = $user->profile_id; $status->profile_id = $user->profile_id;
@ -3530,7 +3528,6 @@ class ApiV1Controller extends Controller
if (! $in_reply_to_id) { if (! $in_reply_to_id) {
$status = new Status; $status = new Status;
$status->caption = $content; $status->caption = $content;
$status->rendered = $rendered;
$status->profile_id = $user->profile_id; $status->profile_id = $user->profile_id;
$status->is_nsfw = $cw; $status->is_nsfw = $cw;
$status->cw_summary = $spoilerText; $status->cw_summary = $spoilerText;

View file

@ -37,7 +37,6 @@ use App\Status;
use App\StatusArchived; use App\StatusArchived;
use App\User; use App\User;
use App\UserSetting; use App\UserSetting;
use App\Util\Lexer\Autolink;
use App\Util\Lexer\RestrictedNames; use App\Util\Lexer\RestrictedNames;
use Cache; use Cache;
use DB; use DB;
@ -49,6 +48,7 @@ use Jenssegers\Agent\Agent;
use League\Fractal; use League\Fractal;
use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Serializer\ArraySerializer;
use Mail; use Mail;
use Purify;
class ApiV1Dot1Controller extends Controller class ApiV1Dot1Controller extends Controller
{ {
@ -1293,14 +1293,12 @@ class ApiV1Dot1Controller extends Controller
return []; return [];
} }
$content = strip_tags($request->input('status')); $content = $request->filled('status') ? strip_tags(Purify::clean($request->input('status'))) : null;
$rendered = Autolink::create()->autolink($content);
$cw = $user->profile->cw == true ? true : $request->boolean('sensitive', false); $cw = $user->profile->cw == true ? true : $request->boolean('sensitive', false);
$spoilerText = $cw && $request->filled('spoiler_text') ? $request->input('spoiler_text') : null; $spoilerText = $cw && $request->filled('spoiler_text') ? $request->input('spoiler_text') : null;
$status = new Status; $status = new Status;
$status->caption = $content; $status->caption = $content;
$status->rendered = $rendered;
$status->profile_id = $user->profile_id; $status->profile_id = $user->profile_id;
$status->is_nsfw = $cw; $status->is_nsfw = $cw;
$status->cw_summary = $spoilerText; $status->cw_summary = $spoilerText;

View file

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

View file

@ -25,7 +25,6 @@ use App\Services\UserStorageService;
use App\Status; use App\Status;
use App\Transformer\Api\MediaTransformer; use App\Transformer\Api\MediaTransformer;
use App\UserFilter; use App\UserFilter;
use App\Util\Lexer\Autolink;
use App\Util\Media\Filter; use App\Util\Media\Filter;
use App\Util\Media\License; use App\Util\Media\License;
use Auth; use Auth;
@ -43,8 +42,8 @@ class ComposeController extends Controller
public function __construct() public function __construct()
{ {
$this->middleware('auth'); $this->middleware('auth');
$this->fractal = new Fractal\Manager(); $this->fractal = new Fractal\Manager;
$this->fractal->setSerializer(new ArraySerializer()); $this->fractal->setSerializer(new ArraySerializer);
} }
public function show(Request $request) public function show(Request $request)
@ -112,14 +111,14 @@ class ComposeController extends Controller
abort_if(MediaBlocklistService::exists($hash) == true, 451); abort_if(MediaBlocklistService::exists($hash) == true, 451);
$media = new Media(); $media = new Media;
$media->status_id = null; $media->status_id = null;
$media->profile_id = $profile->id; $media->profile_id = $profile->id;
$media->user_id = $user->id; $media->user_id = $user->id;
$media->media_path = $path; $media->media_path = $path;
$media->original_sha256 = $hash; $media->original_sha256 = $hash;
$media->size = $photo->getSize(); $media->size = $photo->getSize();
$media->caption = ""; $media->caption = '';
$media->mime = $mime; $media->mime = $mime;
$media->filter_class = $filterClass; $media->filter_class = $filterClass;
$media->filter_name = $filterName; $media->filter_name = $filterName;
@ -151,7 +150,7 @@ class ComposeController extends Controller
$user->save(); $user->save();
Cache::forget($limitKey); Cache::forget($limitKey);
$resource = new Fractal\Resource\Item($media, new MediaTransformer()); $resource = new Fractal\Resource\Item($media, new MediaTransformer);
$res = $this->fractal->createData($resource)->toArray(); $res = $this->fractal->createData($resource)->toArray();
$res['preview_url'] = $preview_url; $res['preview_url'] = $preview_url;
$res['url'] = $url; $res['url'] = $url;
@ -571,7 +570,6 @@ class ComposeController extends Controller
} }
$status->caption = strip_tags($request->caption); $status->caption = strip_tags($request->caption);
$status->rendered = Autolink::create()->autolink($status->caption);
$status->scope = 'draft'; $status->scope = 'draft';
$status->visibility = 'draft'; $status->visibility = 'draft';
$status->profile_id = $profile->id; $status->profile_id = $profile->id;
@ -693,7 +691,6 @@ class ComposeController extends Controller
$status->visibility = $visibility; $status->visibility = $visibility;
$status->scope = $visibility; $status->scope = $visibility;
$status->type = 'text'; $status->type = 'text';
$status->rendered = Autolink::create()->autolink($status->caption);
$status->entities = json_encode(array_merge([ $status->entities = json_encode(array_merge([
'timg' => [ 'timg' => [
'version' => 0, 'version' => 0,
@ -806,7 +803,6 @@ class ComposeController extends Controller
$status = new Status; $status = new Status;
$status->profile_id = $request->user()->profile_id; $status->profile_id = $request->user()->profile_id;
$status->caption = $request->input('caption'); $status->caption = $request->input('caption');
$status->rendered = Autolink::create()->autolink($status->caption);
$status->visibility = 'draft'; $status->visibility = 'draft';
$status->scope = 'draft'; $status->scope = 'draft';
$status->type = 'poll'; $status->type = 'poll';

View file

@ -22,6 +22,7 @@ use App\Services\WebfingerService;
use App\Status; use App\Status;
use App\UserFilter; use App\UserFilter;
use App\Util\ActivityPub\Helpers; use App\Util\ActivityPub\Helpers;
use App\Util\Lexer\Autolink;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -326,7 +327,6 @@ class DirectMessageController extends Controller
$status = new Status; $status = new Status;
$status->profile_id = $profile->id; $status->profile_id = $profile->id;
$status->caption = $msg; $status->caption = $msg;
$status->rendered = $msg;
$status->visibility = 'direct'; $status->visibility = 'direct';
$status->scope = 'direct'; $status->scope = 'direct';
$status->in_reply_to_profile_id = $recipient->id; $status->in_reply_to_profile_id = $recipient->id;
@ -636,7 +636,6 @@ class DirectMessageController extends Controller
$status = new Status; $status = new Status;
$status->profile_id = $profile->id; $status->profile_id = $profile->id;
$status->caption = null; $status->caption = null;
$status->rendered = null;
$status->visibility = 'direct'; $status->visibility = 'direct';
$status->scope = 'direct'; $status->scope = 'direct';
$status->in_reply_to_profile_id = $recipient->id; $status->in_reply_to_profile_id = $recipient->id;
@ -830,6 +829,11 @@ class DirectMessageController extends Controller
{ {
$profile = $dm->author; $profile = $dm->author;
$url = $dm->recipient->sharedInbox ?? $dm->recipient->inbox_url; $url = $dm->recipient->sharedInbox ?? $dm->recipient->inbox_url;
$status = $dm->status;
if (! $status) {
return;
}
$tags = [ $tags = [
[ [
@ -839,6 +843,8 @@ class DirectMessageController extends Controller
], ],
]; ];
$content = $status->caption ? Autolink::create()->autolink($status->caption) : null;
$body = [ $body = [
'@context' => [ '@context' => [
'https://w3id.org/security/v1', 'https://w3id.org/security/v1',
@ -854,7 +860,7 @@ class DirectMessageController extends Controller
'id' => $dm->status->url(), 'id' => $dm->status->url(),
'type' => 'Note', 'type' => 'Note',
'summary' => null, 'summary' => null,
'content' => $dm->status->rendered ?? $dm->status->caption, 'content' => $content,
'inReplyTo' => null, 'inReplyTo' => null,
'published' => $dm->status->created_at->toAtomString(), 'published' => $dm->status->created_at->toAtomString(),
'url' => $dm->status->url(), 'url' => $dm->status->url(),

View file

@ -2,102 +2,106 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use App\Models\Group; use App\Models\Group;
use App\Models\GroupPost; use App\Models\GroupPost;
use App\Status;
use App\Models\InstanceActor; use App\Models\InstanceActor;
use App\Services\MediaService; use App\Services\MediaService;
use App\Status;
use App\Util\Lexer\Autolink;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class GroupFederationController extends Controller class GroupFederationController extends Controller
{ {
public function getGroupObject(Request $request, $id) public function getGroupObject(Request $request, $id)
{ {
$group = Group::whereLocal(true)->whereActivitypub(true)->findOrFail($id); $group = Group::whereLocal(true)->whereActivitypub(true)->findOrFail($id);
$res = $this->showGroupObject($group); $res = $this->showGroupObject($group);
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
public function showGroupObject($group) return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
{ }
return Cache::remember('ap:groups:object:' . $group->id, 3600, function() use($group) {
return [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $group->url(),
'inbox' => $group->permalink('/inbox'),
'name' => $group->name,
'outbox' => $group->permalink('/outbox'),
'summary' => $group->description,
'type' => 'Group',
'attributedTo' => [
'type' => 'Person',
'id' => $group->admin->permalink()
],
// 'endpoints' => [
// 'sharedInbox' => config('app.url') . '/f/inbox'
// ],
'preferredUsername' => 'gid_' . $group->id,
'publicKey' => [
'id' => $group->permalink('#main-key'),
'owner' => $group->permalink(),
'publicKeyPem' => InstanceActor::first()->public_key,
],
'url' => $group->permalink()
];
if($group->metadata && isset($group->metadata['avatar'])) { public function showGroupObject($group)
$res['icon'] = [ {
'type' => 'Image', return Cache::remember('ap:groups:object:'.$group->id, 3600, function () use ($group) {
'url' => $group->metadata['avatar']['url'] return [
]; '@context' => 'https://www.w3.org/ns/activitystreams',
} 'id' => $group->url(),
'inbox' => $group->permalink('/inbox'),
'name' => $group->name,
'outbox' => $group->permalink('/outbox'),
'summary' => $group->description,
'type' => 'Group',
'attributedTo' => [
'type' => 'Person',
'id' => $group->admin->permalink(),
],
// 'endpoints' => [
// 'sharedInbox' => config('app.url') . '/f/inbox'
// ],
'preferredUsername' => 'gid_'.$group->id,
'publicKey' => [
'id' => $group->permalink('#main-key'),
'owner' => $group->permalink(),
'publicKeyPem' => InstanceActor::first()->public_key,
],
'url' => $group->permalink(),
];
if($group->metadata && isset($group->metadata['header'])) { if ($group->metadata && isset($group->metadata['avatar'])) {
$res['image'] = [ $res['icon'] = [
'type' => 'Image', 'type' => 'Image',
'url' => $group->metadata['header']['url'] 'url' => $group->metadata['avatar']['url'],
]; ];
} }
ksort($res);
return $res;
});
}
public function getStatusObject(Request $request, $gid, $sid) if ($group->metadata && isset($group->metadata['header'])) {
{ $res['image'] = [
$group = Group::whereLocal(true)->whereActivitypub(true)->findOrFail($gid); 'type' => 'Image',
$gp = GroupPost::whereGroupId($gid)->findOrFail($sid); 'url' => $group->metadata['header']['url'],
$status = Status::findOrFail($gp->status_id); ];
// permission check }
ksort($res);
$res = [ return $res;
'@context' => 'https://www.w3.org/ns/activitystreams', });
'id' => $gp->url(), }
'type' => 'Note', public function getStatusObject(Request $request, $gid, $sid)
{
$group = Group::whereLocal(true)->whereActivitypub(true)->findOrFail($gid);
$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(),
'summary' => null, 'type' => 'Note',
'content' => $status->rendered ?? $status->caption,
'inReplyTo' => null,
'published' => $status->created_at->toAtomString(), 'summary' => null,
'url' => $gp->url(), 'content' => $content,
'attributedTo' => $status->profile->permalink(), 'inReplyTo' => null,
'to' => [
'https://www.w3.org/ns/activitystreams#Public', 'published' => $status->created_at->toAtomString(),
$group->permalink('/followers'), 'url' => $gp->url(),
], 'attributedTo' => $status->profile->permalink(),
'cc' => [], 'to' => [
'sensitive' => (bool) $status->is_nsfw, 'https://www.w3.org/ns/activitystreams#Public',
'attachment' => MediaService::activitypub($status->id), $group->permalink('/followers'),
'target' => [ ],
'type' => 'Collection', 'cc' => [],
'id' => $group->permalink('/wall'), 'sensitive' => (bool) $status->is_nsfw,
'attributedTo' => $group->permalink() 'attachment' => MediaService::activitypub($status->id),
] 'target' => [
]; 'type' => 'Collection',
// ksort($res); 'id' => $group->permalink('/wall'),
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); 'attributedTo' => $group->permalink(),
} ],
];
// ksort($res);
return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
} }

View file

@ -2,442 +2,424 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Http\Request; use App\AccountInterstitial;
use App\{ use App\Bookmark;
AccountInterstitial, use App\DirectMessage;
Bookmark, use App\DiscoverCategory;
DirectMessage, use App\Follower;
DiscoverCategory,
Hashtag,
Follower,
Like,
Media,
MediaTag,
Notification,
Profile,
StatusHashtag,
Status,
User,
UserFilter,
};
use Auth,Cache;
use Illuminate\Support\Facades\Redis;
use Carbon\Carbon;
use League\Fractal;
use App\Transformer\Api\{
AccountTransformer,
StatusTransformer,
// StatusMediaContainerTransformer,
};
use App\Util\Media\Filter;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Jobs\ModPipeline\HandleSpammerPipeline; use App\Jobs\ModPipeline\HandleSpammerPipeline;
use League\Fractal\Serializer\ArraySerializer; use App\Profile;
use League\Fractal\Pagination\IlluminatePaginatorAdapter; use App\Services\BookmarkService;
use Illuminate\Validation\Rule; use App\Services\DiscoverService;
use Illuminate\Support\Str;
use App\Services\MediaTagService;
use App\Services\ModLogService; use App\Services\ModLogService;
use App\Services\PublicTimelineService; use App\Services\PublicTimelineService;
use App\Services\SnowflakeService;
use App\Services\StatusService; use App\Services\StatusService;
use App\Services\UserFilterService; use App\Services\UserFilterService;
use App\Services\DiscoverService; use App\Status; // StatusMediaContainerTransformer,
use App\Services\BookmarkService; use App\Transformer\Api\StatusTransformer;
use App\User;
use Auth;
use Cache;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redis;
use Illuminate\Validation\Rule;
use League\Fractal;
use League\Fractal\Serializer\ArraySerializer;
class InternalApiController extends Controller class InternalApiController extends Controller
{ {
protected $fractal; protected $fractal;
public function __construct() public function __construct()
{ {
$this->middleware('auth'); $this->middleware('auth');
$this->fractal = new Fractal\Manager(); $this->fractal = new Fractal\Manager;
$this->fractal->setSerializer(new ArraySerializer()); $this->fractal->setSerializer(new ArraySerializer);
} }
// deprecated v2 compose api // deprecated v2 compose api
public function compose(Request $request) public function compose(Request $request)
{ {
return redirect('/'); return redirect('/');
} }
// deprecated // deprecated
public function discover(Request $request) public function discover(Request $request) {}
{
return;
}
public function discoverPosts(Request $request) public function discoverPosts(Request $request)
{ {
$pid = $request->user()->profile_id; $pid = $request->user()->profile_id;
$filters = UserFilterService::filters($pid); $filters = UserFilterService::filters($pid);
$forYou = DiscoverService::getForYou(); $forYou = DiscoverService::getForYou();
$posts = $forYou->take(50)->map(function($post) { $posts = $forYou->take(50)->map(function ($post) {
return StatusService::get($post); return StatusService::get($post);
}) })
->filter(function($post) use($filters) { ->filter(function ($post) use ($filters) {
return $post && return $post &&
isset($post['account']) && isset($post['account']) &&
isset($post['account']['id']) && isset($post['account']['id']) &&
!in_array($post['account']['id'], $filters); ! in_array($post['account']['id'], $filters);
}) })
->take(12) ->take(12)
->values(); ->values();
return response()->json(compact('posts'));
}
public function directMessage(Request $request, $profileId, $threadId) return response()->json(compact('posts'));
{ }
$profile = Auth::user()->profile;
if($profileId != $profile->id) { public function directMessage(Request $request, $profileId, $threadId)
abort(403); {
} $profile = Auth::user()->profile;
$msg = DirectMessage::whereToId($profile->id) if ($profileId != $profile->id) {
->orWhere('from_id',$profile->id) abort(403);
->findOrFail($threadId); }
$thread = DirectMessage::with('status')->whereIn('to_id', [$profile->id, $msg->from_id]) $msg = DirectMessage::whereToId($profile->id)
->whereIn('from_id', [$profile->id,$msg->from_id]) ->orWhere('from_id', $profile->id)
->orderBy('created_at', 'asc') ->findOrFail($threadId);
->paginate(30);
return response()->json(compact('msg', 'profile', 'thread'), 200, [], JSON_PRETTY_PRINT); $thread = DirectMessage::with('status')->whereIn('to_id', [$profile->id, $msg->from_id])
} ->whereIn('from_id', [$profile->id, $msg->from_id])
->orderBy('created_at', 'asc')
->paginate(30);
public function statusReplies(Request $request, int $id) return response()->json(compact('msg', 'profile', 'thread'), 200, [], JSON_PRETTY_PRINT);
{ }
$this->validate($request, [
'limit' => 'nullable|int|min:1|max:6'
]);
$parent = Status::whereScope('public')->findOrFail($id);
$limit = $request->input('limit') ?? 3;
$children = Status::whereInReplyToId($parent->id)
->orderBy('created_at', 'desc')
->take($limit)
->get();
$resource = new Fractal\Resource\Collection($children, new StatusTransformer());
$res = $this->fractal->createData($resource)->toArray();
return response()->json($res); public function statusReplies(Request $request, int $id)
} {
$this->validate($request, [
'limit' => 'nullable|int|min:1|max:6',
]);
$parent = Status::whereScope('public')->findOrFail($id);
$limit = $request->input('limit') ?? 3;
$children = Status::whereInReplyToId($parent->id)
->orderBy('created_at', 'desc')
->take($limit)
->get();
$resource = new Fractal\Resource\Collection($children, new StatusTransformer);
$res = $this->fractal->createData($resource)->toArray();
public function stories(Request $request) return response()->json($res);
{ }
} public function stories(Request $request) {}
public function discoverCategories(Request $request) public function discoverCategories(Request $request)
{ {
$categories = DiscoverCategory::whereActive(true)->orderBy('order')->take(10)->get(); $categories = DiscoverCategory::whereActive(true)->orderBy('order')->take(10)->get();
$res = $categories->map(function($item) { $res = $categories->map(function ($item) {
return [ return [
'name' => $item->name, 'name' => $item->name,
'url' => $item->url(), 'url' => $item->url(),
'thumb' => $item->thumb() 'thumb' => $item->thumb(),
]; ];
}); });
return response()->json($res);
}
public function modAction(Request $request) return response()->json($res);
{ }
abort_unless(Auth::user()->is_admin, 400);
$this->validate($request, [
'action' => [
'required',
'string',
Rule::in([
'addcw',
'remcw',
'unlist',
'spammer'
])
],
'item_id' => 'required|integer|min:1',
'item_type' => [
'required',
'string',
Rule::in(['profile', 'status'])
]
]);
$action = $request->input('action'); public function modAction(Request $request)
$item_id = $request->input('item_id'); {
$item_type = $request->input('item_type'); abort_unless(Auth::user()->is_admin, 400);
$this->validate($request, [
'action' => [
'required',
'string',
Rule::in([
'addcw',
'remcw',
'unlist',
'spammer',
]),
],
'item_id' => 'required|integer|min:1',
'item_type' => [
'required',
'string',
Rule::in(['profile', 'status']),
],
]);
$status = Status::findOrFail($item_id); $action = $request->input('action');
$author = User::whereProfileId($status->profile_id)->first(); $item_id = $request->input('item_id');
abort_if($author && $author->is_admin, 422, 'Cannot moderate administrator accounts'); $item_type = $request->input('item_type');
switch($action) { $status = Status::findOrFail($item_id);
case 'addcw': $author = User::whereProfileId($status->profile_id)->first();
$status->is_nsfw = true; abort_if($author && $author->is_admin, 422, 'Cannot moderate administrator accounts');
$status->save();
ModLogService::boot()
->user(Auth::user())
->objectUid($status->profile->user_id)
->objectId($status->id)
->objectType('App\Status::class')
->action('admin.status.moderate')
->metadata([
'action' => 'cw',
'message' => 'Success!'
])
->accessLevel('admin')
->save();
if($status->uri == null) { switch ($action) {
$media = $status->media; case 'addcw':
$ai = new AccountInterstitial; $status->is_nsfw = true;
$ai->user_id = $status->profile->user_id; $status->save();
$ai->type = 'post.cw'; ModLogService::boot()
$ai->view = 'account.moderation.post.cw'; ->user(Auth::user())
$ai->item_type = 'App\Status'; ->objectUid($status->profile->user_id)
$ai->item_id = $status->id; ->objectId($status->id)
$ai->has_media = (bool) $media->count(); ->objectType('App\Status::class')
$ai->blurhash = $media->count() ? $media->first()->blurhash : null; ->action('admin.status.moderate')
$ai->meta = json_encode([ ->metadata([
'caption' => $status->caption, 'action' => 'cw',
'created_at' => $status->created_at, 'message' => 'Success!',
'type' => $status->type, ])
'url' => $status->url(), ->accessLevel('admin')
'is_nsfw' => $status->is_nsfw, ->save();
'scope' => $status->scope,
'reblog' => $status->reblog_of_id,
'likes_count' => $status->likes_count,
'reblogs_count' => $status->reblogs_count,
]);
$ai->save();
$u = $status->profile->user; if ($status->uri == null) {
$u->has_interstitial = true; $media = $status->media;
$u->save(); $ai = new AccountInterstitial;
} $ai->user_id = $status->profile->user_id;
break; $ai->type = 'post.cw';
$ai->view = 'account.moderation.post.cw';
$ai->item_type = 'App\Status';
$ai->item_id = $status->id;
$ai->has_media = (bool) $media->count();
$ai->blurhash = $media->count() ? $media->first()->blurhash : null;
$ai->meta = json_encode([
'caption' => $status->caption,
'created_at' => $status->created_at,
'type' => $status->type,
'url' => $status->url(),
'is_nsfw' => $status->is_nsfw,
'scope' => $status->scope,
'reblog' => $status->reblog_of_id,
'likes_count' => $status->likes_count,
'reblogs_count' => $status->reblogs_count,
]);
$ai->save();
case 'remcw': $u = $status->profile->user;
$status->is_nsfw = false; $u->has_interstitial = true;
$status->save(); $u->save();
ModLogService::boot() }
->user(Auth::user()) break;
->objectUid($status->profile->user_id)
->objectId($status->id)
->objectType('App\Status::class')
->action('admin.status.moderate')
->metadata([
'action' => 'remove_cw',
'message' => 'Success!'
])
->accessLevel('admin')
->save();
if($status->uri == null) {
$ai = AccountInterstitial::whereUserId($status->profile->user_id)
->whereType('post.cw')
->whereItemId($status->id)
->whereItemType('App\Status')
->first();
$ai->delete();
}
break;
case 'unlist': case 'remcw':
$status->scope = $status->visibility = 'unlisted'; $status->is_nsfw = false;
$status->save(); $status->save();
PublicTimelineService::del($status->id); ModLogService::boot()
ModLogService::boot() ->user(Auth::user())
->user(Auth::user()) ->objectUid($status->profile->user_id)
->objectUid($status->profile->user_id) ->objectId($status->id)
->objectId($status->id) ->objectType('App\Status::class')
->objectType('App\Status::class') ->action('admin.status.moderate')
->action('admin.status.moderate') ->metadata([
->metadata([ 'action' => 'remove_cw',
'action' => 'unlist', 'message' => 'Success!',
'message' => 'Success!' ])
]) ->accessLevel('admin')
->accessLevel('admin') ->save();
->save(); if ($status->uri == null) {
$ai = AccountInterstitial::whereUserId($status->profile->user_id)
->whereType('post.cw')
->whereItemId($status->id)
->whereItemType('App\Status')
->first();
$ai->delete();
}
break;
if($status->uri == null) { case 'unlist':
$media = $status->media; $status->scope = $status->visibility = 'unlisted';
$ai = new AccountInterstitial; $status->save();
$ai->user_id = $status->profile->user_id; PublicTimelineService::del($status->id);
$ai->type = 'post.unlist'; ModLogService::boot()
$ai->view = 'account.moderation.post.unlist'; ->user(Auth::user())
$ai->item_type = 'App\Status'; ->objectUid($status->profile->user_id)
$ai->item_id = $status->id; ->objectId($status->id)
$ai->has_media = (bool) $media->count(); ->objectType('App\Status::class')
$ai->blurhash = $media->count() ? $media->first()->blurhash : null; ->action('admin.status.moderate')
$ai->meta = json_encode([ ->metadata([
'caption' => $status->caption, 'action' => 'unlist',
'created_at' => $status->created_at, 'message' => 'Success!',
'type' => $status->type, ])
'url' => $status->url(), ->accessLevel('admin')
'is_nsfw' => $status->is_nsfw, ->save();
'scope' => $status->scope,
'reblog' => $status->reblog_of_id,
'likes_count' => $status->likes_count,
'reblogs_count' => $status->reblogs_count,
]);
$ai->save();
$u = $status->profile->user; if ($status->uri == null) {
$u->has_interstitial = true; $media = $status->media;
$u->save(); $ai = new AccountInterstitial;
} $ai->user_id = $status->profile->user_id;
break; $ai->type = 'post.unlist';
$ai->view = 'account.moderation.post.unlist';
$ai->item_type = 'App\Status';
$ai->item_id = $status->id;
$ai->has_media = (bool) $media->count();
$ai->blurhash = $media->count() ? $media->first()->blurhash : null;
$ai->meta = json_encode([
'caption' => $status->caption,
'created_at' => $status->created_at,
'type' => $status->type,
'url' => $status->url(),
'is_nsfw' => $status->is_nsfw,
'scope' => $status->scope,
'reblog' => $status->reblog_of_id,
'likes_count' => $status->likes_count,
'reblogs_count' => $status->reblogs_count,
]);
$ai->save();
case 'spammer': $u = $status->profile->user;
HandleSpammerPipeline::dispatch($status->profile); $u->has_interstitial = true;
ModLogService::boot() $u->save();
->user(Auth::user()) }
->objectUid($status->profile->user_id) break;
->objectId($status->id)
->objectType('App\User::class')
->action('admin.status.moderate')
->metadata([
'action' => 'spammer',
'message' => 'Success!'
])
->accessLevel('admin')
->save();
break;
}
StatusService::del($status->id, true); case 'spammer':
return ['msg' => 200]; HandleSpammerPipeline::dispatch($status->profile);
} ModLogService::boot()
->user(Auth::user())
->objectUid($status->profile->user_id)
->objectId($status->id)
->objectType('App\User::class')
->action('admin.status.moderate')
->metadata([
'action' => 'spammer',
'message' => 'Success!',
])
->accessLevel('admin')
->save();
break;
}
public function composePost(Request $request) StatusService::del($status->id, true);
{
abort(400, 'Endpoint deprecated');
}
public function bookmarks(Request $request) return ['msg' => 200];
{ }
$pid = $request->user()->profile_id;
$res = Bookmark::whereProfileId($pid)
->orderByDesc('created_at')
->simplePaginate(10)
->map(function($bookmark) use($pid) {
$status = StatusService::get($bookmark->status_id, false);
if(!$status) {
return false;
}
$status['bookmarked_at'] = str_replace('+00:00', 'Z', $bookmark->created_at->format(DATE_RFC3339_EXTENDED));
if($status) { public function composePost(Request $request)
BookmarkService::add($pid, $status['id']); {
} abort(400, 'Endpoint deprecated');
return $status; }
})
->filter(function($bookmark) {
return $bookmark && isset($bookmark['id']);
})
->values();
return response()->json($res); public function bookmarks(Request $request)
} {
$pid = $request->user()->profile_id;
$res = Bookmark::whereProfileId($pid)
->orderByDesc('created_at')
->simplePaginate(10)
->map(function ($bookmark) use ($pid) {
$status = StatusService::get($bookmark->status_id, false);
if (! $status) {
return false;
}
$status['bookmarked_at'] = str_replace('+00:00', 'Z', $bookmark->created_at->format(DATE_RFC3339_EXTENDED));
public function accountStatuses(Request $request, $id) if ($status) {
{ BookmarkService::add($pid, $status['id']);
$this->validate($request, [ }
'only_media' => 'nullable',
'pinned' => 'nullable',
'exclude_replies' => 'nullable',
'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'
]);
$profile = Profile::whereNull('status')->findOrFail($id); return $status;
})
->filter(function ($bookmark) {
return $bookmark && isset($bookmark['id']);
})
->values();
$limit = $request->limit ?? 9; return response()->json($res);
$max_id = $request->max_id; }
$min_id = $request->min_id;
$scope = $request->only_media == true ?
['photo', 'photo:album', 'video', 'video:album'] :
['photo', 'photo:album', 'video', 'video:album', 'share', 'reply'];
if($profile->is_private) { public function accountStatuses(Request $request, $id)
if(!Auth::check()) { {
return response()->json([]); $this->validate($request, [
} 'only_media' => 'nullable',
$pid = Auth::user()->profile->id; 'pinned' => 'nullable',
$following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) { 'exclude_replies' => 'nullable',
$following = Follower::whereProfileId($pid)->pluck('following_id'); 'max_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
return $following->push($pid)->toArray(); 'since_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
}); 'min_id' => 'nullable|integer|min:0|max:'.PHP_INT_MAX,
$visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : []; 'limit' => 'nullable|integer|min:1|max:24',
} 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'];
} else {
$visibility = ['public', 'unlisted'];
}
}
$dir = $min_id ? '>' : '<'; $profile = Profile::whereNull('status')->findOrFail($id);
$id = $min_id ?? $max_id;
$timeline = Status::select(
'id',
'uri',
'caption',
'rendered',
'profile_id',
'type',
'in_reply_to_id',
'reblog_of_id',
'is_nsfw',
'likes_count',
'reblogs_count',
'scope',
'local',
'created_at',
'updated_at'
)->whereProfileId($profile->id)
->whereIn('type', $scope)
->where('id', $dir, $id)
->whereIn('visibility', $visibility)
->latest()
->limit($limit)
->get();
$resource = new Fractal\Resource\Collection($timeline, new StatusTransformer()); $limit = $request->limit ?? 9;
$res = $this->fractal->createData($resource)->toArray(); $max_id = $request->max_id;
$min_id = $request->min_id;
$scope = $request->only_media == true ?
['photo', 'photo:album', 'video', 'video:album'] :
['photo', 'photo:album', 'video', 'video:album', 'share', 'reply'];
return response()->json($res); if ($profile->is_private) {
} if (! Auth::check()) {
return response()->json([]);
}
$pid = Auth::user()->profile->id;
$following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function () use ($pid) {
$following = Follower::whereProfileId($pid)->pluck('following_id');
public function remoteProfile(Request $request, $id) return $following->push($pid)->toArray();
{ });
return redirect('/i/web/profile/' . $id); $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');
public function remoteStatus(Request $request, $profileId, $statusId) return $following->push($pid)->toArray();
{ });
return redirect('/i/web/post/' . $statusId); $visibility = in_array($profile->id, $following) == true ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
} } else {
$visibility = ['public', 'unlisted'];
}
}
public function requestEmailVerification(Request $request) $dir = $min_id ? '>' : '<';
{ $id = $min_id ?? $max_id;
$pid = $request->user()->profile_id; $timeline = Status::select(
$exists = Redis::sismember('email:manual', $pid); 'id',
return view('account.email.request_verification', compact('exists')); 'uri',
} 'caption',
'profile_id',
'type',
'in_reply_to_id',
'reblog_of_id',
'is_nsfw',
'likes_count',
'reblogs_count',
'scope',
'local',
'created_at',
'updated_at'
)->whereProfileId($profile->id)
->whereIn('type', $scope)
->where('id', $dir, $id)
->whereIn('visibility', $visibility)
->latest()
->limit($limit)
->get();
public function requestEmailVerificationStore(Request $request) $resource = new Fractal\Resource\Collection($timeline, new StatusTransformer);
{ $res = $this->fractal->createData($resource)->toArray();
$pid = $request->user()->profile_id;
Redis::sadd('email:manual', $pid); return response()->json($res);
return redirect('/i/verify-email')->with(['status' => 'Successfully sent manual verification request!']); }
}
public function remoteProfile(Request $request, $id)
{
return redirect('/i/web/profile/'.$id);
}
public function remoteStatus(Request $request, $profileId, $statusId)
{
return redirect('/i/web/post/'.$statusId);
}
public function requestEmailVerification(Request $request)
{
$pid = $request->user()->profile_id;
$exists = Redis::sismember('email:manual', $pid);
return view('account.email.request_verification', compact('exists'));
}
public function requestEmailVerificationStore(Request $request)
{
$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,66 +2,65 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Status;
use Auth;
use DB;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\{
Profile,
Status,
};
use Auth, DB, Purify;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
class MicroController extends Controller class MicroController extends Controller
{ {
public function __construct() public function __construct()
{ {
$this->middleware('auth'); $this->middleware('auth');
} }
public function composeText(Request $request) public function composeText(Request $request)
{ {
$this->validate($request, [ $this->validate($request, [
'type' => [ 'type' => [
'required', 'required',
'string', 'string',
Rule::in(['text']) Rule::in(['text']),
], ],
'title' => 'nullable|string|max:140', 'title' => 'nullable|string|max:140',
'content' => 'required|string|max:500', 'content' => 'required|string|max:500',
'visibility' => [ 'visibility' => [
'required', 'required',
'string', 'string',
Rule::in([ Rule::in([
'public', 'public',
'unlisted', 'unlisted',
'private', 'private',
'draft' 'draft',
]) ]),
] ],
]); ]);
$profile = Auth::user()->profile; $profile = Auth::user()->profile;
$title = $request->input('title'); $title = $request->input('title');
$content = $request->input('content'); $content = $request->input('content');
$visibility = $request->input('visibility'); $visibility = $request->input('visibility');
$status = DB::transaction(function() use($profile, $content, $visibility, $title) { $status = DB::transaction(function () use ($profile, $content, $visibility, $title) {
$status = new Status; $status = new Status;
$status->type = 'text'; $status->type = 'text';
$status->profile_id = $profile->id; $status->profile_id = $profile->id;
$status->caption = strip_tags($content); $status->caption = strip_tags($content);
$status->rendered = Purify::clean($content); $status->is_nsfw = false;
$status->is_nsfw = false;
// TODO: remove deprecated visibility in favor of scope // TODO: remove deprecated visibility in favor of scope
$status->visibility = $visibility; $status->visibility = $visibility;
$status->scope = $visibility; $status->scope = $visibility;
$status->entities = json_encode(['title'=>$title]); $status->entities = json_encode(['title' => $title]);
$status->save(); $status->save();
return $status;
});
$fractal = new \League\Fractal\Manager(); return $status;
$fractal->setSerializer(new \League\Fractal\Serializer\ArraySerializer()); });
$s = new \League\Fractal\Resource\Item($status, new \App\Transformer\Api\StatusTransformer());
return $fractal->createData($s)->toArray(); $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

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

View file

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

View file

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

View file

@ -2,129 +2,122 @@
namespace App\Jobs\GroupPipeline; namespace App\Jobs\GroupPipeline;
use App\Notification;
use App\Hashtag; use App\Hashtag;
use App\Mention; use App\Mention;
use App\Profile;
use App\Status;
use App\StatusHashtag;
use App\Models\GroupPostHashtag;
use App\Models\GroupPost; use App\Models\GroupPost;
use Cache; use App\Models\GroupPostHashtag;
use App\Profile;
use App\Services\StatusService;
use App\Status;
use App\Util\Lexer\Autolink;
use App\Util\Lexer\Extractor;
use DB; use DB;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Redis;
use App\Services\MediaStorageService;
use App\Services\NotificationService;
use App\Services\StatusService;
use App\Util\Lexer\Autolink;
use App\Util\Lexer\Extractor;
class NewStatusPipeline implements ShouldQueue class NewStatusPipeline implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $status; protected $status;
protected $gp;
protected $tags;
protected $mentions;
public function __construct(Status $status, GroupPost $gp) protected $gp;
{
$this->status = $status;
$this->gp = $gp;
}
public function handle() protected $tags;
{
$status = $this->status;
$autolink = Autolink::create() protected $mentions;
->setAutolinkActiveUsersOnly(true)
->setBaseHashPath("/groups/{$status->group_id}/topics/") public function __construct(Status $status, GroupPost $gp)
->setBaseUserPath("/groups/{$status->group_id}/username/") {
->autolink($status->caption); $this->status = $status;
$this->gp = $gp;
}
public function handle()
{
$status = $this->status;
$autolink = Autolink::create()
->setAutolinkActiveUsersOnly(true)
->setBaseHashPath("/groups/{$status->group_id}/topics/")
->setBaseUserPath("/groups/{$status->group_id}/username/")
->autolink($status->caption);
$entities = Extractor::create()->extract($status->caption); $entities = Extractor::create()->extract($status->caption);
$status->entities = null;
$status->save();
$autolink = str_replace('/discover/tags/', '/groups/' . $status->group_id . '/topics/', $autolink); $this->tags = array_unique($entities['hashtags']);
$this->mentions = array_unique($entities['mentions']);
$status->rendered = nl2br($autolink); if (count($this->tags)) {
$status->entities = null; $this->storeHashtags();
$status->save(); }
$this->tags = array_unique($entities['hashtags']); if (count($this->mentions)) {
$this->mentions = array_unique($entities['mentions']); $this->storeMentions($this->mentions);
}
}
if(count($this->tags)) { protected function storeHashtags()
$this->storeHashtags(); {
} $tags = $this->tags;
$status = $this->status;
$gp = $this->gp;
if(count($this->mentions)) { foreach ($tags as $tag) {
$this->storeMentions($this->mentions); if (mb_strlen($tag) > 124) {
} continue;
} }
protected function storeHashtags() DB::transaction(function () use ($status, $tag, $gp) {
{ $slug = str_slug($tag, '-', false);
$tags = $this->tags; $hashtag = Hashtag::firstOrCreate(
$status = $this->status; ['name' => $tag, 'slug' => $slug]
$gp = $this->gp; );
GroupPostHashtag::firstOrCreate(
[
'group_id' => $status->group_id,
'group_post_id' => $gp->id,
'status_id' => $status->id,
'hashtag_id' => $hashtag->id,
'profile_id' => $status->profile_id,
]
);
foreach ($tags as $tag) { });
if(mb_strlen($tag) > 124) { }
continue;
}
DB::transaction(function () use ($status, $tag, $gp) { if (count($this->mentions)) {
$slug = str_slug($tag, '-', false); $this->storeMentions();
$hashtag = Hashtag::firstOrCreate( }
['name' => $tag, 'slug' => $slug] StatusService::del($status->id);
); }
GroupPostHashtag::firstOrCreate(
[
'group_id' => $status->group_id,
'group_post_id' => $gp->id,
'status_id' => $status->id,
'hashtag_id' => $hashtag->id,
'profile_id' => $status->profile_id,
]
);
}); protected function storeMentions()
} {
$mentions = $this->mentions;
$status = $this->status;
if(count($this->mentions)) { foreach ($mentions as $mention) {
$this->storeMentions(); $mentioned = Profile::whereUsername($mention)->first();
}
StatusService::del($status->id);
}
protected function storeMentions() if (empty($mentioned) || ! isset($mentioned->id)) {
{ continue;
$mentions = $this->mentions; }
$status = $this->status;
foreach ($mentions as $mention) { DB::transaction(function () use ($status, $mentioned) {
$mentioned = Profile::whereUsername($mention)->first(); $m = new Mention;
$m->status_id = $status->id;
$m->profile_id = $mentioned->id;
$m->save();
if (empty($mentioned) || !isset($mentioned->id)) { MentionPipeline::dispatch($status, $m);
continue; });
} }
StatusService::del($status->id);
DB::transaction(function () use ($status, $mentioned) { }
$m = new Mention();
$m->status_id = $status->id;
$m->profile_id = $mentioned->id;
$m->save();
MentionPipeline::dispatch($status, $m);
});
}
StatusService::del($status->id);
}
} }

View file

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

View file

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

View file

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

View file

@ -3,135 +3,133 @@
namespace App\Services\Status; namespace App\Services\Status;
use App\Media; use App\Media;
use App\ModLog;
use App\Status;
use App\Models\StatusEdit; use App\Models\StatusEdit;
use Purify; use App\ModLog;
use App\Util\Lexer\Autolink;
use App\Services\MediaService; use App\Services\MediaService;
use App\Services\MediaStorageService; use App\Services\MediaStorageService;
use App\Services\StatusService; use App\Services\StatusService;
use App\Status;
use Purify;
class UpdateStatusService class UpdateStatusService
{ {
public static function call(Status $status, $attributes) public static function call(Status $status, $attributes)
{ {
self::createPreviousEdit($status); self::createPreviousEdit($status);
self::updateMediaAttachements($status, $attributes); self::updateMediaAttachements($status, $attributes);
self::handleImmediateAttributes($status, $attributes); self::handleImmediateAttributes($status, $attributes);
self::createEdit($status, $attributes); self::createEdit($status, $attributes);
return StatusService::get($status->id); return StatusService::get($status->id);
} }
public static function updateMediaAttachements(Status $status, $attributes) public static function updateMediaAttachements(Status $status, $attributes)
{ {
$count = $status->media()->count(); $count = $status->media()->count();
if($count === 0 || $count === 1) { if ($count === 0 || $count === 1) {
return; return;
} }
$oids = $status->media()->orderBy('order')->pluck('id')->map(function($m) { return (string) $m; }); $oids = $status->media()->orderBy('order')->pluck('id')->map(function ($m) {
$nids = collect($attributes['media_ids']); return (string) $m;
});
$nids = collect($attributes['media_ids']);
if($oids->toArray() === $nids->toArray()) { if ($oids->toArray() === $nids->toArray()) {
return; return;
} }
foreach($oids->diff($nids)->values()->toArray() as $mid) { foreach ($oids->diff($nids)->values()->toArray() as $mid) {
$media = Media::find($mid); $media = Media::find($mid);
if(!$media) { if (! $media) {
continue; continue;
} }
$media->status_id = null; $media->status_id = null;
$media->save(); $media->save();
MediaStorageService::delete($media, true); MediaStorageService::delete($media, true);
} }
$nids->each(function($nid, $idx) { $nids->each(function ($nid, $idx) {
$media = Media::find($nid); $media = Media::find($nid);
if(!$media) { if (! $media) {
return; return;
} }
$media->order = $idx; $media->order = $idx;
$media->save(); $media->save();
}); });
MediaService::del($status->id); MediaService::del($status->id);
} }
public static function handleImmediateAttributes(Status $status, $attributes) public static function handleImmediateAttributes(Status $status, $attributes)
{ {
if(isset($attributes['status'])) { if (isset($attributes['status'])) {
$cleaned = Purify::clean($attributes['status']); $cleaned = Purify::clean($attributes['status']);
$status->caption = $cleaned; $status->caption = $cleaned;
$status->rendered = nl2br(Autolink::create()->autolink($cleaned)); } else {
} else { $status->caption = null;
$status->caption = null; }
$status->rendered = null; if (isset($attributes['sensitive'])) {
} if ($status->is_nsfw != (bool) $attributes['sensitive'] &&
if(isset($attributes['sensitive'])) { (bool) $attributes['sensitive'] == false) {
if($status->is_nsfw != (bool) $attributes['sensitive'] && $exists = ModLog::whereObjectType('App\Status::class')
(bool) $attributes['sensitive'] == false) ->whereObjectId($status->id)
{ ->whereAction('admin.status.moderate')
$exists = ModLog::whereObjectType('App\Status::class') ->exists();
->whereObjectId($status->id) if (! $exists) {
->whereAction('admin.status.moderate') $status->is_nsfw = (bool) $attributes['sensitive'];
->exists(); }
if(!$exists) { } else {
$status->is_nsfw = (bool) $attributes['sensitive']; $status->is_nsfw = (bool) $attributes['sensitive'];
} }
} else { }
$status->is_nsfw = (bool) $attributes['sensitive']; if (isset($attributes['spoiler_text'])) {
} $status->cw_summary = Purify::clean($attributes['spoiler_text']);
} } else {
if(isset($attributes['spoiler_text'])) { $status->cw_summary = null;
$status->cw_summary = Purify::clean($attributes['spoiler_text']); }
} else { if (isset($attributes['location'])) {
$status->cw_summary = null; if (isset($attributes['location']['id'])) {
} $status->place_id = $attributes['location']['id'];
if(isset($attributes['location'])) { } else {
if (isset($attributes['location']['id'])) { $status->place_id = null;
$status->place_id = $attributes['location']['id']; }
} else { }
$status->place_id = null; if ($status->cw_summary && ! $status->is_nsfw) {
} $status->cw_summary = null;
} }
if($status->cw_summary && !$status->is_nsfw) { $status->edited_at = now();
$status->cw_summary = null; $status->save();
} StatusService::del($status->id);
$status->edited_at = now(); }
$status->save();
StatusService::del($status->id);
}
public static function createPreviousEdit(Status $status) public static function createPreviousEdit(Status $status)
{ {
if(!$status->edits()->count()) { if (! $status->edits()->count()) {
StatusEdit::create([ StatusEdit::create([
'status_id' => $status->id, 'status_id' => $status->id,
'profile_id' => $status->profile_id, 'profile_id' => $status->profile_id,
'caption' => $status->caption, 'caption' => $status->caption,
'spoiler_text' => $status->cw_summary, 'spoiler_text' => $status->cw_summary,
'is_nsfw' => $status->is_nsfw, 'is_nsfw' => $status->is_nsfw,
'ordered_media_attachment_ids' => $status->media()->orderBy('order')->pluck('id')->toArray(), 'ordered_media_attachment_ids' => $status->media()->orderBy('order')->pluck('id')->toArray(),
'created_at' => $status->created_at 'created_at' => $status->created_at,
]); ]);
} }
} }
public static function createEdit(Status $status, $attributes) public static function createEdit(Status $status, $attributes)
{ {
$cleaned = isset($attributes['status']) ? Purify::clean($attributes['status']) : null; $cleaned = isset($attributes['status']) ? Purify::clean($attributes['status']) : null;
$spoiler_text = isset($attributes['spoiler_text']) ? Purify::clean($attributes['spoiler_text']) : null; $spoiler_text = isset($attributes['spoiler_text']) ? Purify::clean($attributes['spoiler_text']) : null;
$sensitive = isset($attributes['sensitive']) ? $attributes['sensitive'] : null; $sensitive = isset($attributes['sensitive']) ? $attributes['sensitive'] : null;
$mids = $status->media()->count() ? $status->media()->orderBy('order')->pluck('id')->toArray() : null; $mids = $status->media()->count() ? $status->media()->orderBy('order')->pluck('id')->toArray() : null;
StatusEdit::create([ StatusEdit::create([
'status_id' => $status->id, 'status_id' => $status->id,
'profile_id' => $status->profile_id, 'profile_id' => $status->profile_id,
'caption' => $cleaned, 'caption' => $cleaned,
'spoiler_text' => $spoiler_text, 'spoiler_text' => $spoiler_text,
'is_nsfw' => $sensitive, 'is_nsfw' => $sensitive,
'ordered_media_attachment_ids' => $mids 'ordered_media_attachment_ids' => $mids,
]); ]);
} }
} }

View file

@ -694,8 +694,7 @@ class Helpers
$status->url = isset($res['url']) ? $res['url'] : $url; $status->url = isset($res['url']) ? $res['url'] : $url;
$status->uri = isset($res['url']) ? $res['url'] : $url; $status->uri = isset($res['url']) ? $res['url'] : $url;
$status->object_url = $id; $status->object_url = $id;
$status->caption = strip_tags($res['content']); $status->caption = strip_tags(Purify::clean($res['content']));
$status->rendered = Purify::clean($res['content']);
$status->created_at = Carbon::parse($ts)->tz('UTC'); $status->created_at = Carbon::parse($ts)->tz('UTC');
$status->in_reply_to_id = null; $status->in_reply_to_id = null;
$status->local = false; $status->local = false;

View file

@ -438,7 +438,6 @@ class Inbox
$status = new Status; $status = new Status;
$status->profile_id = $actor->id; $status->profile_id = $actor->id;
$status->caption = $msgText; $status->caption = $msgText;
$status->rendered = $msg;
$status->visibility = 'direct'; $status->visibility = 'direct';
$status->scope = 'direct'; $status->scope = 'direct';
$status->url = $activity['id']; $status->url = $activity['id'];
@ -1081,7 +1080,6 @@ class Inbox
$status->uri = $url; $status->uri = $url;
$status->object_url = $url; $status->object_url = $url;
$status->caption = $text; $status->caption = $text;
$status->rendered = $text;
$status->scope = 'direct'; $status->scope = 'direct';
$status->visibility = 'direct'; $status->visibility = 'direct';
$status->in_reply_to_profile_id = $story->profile_id; $status->in_reply_to_profile_id = $story->profile_id;
@ -1199,7 +1197,6 @@ class Inbox
$status->profile_id = $actorProfile->id; $status->profile_id = $actorProfile->id;
$status->type = 'story:reply'; $status->type = 'story:reply';
$status->caption = $text; $status->caption = $text;
$status->rendered = $text;
$status->url = $url; $status->url = $url;
$status->uri = $url; $status->uri = $url;
$status->object_url = $url; $status->object_url = $url;