Browse Source

Update Status caption logic, stop storing duplicate html caption in db and defer to cached StatusService rendering

Daniel Supernault 7 months ago
parent
commit
9eeb7b6741

+ 27 - 23
app/Console/Commands/TransformImports.php

@@ -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
 {
@@ -35,23 +34,24 @@ class TransformImports extends Command
      */
     public function handle()
     {
-        if(!config('import.instagram.enabled')) {
+        if (! config('import.instagram.enabled')) {
             return;
         }
 
         $ips = ImportPost::whereNull('status_id')->where('skip_missing_media', '!=', true)->take(500)->get();
 
-        if(!$ips->count()) {
+        if (! $ips->count()) {
             return;
         }
 
-        foreach($ips as $ip) {
+        foreach ($ips as $ip) {
             $id = $ip->user_id;
             $pid = $ip->profile_id;
             $profile = Profile::find($pid);
-            if(!$profile) {
+            if (! $profile) {
                 $ip->skip_missing_media = true;
                 $ip->save();
+
                 continue;
             }
 
@@ -63,39 +63,43 @@ class TransformImports extends Command
                 ->where('creation_day', $ip->creation_day)
                 ->exists();
 
-            if($exists == true) {
+            if ($exists == true) {
                 $ip->skip_missing_media = true;
                 $ip->save();
+
                 continue;
             }
 
             $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->save();
+
                 continue;
             }
 
-            if(Storage::exists('imports/' . $id . '/' . $ip->filename) === false) {
+            if (Storage::exists('imports/'.$id.'/'.$ip->filename) === false) {
                 ImportService::clearAttempts($profile->id);
                 ImportService::getPostCount($profile->id, true);
                 $ip->skip_missing_media = true;
                 $ip->save();
+
                 continue;
             }
 
             $missingMedia = false;
-            foreach($ip->media as $ipm) {
+            foreach ($ip->media as $ipm) {
                 $fileName = last(explode('/', $ipm['uri']));
-                $og = 'imports/' . $id . '/' . $fileName;
-                if(!Storage::exists($og)) {
+                $og = 'imports/'.$id.'/'.$fileName;
+                if (! Storage::exists($og)) {
                     $missingMedia = true;
                 }
             }
 
-            if($missingMedia === true) {
+            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';
@@ -112,20 +115,21 @@ class TransformImports extends Command
             $status->created_at = now()->parse($ip->creation_date);
             $status->save();
 
-            foreach($ip->media as $ipm) {
+            foreach ($ip->media as $ipm) {
                 $fileName = last(explode('/', $ipm['uri']));
                 $ext = last(explode('.', $fileName));
                 $basePath = MediaPathService::get($profile);
-                $og = 'imports/' . $id . '/' . $fileName;
-                if(!Storage::exists($og)) {
+                $og = 'imports/'.$id.'/'.$fileName;
+                if (! Storage::exists($og)) {
                     $ip->skip_missing_media = true;
                     $ip->save();
+
                     continue;
                 }
                 $size = Storage::size($og);
                 $mime = Storage::mimeType($og);
-                $newFile = Str::random(40) . '.' . $ext;
-                $np = $basePath . '/' . $newFile;
+                $newFile = Str::random(40).'.'.$ext;
+                $np = $basePath.'/'.$newFile;
                 Storage::move($og, $np);
                 $media = new Media;
                 $media->profile_id = $pid;

+ 1 - 4
app/Http/Controllers/Api/ApiV1Controller.php

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

+ 2 - 4
app/Http/Controllers/Api/ApiV1Dot1Controller.php

@@ -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
 {
@@ -1293,14 +1293,12 @@ class ApiV1Dot1Controller extends Controller
             return [];
         }
 
-        $content = strip_tags($request->input('status'));
-        $rendered = Autolink::create()->autolink($content);
+        $content = $request->filled('status') ? strip_tags(Purify::clean($request->input('status'))) : null;
         $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->profile_id = $user->profile_id;
         $status->is_nsfw = $cw;
         $status->cw_summary = $spoilerText;

+ 6 - 8
app/Http/Controllers/CommentController.php

@@ -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,10 @@ 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->in_reply_to_id = $status->id;
             $reply->in_reply_to_profile_id = $status->profile_id;
             $reply->scope = $scope;
@@ -76,9 +74,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,

+ 5 - 9
app/Http/Controllers/ComposeController.php

@@ -25,7 +25,6 @@ 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;
@@ -43,8 +42,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 +111,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 +150,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;
@@ -571,7 +570,6 @@ class ComposeController extends Controller
         }
 
         $status->caption = strip_tags($request->caption);
-        $status->rendered = Autolink::create()->autolink($status->caption);
         $status->scope = 'draft';
         $status->visibility = 'draft';
         $status->profile_id = $profile->id;
@@ -693,7 +691,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 +803,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';

+ 9 - 3
app/Http/Controllers/DirectMessageController.php

@@ -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;
 
@@ -326,7 +327,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;
@@ -636,7 +636,6 @@ 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;
@@ -830,6 +829,11 @@ class DirectMessageController extends Controller
     {
         $profile = $dm->author;
         $url = $dm->recipient->sharedInbox ?? $dm->recipient->inbox_url;
+        $status = $dm->status;
+
+        if (! $status) {
+            return;
+        }
 
         $tags = [
             [
@@ -839,6 +843,8 @@ class DirectMessageController extends Controller
             ],
         ];
 
+        $content = $status->caption ? Autolink::create()->autolink($status->caption) : null;
+
         $body = [
             '@context' => [
                 'https://w3id.org/security/v1',
@@ -854,7 +860,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(),

+ 87 - 83
app/Http/Controllers/GroupFederationController.php

@@ -2,102 +2,106 @@
 
 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
 {
-	public function getGroupObject(Request $request, $id)
-	{
-		$group = Group::whereLocal(true)->whereActivitypub(true)->findOrFail($id);
-		$res = $this->showGroupObject($group);
-		return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
-	}
+    public function getGroupObject(Request $request, $id)
+    {
+        $group = Group::whereLocal(true)->whereActivitypub(true)->findOrFail($id);
+        $res = $this->showGroupObject($group);
+
+        return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+    }
+
+    public function showGroupObject($group)
+    {
+        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(),
+            ];
 
-	public function showGroupObject($group)
-	{
-		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'])) {
+                $res['icon'] = [
+                    'type' => 'Image',
+                    'url' => $group->metadata['avatar']['url'],
+                ];
+            }
 
-			if($group->metadata && isset($group->metadata['avatar'])) {
-				$res['icon'] = [
-					'type' => 'Image',
-					'url' => $group->metadata['avatar']['url']
-				];
-			}
+            if ($group->metadata && isset($group->metadata['header'])) {
+                $res['image'] = [
+                    'type' => 'Image',
+                    'url' => $group->metadata['header']['url'],
+                ];
+            }
+            ksort($res);
 
-			if($group->metadata && isset($group->metadata['header'])) {
-				$res['image'] = [
-					'type' => 'Image',
-					'url' => $group->metadata['header']['url']
-				];
-			}
-			ksort($res);
-			return $res;
-		});
-	}
+            return $res;
+        });
+    }
 
-	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
+    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(),
 
-		$res = [
-			'@context' => 'https://www.w3.org/ns/activitystreams',
-			'id' => $gp->url(),
+            'type' => 'Note',
 
-			'type' => 'Note',
+            'summary' => null,
+            'content' => $content,
+            'inReplyTo' => null,
 
-			'summary'   => null,
-			'content'   => $status->rendered ?? $status->caption,
-			'inReplyTo' => null,
+            'published' => $status->created_at->toAtomString(),
+            'url' => $gp->url(),
+            'attributedTo' => $status->profile->permalink(),
+            'to' => [
+                'https://www.w3.org/ns/activitystreams#Public',
+                $group->permalink('/followers'),
+            ],
+            'cc' => [],
+            'sensitive' => (bool) $status->is_nsfw,
+            'attachment' => MediaService::activitypub($status->id),
+            'target' => [
+                'type' => 'Collection',
+                'id' => $group->permalink('/wall'),
+                'attributedTo' => $group->permalink(),
+            ],
+        ];
 
-			'published'    => $status->created_at->toAtomString(),
-			'url'          => $gp->url(),
-			'attributedTo' => $status->profile->permalink(),
-			'to'           => [
-				'https://www.w3.org/ns/activitystreams#Public',
-				$group->permalink('/followers'),
-			],
-			'cc' => [],
-			'sensitive'        => (bool) $status->is_nsfw,
-			'attachment'       => MediaService::activitypub($status->id),
-			'target' => [
-				'type' => 'Collection',
-				'id' => $group->permalink('/wall'),
-				'attributedTo' => $group->permalink()
-			]
-		];
-		// ksort($res);
-		return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
-	}
+        // ksort($res);
+        return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
+    }
 }

+ 412 - 430
app/Http/Controllers/InternalApiController.php

@@ -2,442 +2,424 @@
 
 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
 {
-	protected $fractal;
-
-	public function __construct()
-	{
-		$this->middleware('auth');
-		$this->fractal = new Fractal\Manager();
-		$this->fractal->setSerializer(new ArraySerializer());
-	}
-
-	// deprecated v2 compose api
-	public function compose(Request $request)
-	{
-		return redirect('/');
-	}
-
-	// deprecated
-	public function discover(Request $request)
-	{
-		return;
-	}
-
-	public function discoverPosts(Request $request)
-	{
-		$pid = $request->user()->profile_id;
-		$filters = UserFilterService::filters($pid);
-		$forYou = DiscoverService::getForYou();
-		$posts = $forYou->take(50)->map(function($post) {
-			return StatusService::get($post);
-		})
-		->filter(function($post) use($filters) {
-			return $post &&
-				isset($post['account']) &&
-				isset($post['account']['id']) &&
-				!in_array($post['account']['id'], $filters);
-		})
-		->take(12)
-		->values();
-		return response()->json(compact('posts'));
-	}
-
-	public function directMessage(Request $request, $profileId, $threadId)
-	{
-		$profile = Auth::user()->profile;
-
-		if($profileId != $profile->id) {
-			abort(403);
-		}
-
-		$msg = DirectMessage::whereToId($profile->id)
-			->orWhere('from_id',$profile->id)
-			->findOrFail($threadId);
-
-		$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);
-
-		return response()->json(compact('msg', 'profile', 'thread'), 200, [], JSON_PRETTY_PRINT);
-	}
-
-	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();
-
-		return response()->json($res);
-	}
-
-	public function stories(Request $request)
-	{
-
-	}
-
-	public function discoverCategories(Request $request)
-	{
-		$categories = DiscoverCategory::whereActive(true)->orderBy('order')->take(10)->get();
-		$res = $categories->map(function($item) {
-			return [
-				'name' => $item->name,
-				'url' => $item->url(),
-				'thumb' => $item->thumb()
-			];
-		});
-		return response()->json($res);
-	}
-
-	public function modAction(Request $request)
-	{
-		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');
-		$item_id = $request->input('item_id');
-		$item_type = $request->input('item_type');
-
-		$status = Status::findOrFail($item_id);
-		$author = User::whereProfileId($status->profile_id)->first();
-		abort_if($author && $author->is_admin, 422, 'Cannot moderate administrator accounts');
-
-		switch($action) {
-			case 'addcw':
-				$status->is_nsfw = true;
-				$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) {
-					$media = $status->media;
-					$ai = new AccountInterstitial;
-					$ai->user_id = $status->profile->user_id;
-					$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();
-
-					$u = $status->profile->user;
-					$u->has_interstitial = true;
-					$u->save();
-				}
-			break;
-
-			case 'remcw':
-				$status->is_nsfw = false;
-				$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' => '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':
-				$status->scope = $status->visibility = 'unlisted';
-				$status->save();
-				PublicTimelineService::del($status->id);
-				ModLogService::boot()
-					->user(Auth::user())
-					->objectUid($status->profile->user_id)
-					->objectId($status->id)
-					->objectType('App\Status::class')
-					->action('admin.status.moderate')
-					->metadata([
-						'action' => 'unlist',
-						'message' => 'Success!'
-					])
-					->accessLevel('admin')
-					->save();
-
-				if($status->uri == null) {
-					$media = $status->media;
-					$ai = new AccountInterstitial;
-					$ai->user_id = $status->profile->user_id;
-					$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();
-
-					$u = $status->profile->user;
-					$u->has_interstitial = true;
-					$u->save();
-				}
-			break;
-
-			case 'spammer':
-				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;
-		}
-
-		StatusService::del($status->id, true);
-		return ['msg' => 200];
-	}
-
-	public function composePost(Request $request)
-	{
-		abort(400, 'Endpoint deprecated');
-	}
-
-	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));
-
-				if($status) {
-					BookmarkService::add($pid, $status['id']);
-				}
-				return $status;
-			})
-			->filter(function($bookmark) {
-				return $bookmark && isset($bookmark['id']);
-			})
-			->values();
-
-		return response()->json($res);
-	}
-
-	public function accountStatuses(Request $request, $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);
-
-		$limit = $request->limit ?? 9;
-		$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) {
-			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');
-				return $following->push($pid)->toArray();
-			});
-			$visibility = true == in_array($profile->id, $following) ? ['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'];
-			} else {
-				$visibility = ['public', 'unlisted'];
-			}
-		}
-
-		$dir = $min_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());
-		$res = $this->fractal->createData($resource)->toArray();
-
-		return response()->json($res);
-	}
-
-	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!']);
-	}
+    protected $fractal;
+
+    public function __construct()
+    {
+        $this->middleware('auth');
+        $this->fractal = new Fractal\Manager;
+        $this->fractal->setSerializer(new ArraySerializer);
+    }
+
+    // deprecated v2 compose api
+    public function compose(Request $request)
+    {
+        return redirect('/');
+    }
+
+    // deprecated
+    public function discover(Request $request) {}
+
+    public function discoverPosts(Request $request)
+    {
+        $pid = $request->user()->profile_id;
+        $filters = UserFilterService::filters($pid);
+        $forYou = DiscoverService::getForYou();
+        $posts = $forYou->take(50)->map(function ($post) {
+            return StatusService::get($post);
+        })
+            ->filter(function ($post) use ($filters) {
+                return $post &&
+                    isset($post['account']) &&
+                    isset($post['account']['id']) &&
+                    ! in_array($post['account']['id'], $filters);
+            })
+            ->take(12)
+            ->values();
+
+        return response()->json(compact('posts'));
+    }
+
+    public function directMessage(Request $request, $profileId, $threadId)
+    {
+        $profile = Auth::user()->profile;
+
+        if ($profileId != $profile->id) {
+            abort(403);
+        }
+
+        $msg = DirectMessage::whereToId($profile->id)
+            ->orWhere('from_id', $profile->id)
+            ->findOrFail($threadId);
+
+        $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);
+
+        return response()->json(compact('msg', 'profile', 'thread'), 200, [], JSON_PRETTY_PRINT);
+    }
+
+    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();
+
+        return response()->json($res);
+    }
+
+    public function stories(Request $request) {}
+
+    public function discoverCategories(Request $request)
+    {
+        $categories = DiscoverCategory::whereActive(true)->orderBy('order')->take(10)->get();
+        $res = $categories->map(function ($item) {
+            return [
+                'name' => $item->name,
+                'url' => $item->url(),
+                'thumb' => $item->thumb(),
+            ];
+        });
+
+        return response()->json($res);
+    }
+
+    public function modAction(Request $request)
+    {
+        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');
+        $item_id = $request->input('item_id');
+        $item_type = $request->input('item_type');
+
+        $status = Status::findOrFail($item_id);
+        $author = User::whereProfileId($status->profile_id)->first();
+        abort_if($author && $author->is_admin, 422, 'Cannot moderate administrator accounts');
+
+        switch ($action) {
+            case 'addcw':
+                $status->is_nsfw = true;
+                $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) {
+                    $media = $status->media;
+                    $ai = new AccountInterstitial;
+                    $ai->user_id = $status->profile->user_id;
+                    $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();
+
+                    $u = $status->profile->user;
+                    $u->has_interstitial = true;
+                    $u->save();
+                }
+                break;
+
+            case 'remcw':
+                $status->is_nsfw = false;
+                $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' => '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':
+                $status->scope = $status->visibility = 'unlisted';
+                $status->save();
+                PublicTimelineService::del($status->id);
+                ModLogService::boot()
+                    ->user(Auth::user())
+                    ->objectUid($status->profile->user_id)
+                    ->objectId($status->id)
+                    ->objectType('App\Status::class')
+                    ->action('admin.status.moderate')
+                    ->metadata([
+                        'action' => 'unlist',
+                        'message' => 'Success!',
+                    ])
+                    ->accessLevel('admin')
+                    ->save();
+
+                if ($status->uri == null) {
+                    $media = $status->media;
+                    $ai = new AccountInterstitial;
+                    $ai->user_id = $status->profile->user_id;
+                    $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();
+
+                    $u = $status->profile->user;
+                    $u->has_interstitial = true;
+                    $u->save();
+                }
+                break;
+
+            case 'spammer':
+                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;
+        }
+
+        StatusService::del($status->id, true);
+
+        return ['msg' => 200];
+    }
+
+    public function composePost(Request $request)
+    {
+        abort(400, 'Endpoint deprecated');
+    }
+
+    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));
+
+                if ($status) {
+                    BookmarkService::add($pid, $status['id']);
+                }
+
+                return $status;
+            })
+            ->filter(function ($bookmark) {
+                return $bookmark && isset($bookmark['id']);
+            })
+            ->values();
+
+        return response()->json($res);
+    }
+
+    public function accountStatuses(Request $request, $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);
+
+        $limit = $request->limit ?? 9;
+        $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) {
+            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');
+
+                return $following->push($pid)->toArray();
+            });
+            $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 = in_array($profile->id, $following) == true ? ['public', 'unlisted', 'private'] : ['public', 'unlisted'];
+            } else {
+                $visibility = ['public', 'unlisted'];
+            }
+        }
+
+        $dir = $min_id ? '>' : '<';
+        $id = $min_id ?? $max_id;
+        $timeline = Status::select(
+            'id',
+            '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();
+
+        $resource = new Fractal\Resource\Collection($timeline, new StatusTransformer);
+        $res = $this->fractal->createData($resource)->toArray();
+
+        return response()->json($res);
+    }
+
+    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!']);
+    }
 }

+ 52 - 53
app/Http/Controllers/MicroController.php

@@ -2,66 +2,65 @@
 
 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
 {
-	public function __construct()
-	{
-		$this->middleware('auth');
-	}
+    public function __construct()
+    {
+        $this->middleware('auth');
+    }
 
-	public function composeText(Request $request)
-	{
-		$this->validate($request, [
-			'type' => [
-				'required',
-				'string',
-				Rule::in(['text'])
-			],
-			'title' => 'nullable|string|max:140',
-			'content' => 'required|string|max:500',
-			'visibility' => [
-				'required',
-				'string',
-				Rule::in([
-					'public',
-					'unlisted',
-					'private',
-					'draft'
-				])
-			]
-		]);
-		$profile = Auth::user()->profile;
-		$title = $request->input('title');
-		$content = $request->input('content');
-		$visibility = $request->input('visibility');
+    public function composeText(Request $request)
+    {
+        $this->validate($request, [
+            'type' => [
+                'required',
+                'string',
+                Rule::in(['text']),
+            ],
+            'title' => 'nullable|string|max:140',
+            'content' => 'required|string|max:500',
+            'visibility' => [
+                'required',
+                'string',
+                Rule::in([
+                    'public',
+                    'unlisted',
+                    'private',
+                    'draft',
+                ]),
+            ],
+        ]);
+        $profile = Auth::user()->profile;
+        $title = $request->input('title');
+        $content = $request->input('content');
+        $visibility = $request->input('visibility');
 
-		$status = DB::transaction(function() use($profile, $content, $visibility, $title) {
-			$status = new Status;
-			$status->type = 'text';
-			$status->profile_id = $profile->id;
-			$status->caption = strip_tags($content);
-			$status->rendered = Purify::clean($content);
-			$status->is_nsfw = false;
+        $status = DB::transaction(function () use ($profile, $content, $visibility, $title) {
+            $status = new Status;
+            $status->type = 'text';
+            $status->profile_id = $profile->id;
+            $status->caption = strip_tags($content);
+            $status->is_nsfw = false;
 
-			// TODO: remove deprecated visibility in favor of scope
-			$status->visibility = $visibility;
-			$status->scope = $visibility;
-			$status->entities = json_encode(['title'=>$title]);
-			$status->save();
-			return $status;
-		});
+            // TODO: remove deprecated visibility in favor of scope
+            $status->visibility = $visibility;
+            $status->scope = $visibility;
+            $status->entities = json_encode(['title' => $title]);
+            $status->save();
 
-		$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();
-	}
+            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);
+
+        return $fractal->createData($s)->toArray();
+    }
 }

+ 11 - 2
app/Http/Controllers/SearchController.php

@@ -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(),
             ]];

+ 1 - 2
app/Http/Controllers/Stories/StoryApiV1Controller.php

@@ -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;

+ 1 - 3
app/Http/Controllers/StoryComposeController.php

@@ -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;

+ 99 - 106
app/Jobs/GroupPipeline/NewStatusPipeline.php

@@ -2,129 +2,122 @@
 
 namespace App\Jobs\GroupPipeline;
 
-use App\Notification;
 use App\Hashtag;
 use App\Mention;
+use App\Models\GroupPost;
+use App\Models\GroupPostHashtag;
 use App\Profile;
+use App\Services\StatusService;
 use App\Status;
-use App\StatusHashtag;
-use App\Models\GroupPostHashtag;
-use App\Models\GroupPost;
-use Cache;
+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;
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
 
-	protected $status;
-	protected $gp;
-	protected $tags;
-	protected $mentions;
+    protected $status;
 
-	public function __construct(Status $status, GroupPost $gp)
-	{
-		$this->status = $status;
-		$this->gp = $gp;
-	}
+    protected $gp;
 
-	public function handle()
-	{
-		$status = $this->status;
+    protected $tags;
 
-		$autolink = Autolink::create()
-			->setAutolinkActiveUsersOnly(true)
-			->setBaseHashPath("/groups/{$status->group_id}/topics/")
-			->setBaseUserPath("/groups/{$status->group_id}/username/")
-			->autolink($status->caption);
+    protected $mentions;
 
-        $entities = Extractor::create()->extract($status->caption);
+    public function __construct(Status $status, GroupPost $gp)
+    {
+        $this->status = $status;
+        $this->gp = $gp;
+    }
 
-		$autolink = str_replace('/discover/tags/', '/groups/' . $status->group_id . '/topics/', $autolink);
-
-		$status->rendered = nl2br($autolink);
-		$status->entities = null;
-		$status->save();
-
-		$this->tags = array_unique($entities['hashtags']);
-		$this->mentions = array_unique($entities['mentions']);
-
-		if(count($this->tags)) {
-			$this->storeHashtags();
-		}
-
-		if(count($this->mentions)) {
-			$this->storeMentions($this->mentions);
-		}
-	}
-
-	protected function storeHashtags()
-	{
-		$tags = $this->tags;
-		$status = $this->status;
-		$gp = $this->gp;
-
-		foreach ($tags as $tag) {
-			if(mb_strlen($tag) > 124) {
-				continue;
-			}
-
-			DB::transaction(function () use ($status, $tag, $gp) {
-				$slug = str_slug($tag, '-', false);
-				$hashtag = Hashtag::firstOrCreate(
-					['name' => $tag, 'slug' => $slug]
-				);
-				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,
-					]
-				);
-
-			});
-		}
-
-		if(count($this->mentions)) {
-			$this->storeMentions();
-		}
-		StatusService::del($status->id);
-	}
-
-	protected function storeMentions()
-	{
-		$mentions = $this->mentions;
-		$status = $this->status;
-
-		foreach ($mentions as $mention) {
-			$mentioned = Profile::whereUsername($mention)->first();
-
-			if (empty($mentioned) || !isset($mentioned->id)) {
-				continue;
-			}
-
-			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);
-	}
+    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);
+        $status->entities = null;
+        $status->save();
+
+        $this->tags = array_unique($entities['hashtags']);
+        $this->mentions = array_unique($entities['mentions']);
+
+        if (count($this->tags)) {
+            $this->storeHashtags();
+        }
+
+        if (count($this->mentions)) {
+            $this->storeMentions($this->mentions);
+        }
+    }
+
+    protected function storeHashtags()
+    {
+        $tags = $this->tags;
+        $status = $this->status;
+        $gp = $this->gp;
+
+        foreach ($tags as $tag) {
+            if (mb_strlen($tag) > 124) {
+                continue;
+            }
+
+            DB::transaction(function () use ($status, $tag, $gp) {
+                $slug = str_slug($tag, '-', false);
+                $hashtag = Hashtag::firstOrCreate(
+                    ['name' => $tag, 'slug' => $slug]
+                );
+                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,
+                    ]
+                );
+
+            });
+        }
+
+        if (count($this->mentions)) {
+            $this->storeMentions();
+        }
+        StatusService::del($status->id);
+    }
+
+    protected function storeMentions()
+    {
+        $mentions = $this->mentions;
+        $status = $this->status;
+
+        foreach ($mentions as $mention) {
+            $mentioned = Profile::whereUsername($mention)->first();
+
+            if (empty($mentioned) || ! isset($mentioned->id)) {
+                continue;
+            }
+
+            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);
+    }
 }

+ 1 - 6
app/Jobs/StatusPipeline/StatusEntityLexer.php

@@ -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();

+ 1 - 2
app/Jobs/StatusPipeline/StatusRemoteUpdatePipeline.php

@@ -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'])) {

+ 113 - 115
app/Services/Status/UpdateStatusService.php

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

+ 1 - 2
app/Util/ActivityPub/Helpers.php

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

+ 0 - 3
app/Util/ActivityPub/Inbox.php

@@ -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;