Update StatusService, add non-public option and improve cache invalidation

This commit is contained in:
Daniel Supernault 2021-07-26 19:02:11 -06:00
parent ee0028bc57
commit 15c4fdd90c
No known key found for this signature in database
GPG key ID: 0DEF1C662C9033F7
2 changed files with 355 additions and 349 deletions

View file

@ -21,10 +21,14 @@ class StatusService {
return self::CACHE_KEY . $id; return self::CACHE_KEY . $id;
} }
public static function get($id) public static function get($id, $publicOnly = true)
{ {
return Cache::remember(self::key($id), now()->addDays(7), function() use($id) { return Cache::remember(self::key($id), now()->addDays(7), function() use($id, $publicOnly) {
$status = Status::whereScope('public')->find($id); if($publicOnly) {
$status = Status::whereScope('public')->find($id);
} else {
$status = Status::whereIn('scope', ['public', 'private', 'unlisted'])->find($id);
}
if(!$status) { if(!$status) {
return null; return null;
} }
@ -37,6 +41,8 @@ class StatusService {
public static function del($id) public static function del($id)
{ {
Cache::forget('pf:services:sh:id:' . $id);
Cache::forget('status:transformer:media:attachments:' . $id);
PublicTimelineService::rem($id); PublicTimelineService::rem($id);
return Cache::forget(self::key($id)); return Cache::forget(self::key($id));
} }

View file

@ -10,408 +10,408 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class Status extends Model class Status extends Model
{ {
use HasSnowflakePrimary, SoftDeletes; use HasSnowflakePrimary, SoftDeletes;
/** /**
* Indicates if the IDs are auto-incrementing. * Indicates if the IDs are auto-incrementing.
* *
* @var bool * @var bool
*/ */
public $incrementing = false; public $incrementing = false;
/** /**
* The attributes that should be mutated to dates. * The attributes that should be mutated to dates.
* *
* @var array * @var array
*/ */
protected $dates = ['deleted_at']; protected $dates = ['deleted_at'];
protected $fillable = ['profile_id', 'visibility', 'in_reply_to_id', 'reblog_of_id', 'type']; protected $fillable = ['profile_id', 'visibility', 'in_reply_to_id', 'reblog_of_id', 'type'];
const STATUS_TYPES = [ const STATUS_TYPES = [
'text', 'text',
'photo', 'photo',
'photo:album', 'photo:album',
'video', 'video',
'video:album', 'video:album',
'photo:video:album', 'photo:video:album',
'share', 'share',
'reply', 'reply',
'story', 'story',
'story:reply', 'story:reply',
'story:reaction', 'story:reaction',
'story:live', 'story:live',
'loop' 'loop'
]; ];
const MAX_MENTIONS = 5; const MAX_MENTIONS = 5;
const MAX_HASHTAGS = 30; const MAX_HASHTAGS = 30;
const MAX_LINKS = 0; const MAX_LINKS = 0;
public function profile() public function profile()
{ {
return $this->belongsTo(Profile::class); return $this->belongsTo(Profile::class);
} }
public function media() public function media()
{ {
return $this->hasMany(Media::class); return $this->hasMany(Media::class);
} }
public function firstMedia() public function firstMedia()
{ {
return $this->hasMany(Media::class)->orderBy('order', 'asc')->first(); return $this->hasMany(Media::class)->orderBy('order', 'asc')->first();
} }
public function viewType() public function viewType()
{ {
if($this->type) { if($this->type) {
return $this->type; return $this->type;
} }
return $this->setType(); return $this->setType();
} }
public function setType() public function setType()
{ {
if(in_array($this->type, self::STATUS_TYPES)) { if(in_array($this->type, self::STATUS_TYPES)) {
return $this->type; return $this->type;
} }
$mimes = $this->media->pluck('mime')->toArray(); $mimes = $this->media->pluck('mime')->toArray();
$type = StatusController::mimeTypeCheck($mimes); $type = StatusController::mimeTypeCheck($mimes);
if($type) { if($type) {
$this->type = $type; $this->type = $type;
$this->save(); $this->save();
return $type; return $type;
} }
} }
public function thumb($showNsfw = false) public function thumb($showNsfw = false)
{ {
$key = $showNsfw ? 'status:thumb:nsfw1'.$this->id : 'status:thumb:nsfw0'.$this->id; $key = $showNsfw ? 'status:thumb:nsfw1'.$this->id : 'status:thumb:nsfw0'.$this->id;
return Cache::remember($key, now()->addMinutes(15), function() use ($showNsfw) { return Cache::remember($key, now()->addMinutes(15), function() use ($showNsfw) {
$type = $this->type ?? $this->setType(); $type = $this->type ?? $this->setType();
$is_nsfw = !$showNsfw ? $this->is_nsfw : false; $is_nsfw = !$showNsfw ? $this->is_nsfw : false;
if ($this->media->count() == 0 || $is_nsfw || !in_array($type,['photo', 'photo:album', 'video'])) { if ($this->media->count() == 0 || $is_nsfw || !in_array($type,['photo', 'photo:album', 'video'])) {
return url(Storage::url('public/no-preview.png')); return url(Storage::url('public/no-preview.png'));
} }
return url(Storage::url($this->firstMedia()->thumbnail_path)); return url(Storage::url($this->firstMedia()->thumbnail_path));
}); });
} }
public function url() public function url($forceLocal = false)
{ {
if($this->uri) { if($this->uri) {
return $this->uri; return $forceLocal ? "/i/web/post/_/{$this->profile_id}/{$this->id}" : $this->uri;
} else { } else {
$id = $this->id; $id = $this->id;
$username = $this->profile->username; $username = $this->profile->username;
$path = url(config('app.url')."/p/{$username}/{$id}"); $path = url(config('app.url')."/p/{$username}/{$id}");
return $path; return $path;
} }
} }
public function permalink($suffix = '/activity') public function permalink($suffix = '/activity')
{ {
$id = $this->id; $id = $this->id;
$username = $this->profile->username; $username = $this->profile->username;
$path = config('app.url')."/p/{$username}/{$id}{$suffix}"; $path = config('app.url')."/p/{$username}/{$id}{$suffix}";
return url($path); return url($path);
} }
public function editUrl() public function editUrl()
{ {
return $this->url().'/edit'; return $this->url().'/edit';
} }
public function mediaUrl() public function mediaUrl()
{ {
$media = $this->firstMedia(); $media = $this->firstMedia();
$path = $media->media_path; $path = $media->media_path;
$hash = is_null($media->processed_at) ? md5('unprocessed') : md5($media->created_at); $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}"); $url = $media->cdn_url ? $media->cdn_url . "?v={$hash}" : url(Storage::url($path)."?v={$hash}");
return $url; return $url;
} }
public function likes() public function likes()
{ {
return $this->hasMany(Like::class); return $this->hasMany(Like::class);
} }
public function liked() : bool public function liked() : bool
{ {
if(!Auth::check()) { if(!Auth::check()) {
return false; return false;
} }
$pid = Auth::user()->profile_id; $pid = Auth::user()->profile_id;
return Like::select('status_id', 'profile_id') return Like::select('status_id', 'profile_id')
->whereStatusId($this->id) ->whereStatusId($this->id)
->whereProfileId($pid) ->whereProfileId($pid)
->exists(); ->exists();
} }
public function likedBy() public function likedBy()
{ {
return $this->hasManyThrough( return $this->hasManyThrough(
Profile::class, Profile::class,
Like::class, Like::class,
'status_id', 'status_id',
'id', 'id',
'id', 'id',
'profile_id' 'profile_id'
); );
} }
public function comments() public function comments()
{ {
return $this->hasMany(self::class, 'in_reply_to_id'); return $this->hasMany(self::class, 'in_reply_to_id');
} }
public function bookmarked() public function bookmarked()
{ {
if (!Auth::check()) { if (!Auth::check()) {
return false; return false;
} }
$profile = Auth::user()->profile; $profile = Auth::user()->profile;
return Bookmark::whereProfileId($profile->id)->whereStatusId($this->id)->count(); return Bookmark::whereProfileId($profile->id)->whereStatusId($this->id)->count();
} }
public function shares() public function shares()
{ {
return $this->hasMany(self::class, 'reblog_of_id'); return $this->hasMany(self::class, 'reblog_of_id');
} }
public function shared() : bool public function shared() : bool
{ {
if(!Auth::check()) { if(!Auth::check()) {
return false; return false;
} }
$pid = Auth::user()->profile_id; $pid = Auth::user()->profile_id;
return $this->select('profile_id', 'reblog_of_id') return $this->select('profile_id', 'reblog_of_id')
->whereProfileId($pid) ->whereProfileId($pid)
->whereReblogOfId($this->id) ->whereReblogOfId($this->id)
->exists(); ->exists();
} }
public function sharedBy() public function sharedBy()
{ {
return $this->hasManyThrough( return $this->hasManyThrough(
Profile::class, Profile::class,
Status::class, Status::class,
'reblog_of_id', 'reblog_of_id',
'id', 'id',
'id', 'id',
'profile_id' 'profile_id'
); );
} }
public function parent() public function parent()
{ {
$parent = $this->in_reply_to_id ?? $this->reblog_of_id; $parent = $this->in_reply_to_id ?? $this->reblog_of_id;
if (!empty($parent)) { if (!empty($parent)) {
return $this->findOrFail($parent); return $this->findOrFail($parent);
} else { } else {
return false; return false;
} }
} }
public function conversation() public function conversation()
{ {
return $this->hasOne(Conversation::class); return $this->hasOne(Conversation::class);
} }
public function hashtags() public function hashtags()
{ {
return $this->hasManyThrough( return $this->hasManyThrough(
Hashtag::class, Hashtag::class,
StatusHashtag::class, StatusHashtag::class,
'status_id', 'status_id',
'id', 'id',
'id', 'id',
'hashtag_id' 'hashtag_id'
); );
} }
public function mentions() public function mentions()
{ {
return $this->hasManyThrough( return $this->hasManyThrough(
Profile::class, Profile::class,
Mention::class, Mention::class,
'status_id', 'status_id',
'id', 'id',
'id', 'id',
'profile_id' 'profile_id'
); );
} }
public function reportUrl() public function reportUrl()
{ {
return route('report.form')."?type=post&id={$this->id}"; return route('report.form')."?type=post&id={$this->id}";
} }
public function toActivityStream() public function toActivityStream()
{ {
$media = $this->media; $media = $this->media;
$mediaCollection = []; $mediaCollection = [];
foreach ($media as $image) { foreach ($media as $image) {
$mediaCollection[] = [ $mediaCollection[] = [
'type' => 'Link', 'type' => 'Link',
'href' => $image->url(), 'href' => $image->url(),
'mediaType' => $image->mime, 'mediaType' => $image->mime,
]; ];
} }
$obj = [ $obj = [
'@context' => 'https://www.w3.org/ns/activitystreams', '@context' => 'https://www.w3.org/ns/activitystreams',
'type' => 'Image', 'type' => 'Image',
'name' => null, 'name' => null,
'url' => $mediaCollection, 'url' => $mediaCollection,
]; ];
return $obj; return $obj;
} }
public function replyToText() public function replyToText()
{ {
$actorName = $this->profile->username; $actorName = $this->profile->username;
return "{$actorName} ".__('notification.commented'); return "{$actorName} ".__('notification.commented');
} }
public function replyToHtml() public function replyToHtml()
{ {
$actorName = $this->profile->username; $actorName = $this->profile->username;
$actorUrl = $this->profile->url(); $actorUrl = $this->profile->url();
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> ". return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> ".
__('notification.commented'); __('notification.commented');
} }
public function shareToText() public function shareToText()
{ {
$actorName = $this->profile->username; $actorName = $this->profile->username;
return "{$actorName} ".__('notification.shared'); return "{$actorName} ".__('notification.shared');
} }
public function shareToHtml() public function shareToHtml()
{ {
$actorName = $this->profile->username; $actorName = $this->profile->username;
$actorUrl = $this->profile->url(); $actorUrl = $this->profile->url();
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> ". return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> ".
__('notification.shared'); __('notification.shared');
} }
public function recentComments() public function recentComments()
{ {
return $this->comments()->orderBy('created_at', 'desc')->take(3); return $this->comments()->orderBy('created_at', 'desc')->take(3);
} }
public function toActivityPubObject() public function toActivityPubObject()
{ {
if($this->local == false) { if($this->local == false) {
return; return;
} }
$profile = $this->profile; $profile = $this->profile;
$to = $this->scopeToAudience('to'); $to = $this->scopeToAudience('to');
$cc = $this->scopeToAudience('cc'); $cc = $this->scopeToAudience('cc');
return [ return [
'@context' => 'https://www.w3.org/ns/activitystreams', '@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $this->permalink(), 'id' => $this->permalink(),
'type' => 'Create', 'type' => 'Create',
'actor' => $profile->permalink(), 'actor' => $profile->permalink(),
'published' => $this->created_at->format('c'), 'published' => $this->created_at->format('c'),
'to' => $to, 'to' => $to,
'cc' => $cc, 'cc' => $cc,
'object' => [ 'object' => [
'id' => $this->url(), 'id' => $this->url(),
'type' => 'Note', 'type' => 'Note',
'summary' => null, 'summary' => null,
'inReplyTo' => null, 'inReplyTo' => null,
'published' => $this->created_at->format('c'), 'published' => $this->created_at->format('c'),
'url' => $this->url(), 'url' => $this->url(),
'attributedTo' => $this->profile->url(), 'attributedTo' => $this->profile->url(),
'to' => $to, 'to' => $to,
'cc' => $cc, 'cc' => $cc,
'sensitive' => (bool) $this->is_nsfw, 'sensitive' => (bool) $this->is_nsfw,
'content' => $this->rendered, 'content' => $this->rendered,
'attachment' => $this->media->map(function($media) { 'attachment' => $this->media->map(function($media) {
return [ return [
'type' => 'Document', 'type' => 'Document',
'mediaType' => $media->mime, 'mediaType' => $media->mime,
'url' => $media->url(), 'url' => $media->url(),
'name' => null 'name' => null
]; ];
})->toArray() })->toArray()
] ]
]; ];
} }
public function scopeToAudience($audience) public function scopeToAudience($audience)
{ {
if(!in_array($audience, ['to', 'cc']) || $this->local == false) { if(!in_array($audience, ['to', 'cc']) || $this->local == false) {
return; return;
} }
$res = []; $res = [];
$res['to'] = []; $res['to'] = [];
$res['cc'] = []; $res['cc'] = [];
$scope = $this->scope; $scope = $this->scope;
$mentions = $this->mentions->map(function ($mention) { $mentions = $this->mentions->map(function ($mention) {
return $mention->permalink(); return $mention->permalink();
})->toArray(); })->toArray();
if($this->in_reply_to_id != null) { if($this->in_reply_to_id != null) {
$parent = $this->parent(); $parent = $this->parent();
if($parent) { if($parent) {
$mentions = array_merge([$parent->profile->permalink()], $mentions); $mentions = array_merge([$parent->profile->permalink()], $mentions);
} }
} }
switch ($scope) { switch ($scope) {
case 'public': case 'public':
$res['to'] = [ $res['to'] = [
"https://www.w3.org/ns/activitystreams#Public" "https://www.w3.org/ns/activitystreams#Public"
]; ];
$res['cc'] = array_merge([$this->profile->permalink('/followers')], $mentions); $res['cc'] = array_merge([$this->profile->permalink('/followers')], $mentions);
break; break;
case 'unlisted': case 'unlisted':
$res['to'] = array_merge([$this->profile->permalink('/followers')], $mentions); $res['to'] = array_merge([$this->profile->permalink('/followers')], $mentions);
$res['cc'] = [ $res['cc'] = [
"https://www.w3.org/ns/activitystreams#Public" "https://www.w3.org/ns/activitystreams#Public"
]; ];
break; break;
case 'private': case 'private':
$res['to'] = array_merge([$this->profile->permalink('/followers')], $mentions); $res['to'] = array_merge([$this->profile->permalink('/followers')], $mentions);
$res['cc'] = []; $res['cc'] = [];
break; break;
// TODO: Update scope when DMs are supported // TODO: Update scope when DMs are supported
case 'direct': case 'direct':
$res['to'] = []; $res['to'] = [];
$res['cc'] = []; $res['cc'] = [];
break; break;
} }
return $res[$audience]; return $res[$audience];
} }
public function place() public function place()
{ {
return $this->belongsTo(Place::class); return $this->belongsTo(Place::class);
} }
public function directMessage() public function directMessage()
{ {
return $this->hasOne(DirectMessage::class); return $this->hasOne(DirectMessage::class);
} }
} }