<?php namespace App; use Auth, Cache, Hashids, Storage; use Illuminate\Database\Eloquent\Model; use App\HasSnowflakePrimary; use App\Http\Controllers\StatusController; use Illuminate\Database\Eloquent\SoftDeletes; use App\Models\Poll; use App\Services\AccountService; use App\Services\StatusService; use App\Models\StatusEdit; use Illuminate\Support\Str; class Status extends Model { use HasSnowflakePrimary, SoftDeletes; /** * Indicates if the IDs are auto-incrementing. * * @var bool */ public $incrementing = false; /** * The attributes that should be mutated to dates. * * @var array */ protected $casts = [ 'deleted_at' => 'datetime', 'edited_at' => 'datetime' ]; protected $guarded = []; const STATUS_TYPES = [ 'text', 'photo', 'photo:album', 'video', 'video:album', 'photo:video:album', 'share', 'reply', 'story', 'story:reply', 'story:reaction', 'story:live', 'loop' ]; const MAX_MENTIONS = 20; const MAX_HASHTAGS = 60; const MAX_LINKS = 5; public function profile() { return $this->belongsTo(Profile::class); } public function media() { return $this->hasMany(Media::class); } public function firstMedia() { return $this->hasMany(Media::class)->orderBy('order', 'asc')->first(); } public function viewType() { if($this->type) { return $this->type; } return $this->setType(); } public function setType() { if(in_array($this->type, self::STATUS_TYPES)) { return $this->type; } $mimes = $this->media->pluck('mime')->toArray(); $type = StatusController::mimeTypeCheck($mimes); if($type) { $this->type = $type; $this->save(); return $type; } } public function thumb($showNsfw = false) { $entity = StatusService::get($this->id, false); if(!$entity || !isset($entity['media_attachments']) || empty($entity['media_attachments'])) { return url(Storage::url('public/no-preview.png')); } if((!isset($entity['sensitive']) || $entity['sensitive']) && !$showNsfw) { return url(Storage::url('public/no-preview.png')); } if(!isset($entity['visibility']) || !in_array($entity['visibility'], ['public', 'unlisted'])) { return url(Storage::url('public/no-preview.png')); } return collect($entity['media_attachments']) ->filter(fn($media) => $media['type'] == 'image' && in_array($media['mime'], ['image/jpeg', 'image/png'])) ->map(function($media) { if(!Str::endsWith($media['preview_url'], ['no-preview.png', 'no-preview.jpg'])) { return $media['preview_url']; } return $media['url']; }) ->first() ?? url(Storage::url('public/no-preview.png')); } public function url($forceLocal = false) { if($this->uri) { return $forceLocal ? "/i/web/post/_/{$this->profile_id}/{$this->id}" : $this->uri; } else { $id = $this->id; $account = AccountService::get($this->profile_id, true); if(!$account || !isset($account['username'])) { return '/404'; } $path = url(config('app.url')."/p/{$account['username']}/{$id}"); return $path; } } public function permalink($suffix = '/activity') { $id = $this->id; $username = $this->profile->username; $path = config('app.url')."/p/{$username}/{$id}{$suffix}"; return url($path); } public function editUrl() { return $this->url().'/edit'; } public function mediaUrl() { $media = $this->firstMedia(); $path = $media->media_path; $hash = is_null($media->processed_at) ? md5('unprocessed') : md5($media->created_at); $url = $media->cdn_url ? $media->cdn_url . "?v={$hash}" : url(Storage::url($path)."?v={$hash}"); return $url; } public function likes() { return $this->hasMany(Like::class); } public function liked() : bool { if(!Auth::check()) { return false; } $pid = Auth::user()->profile_id; return Like::select('status_id', 'profile_id') ->whereStatusId($this->id) ->whereProfileId($pid) ->exists(); } public function likedBy() { return $this->hasManyThrough( Profile::class, Like::class, 'status_id', 'id', 'id', 'profile_id' ); } public function comments() { return $this->hasMany(self::class, 'in_reply_to_id'); } public function bookmarked() { if (!Auth::check()) { return false; } $profile = Auth::user()->profile; return Bookmark::whereProfileId($profile->id)->whereStatusId($this->id)->count(); } public function shares() { return $this->hasMany(self::class, 'reblog_of_id'); } public function shared() : bool { if(!Auth::check()) { return false; } $pid = Auth::user()->profile_id; return $this->select('profile_id', 'reblog_of_id') ->whereProfileId($pid) ->whereReblogOfId($this->id) ->exists(); } public function sharedBy() { return $this->hasManyThrough( Profile::class, Status::class, 'reblog_of_id', 'id', 'id', 'profile_id' ); } public function parent() { $parent = $this->in_reply_to_id ?? $this->reblog_of_id; if (!empty($parent)) { return $this->findOrFail($parent); } else { return false; } } public function conversation() { return $this->hasOne(Conversation::class); } public function hashtags() { return $this->hasManyThrough( Hashtag::class, StatusHashtag::class, 'status_id', 'id', 'id', 'hashtag_id' ); } public function mentions() { return $this->hasManyThrough( Profile::class, Mention::class, 'status_id', 'id', 'id', 'profile_id' ); } public function reportUrl() { return route('report.form')."?type=post&id={$this->id}"; } public function toActivityStream() { $media = $this->media; $mediaCollection = []; foreach ($media as $image) { $mediaCollection[] = [ 'type' => 'Link', 'href' => $image->url(), 'mediaType' => $image->mime, ]; } $obj = [ '@context' => 'https://www.w3.org/ns/activitystreams', 'type' => 'Image', 'name' => null, 'url' => $mediaCollection, ]; return $obj; } public function recentComments() { return $this->comments()->orderBy('created_at', 'desc')->take(3); } public function toActivityPubObject() { if($this->local == false) { return; } $profile = $this->profile; $to = $this->scopeToAudience('to'); $cc = $this->scopeToAudience('cc'); return [ '@context' => 'https://www.w3.org/ns/activitystreams', 'id' => $this->permalink(), 'type' => 'Create', 'actor' => $profile->permalink(), 'published' => str_replace('+00:00', 'Z', $this->created_at->format(DATE_RFC3339_EXTENDED)), 'to' => $to, 'cc' => $cc, 'object' => [ 'id' => $this->url(), 'type' => 'Note', 'summary' => null, 'inReplyTo' => null, 'published' => str_replace('+00:00', 'Z', $this->created_at->format(DATE_RFC3339_EXTENDED)), 'url' => $this->url(), 'attributedTo' => $this->profile->url(), 'to' => $to, 'cc' => $cc, 'sensitive' => (bool) $this->is_nsfw, 'content' => $this->rendered, 'attachment' => $this->media->map(function($media) { return [ 'type' => 'Document', 'mediaType' => $media->mime, 'url' => $media->url(), 'name' => null ]; })->toArray() ] ]; } public function scopeToAudience($audience) { if(!in_array($audience, ['to', 'cc']) || $this->local == false) { return; } $res = []; $res['to'] = []; $res['cc'] = []; $scope = $this->scope; $mentions = $this->mentions->map(function ($mention) { return $mention->permalink(); })->toArray(); if($this->in_reply_to_id != null) { $parent = $this->parent(); if($parent) { $mentions = array_merge([$parent->profile->permalink()], $mentions); } } switch ($scope) { case 'public': $res['to'] = [ "https://www.w3.org/ns/activitystreams#Public" ]; $res['cc'] = array_merge([$this->profile->permalink('/followers')], $mentions); break; case 'unlisted': $res['to'] = array_merge([$this->profile->permalink('/followers')], $mentions); $res['cc'] = [ "https://www.w3.org/ns/activitystreams#Public" ]; break; case 'private': $res['to'] = array_merge([$this->profile->permalink('/followers')], $mentions); $res['cc'] = []; break; // TODO: Update scope when DMs are supported case 'direct': $res['to'] = []; $res['cc'] = []; break; } return $res[$audience]; } public function place() { return $this->belongsTo(Place::class); } public function directMessage() { return $this->hasOne(DirectMessage::class); } public function poll() { return $this->hasOne(Poll::class); } public function edits() { return $this->hasMany(StatusEdit::class); } }