mirror of https://github.com/pixelfed/pixelfed
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
515 lines
16 KiB
PHP
515 lines
16 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use App\HasSnowflakePrimary;
|
|
use App\Http\Controllers\StatusController;
|
|
use App\Services\AccountService;
|
|
use App\Services\StatusService;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
|
|
/**
|
|
* @property int $id
|
|
* @property string|null $uri
|
|
* @property string|null $caption
|
|
* @property string|null $rendered
|
|
* @property int|null $profile_id
|
|
* @property string|null $type
|
|
* @property int|null $in_reply_to_id
|
|
* @property int|null $reblog_of_id
|
|
* @property string|null $url
|
|
* @property int $is_nsfw
|
|
* @property string $scope
|
|
* @property string $visibility
|
|
* @property int $reply
|
|
* @property int $likes_count
|
|
* @property int $reblogs_count
|
|
* @property string|null $language
|
|
* @property int|null $conversation_id
|
|
* @property int $local
|
|
* @property int|null $application_id
|
|
* @property int|null $in_reply_to_profile_id
|
|
* @property string|null $entities
|
|
* @property Carbon|null $created_at
|
|
* @property Carbon|null $updated_at
|
|
* @property Carbon|null $deleted_at
|
|
* @property string|null $cw_summary
|
|
* @property int|null $reply_count
|
|
* @property int $comments_disabled
|
|
* @property int|null $place_id
|
|
* @property string|null $object_url
|
|
* @property Carbon|null $edited_at
|
|
* @property int|null $trendable
|
|
* @property string|null $media_ids
|
|
* @property int|null $pinned_order
|
|
* @property int|null $group_id
|
|
* @property int|null $quote_policy
|
|
* @property-read Collection<int, Status> $comments
|
|
* @property-read int|null $comments_count
|
|
* @property-read Conversation|null $conversation
|
|
* @property-read DirectMessage|null $directMessage
|
|
* @property-read Collection<int, StatusEdit> $edits
|
|
* @property-read int|null $edits_count
|
|
* @property-read Collection<int, Hashtag> $hashtags
|
|
* @property-read int|null $hashtags_count
|
|
* @property-read Collection<int, Profile> $likedBy
|
|
* @property-read int|null $liked_by_count
|
|
* @property-read Collection<int, Like> $likes
|
|
* @property-read Collection<int, Media> $media
|
|
* @property-read int|null $media_count
|
|
* @property-read Collection<int, Profile> $mentions
|
|
* @property-read int|null $mentions_count
|
|
* @property-read Place|null $place
|
|
* @property-read Poll|null $poll
|
|
* @property-read Profile|null $profile
|
|
* @property-read Collection<int, Profile> $sharedBy
|
|
* @property-read int|null $shared_by_count
|
|
* @property-read Collection<int, Status> $shares
|
|
* @property-read int|null $shares_count
|
|
*
|
|
* @method static \Database\Factories\StatusFactory factory($count = null, $state = [])
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status newModelQuery()
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status newQuery()
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status onlyTrashed()
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status query()
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status toAudience()
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereApplicationId($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereCaption($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereCommentsDisabled($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereConversationId($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereCreatedAt($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereCwSummary($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereDeletedAt($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereEditedAt($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereEntities($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereGroupId($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereId($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereInReplyToId($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereInReplyToProfileId($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereIsNsfw($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereLanguage($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereLikesCount($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereLocal($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereMediaIds($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereObjectUrl($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status wherePinnedOrder($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status wherePlaceId($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereProfileId($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereQuotePolicy($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereReblogOfId($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereReblogsCount($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereRendered($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereReply($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereReplyCount($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereScope($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereTrendable($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereType($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereUpdatedAt($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereUri($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereUrl($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status whereVisibility($value)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status withTrashed(bool $withTrashed = true)
|
|
* @method static \Illuminate\Database\Eloquent\Builder<static>|Status withoutTrashed()
|
|
*
|
|
* @mixin \Eloquent
|
|
*/
|
|
class Status extends Model
|
|
{
|
|
use HasFactory, HasSnowflakePrimary, SoftDeletes;
|
|
|
|
/**
|
|
* Indicates if the IDs are auto-incrementing.
|
|
*
|
|
* @var bool
|
|
*/
|
|
public $incrementing = false;
|
|
|
|
protected $guarded = [];
|
|
|
|
protected function casts(): array
|
|
{
|
|
return [
|
|
'deleted_at' => 'datetime',
|
|
'edited_at' => 'datetime',
|
|
'quote_policy' => 'integer',
|
|
];
|
|
}
|
|
|
|
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(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Profile::class);
|
|
}
|
|
|
|
public function media(): HasMany
|
|
{
|
|
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): bool => $media['type'] == 'image' && in_array($media['mime'], ['image/jpeg', 'image/png', 'image/jpg']))
|
|
->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;
|
|
}
|
|
$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(): string
|
|
{
|
|
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);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* ActivityPub id of the status this one replies to, for use as inReplyTo.
|
|
*
|
|
* For a remote parent this is object_url (the object's id), not uri.
|
|
* uri holds the object's `url` property, which for most software is the
|
|
* HTML permalink. Servers that thread by id cannot match a permalink, so
|
|
* sending it detaches our reply from the thread on their side.
|
|
*/
|
|
public function inReplyToUri(): ?string
|
|
{
|
|
if (! $this->in_reply_to_id) {
|
|
return null;
|
|
}
|
|
|
|
$parent = self::find($this->in_reply_to_id);
|
|
|
|
if (! $parent) {
|
|
return null;
|
|
}
|
|
|
|
return $parent->object_url ?: $parent->url();
|
|
}
|
|
|
|
public function conversation()
|
|
{
|
|
return $this->hasOne(Conversation::class);
|
|
}
|
|
|
|
public function hashtags(): HasManyThrough
|
|
{
|
|
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(): string
|
|
{
|
|
return route('report.form')."?type=post&id={$this->id}";
|
|
}
|
|
|
|
public function toActivityStream(): array
|
|
{
|
|
$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' => 'Document',
|
|
'name' => null,
|
|
'url' => $mediaCollection,
|
|
];
|
|
|
|
return $obj;
|
|
}
|
|
|
|
public function recentComments()
|
|
{
|
|
return $this->comments()->orderBy('created_at', 'desc')->take(3);
|
|
}
|
|
|
|
public function scopeToAudience($audience)
|
|
{
|
|
if (! in_array($audience, ['to', 'cc']) || $this->local == false) {
|
|
return null;
|
|
}
|
|
$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);
|
|
}
|
|
}
|