'datetime:Y-m-d H:i:s', 'created_at' => 'datetime:Y-m-d H:i:s', 'updated_at' => 'datetime:Y-m-d H:i:s', ]; private $titleLimit = 700; private $changed = false; // Detect if the set post was different from the cache public array $attachments = []; public array $resolvableAttachments = []; public $tags = []; public const MICROBLOG_NODE = 'urn:xmpp:microblog:0'; public const COMMENTS_NODE = 'urn:xmpp:microblog:0:comments'; public const STORIES_NODE = 'urn:xmpp:pubsub-social-feed:stories:0'; public function contact() { return $this->hasOne('App\Contact', 'id', 'aid'); } public function tags() { return $this->belongsToMany('App\Tag')->withTimestamps(); } public function comments() { return $this->hasMany('App\Post', 'parent_id', 'id') ->orderBy('published') ->where('like', false); } public function parent() { return $this->hasOne('App\Post', 'id', 'parent_id'); } public function info() { return $this->hasOne('App\Info', ['server', 'node'], ['server', 'node']); } public function userAffiliation() { return $this->hasOne('App\Affiliation', ['server', 'node'], ['server', 'node']) ->where('jid', me()->id); } public function userViews() { return $this->belongsToMany(User::class, 'post_user_views', 'post_id', 'user_id')->withTimestamps(); } public function myViews() { return $this->userViews()->where('user_id', me()->id); } public function likes() { return $this->hasMany('App\Post', 'parent_id', 'id') ->whereIn('id', function ($query) { $query->select(DB::raw('min(id) as id')) ->from('posts') ->where('like', true) ->whereNotNull('aid') ->groupByRaw('aid, parent_id'); }); } public function links() { return $this->hasMany('App\Attachment') ->where('category', 'link') ->whereNotIn('href', function ($query) { $query->select('href') ->from('attachments') ->where('post_id', $this->id) ->where('category', 'picture') ->get(); }); } /** * Attachements */ public function attachments() { return $this->hasMany('App\Attachment'); } public function resolveAttachments(): Collection { return $this->relations['attachments'] ?? $this->attachments()->get(); } public function getOpenlinkAttribute() { return $this->resolveAttachments()->firstWhere('category', 'open'); } public function getEmbedsAttribute() { return $this->resolveAttachments()->where('category', 'embed'); } public function getEmbedAttribute() { return $this->resolveAttachments()->firstWhere('category', 'embed'); } public function getFilesAttribute() { return $this->resolveAttachments()->where('category', 'file'); } public function getPicturesAttribute() { return $this->resolveAttachments() ->where('category', 'picture') ->where('type', '!=', 'content'); } public function getPictureAttribute() { return $this->resolveAttachments()->firstWhere('category', 'picture'); } public function getAttachmentAttribute() { return $this->resolveAttachments() ->whereIn('rel', ['enclosure', 'related']) ->orderBy('rel', 'desc') ->first(); // related first } public function save(array $options = []) { try { if (!$this->validAtom()) { \logError('Invalid Atom: ' . $this->server . '/' . $this->node . '/' . $this->nodeid); if ($this->created_at) { $this->delete(); } return; } if (!$this->changed) return; parent::save($options); if (!$this->isComment()) { $this->healAttachments(); $this->attachments()->delete(); $this->attachments()->saveMany($this->attachments); $this->tags()->sync($this->tags); } } catch (\Exception $e) { /* * When an article is received by two accounts simultaenously * in different processes they can be saved using the insert state * in the DB causing an error */ } } private function validAtom(): bool { return ($this->title != null && $this->updated != null); } public function scopeRestrictToMicroblog($query) { return $query->where('posts.node', Post::MICROBLOG_NODE); } public function scopeRestrictToCommunities($query) { return $query->where('posts.node', '!=', Post::MICROBLOG_NODE); } public function scopeWithoutComments($query) { return $query->whereNull('posts.parent_id'); } public function scopeRestrictUserHost($query) { $configuration = Configuration::get(); if ($configuration->restrictsuggestions) { $query->whereIn('id', function ($query) { $host = me()->session->host; $query->select('id') ->from('posts') ->where('server', 'like', '%.' . $host) ->orWhere('server', 'like', '@' . $host); }); } } public function scopeRestrictReported($query) { $query->whereNotIn('aid', function ($query) { $query->select('reported_id') ->from('reported_user') ->where('user_id', me()->id); }); } public function scopeRestrictNSFW($query) { $query->where('nsfw', false); if (me()->nsfw) { $query->orWhere('nsfw', true); } } public function scopeRecents($query) { $query->join( DB::raw('( select max(published) as published, server, node from posts group by server, node) as recents '), function ($join) { $join->on('posts.node', '=', 'recents.node'); $join->on('posts.published', '=', 'recents.published'); } ); } protected function withStoriesScope($query) { return $query->unionAll( DB::table('posts') ->where('node', Post::STORIES_NODE) ->whereIn('posts.server', function ($query) { $query->from('rosters') ->select('jid') ->where('session_id', SESSION_ID) ->where('subscription', 'both'); }) ); } protected function withFollowScope($query, ?string $since = null) { $posts = DB::table('posts') ->whereIn(DB::raw('(server, node)'), function ($query) { $query->select('server', 'node') ->from('subscriptions') ->where('jid', me()->id); }); if ($since != null) { $posts = $posts->where('published', '>', $since); } return $query->unionAll($posts); } protected function withContactsFollowScope($query) { return $query->unionAll( DB::table('posts') ->whereIn('server', function ($query) { $query->select('server') ->from('subscriptions') ->where('jid', me()->id); }) ->where('node', Post::MICROBLOG_NODE) ); } protected function withMineScope($query, string $node = Post::MICROBLOG_NODE, ?string $since = null) { $posts = DB::table('posts') ->where('node', $node) ->where('server', me()->id); if ($since != null) { $posts = $posts->where('published', '>', $since); } return $query->unionAll($posts); } protected function withCommunitiesFollowScope($query) { return $query->unionAll( DB::table('posts') ->whereIn(DB::raw('(server, node)'), function ($query) { $query->select('server', 'node') ->from('subscriptions') ->where('jid', me()->id) ->where('node', '!=', Post::MICROBLOG_NODE); }) ); } public function scopeMyStories($query, ?int $id = null) { $query = $query->whereIn('id', function ($query) { $filters = DB::table('posts')->where('id', -1); $filters = \App\Post::withMineScope($filters, Post::STORIES_NODE); $filters = \App\Post::withStoriesScope($filters, Post::STORIES_NODE); $query->select('id')->from( $filters, 'posts' ); }) ->where('published', '>', Carbon::now()->subDay()) ->orderBy('published', 'desc'); if ($id != null) $query = $query->where('id', $id); return $query; } public function getColorAttribute(): string { if ($this->contact) { return $this->contact->color; } if ($this->aid) { return stringToColor($this->aid); } return stringToColor($this->node); } public function getPreviousAttribute(): ?Post { return \App\Post::where('server', $this->server) ->where('node', $this->node) ->where('published', '<', $this->published) ->where('open', true) ->orderBy('published', 'desc') ->first(); } public function getNextAttribute(): ?Post { return \App\Post::where('server', $this->server) ->where('node', $this->node) ->where('published', '>', $this->published) ->orderBy('published') ->where('open', true) ->first(); } public function getTruenameAttribute() { if ($this->contact) { return $this->contact->truename; } return $this->aid ? explodeJid($this->aid)['username'] : ''; } public function getDecodedContentRawAttribute() { return htmlspecialchars_decode($this->contentraw, ENT_XML1 | ENT_COMPAT); } private function extractContent(SimpleXMLElement $contents): ?string { $htmlContent = $content = null; foreach ($contents as $c) { switch ($c->attributes()->type) { case 'html': $d = htmlspecialchars_decode((string)$c); $dom = new \DOMDocument('1.0', 'utf-8'); $dom->loadHTML('
' . (string)$entry->entry->summary . '
' : null; $content = $entry->entry->content ? $this->extractContent($entry->entry->content) : null; $this->content = $this->contentcleaned = null; if ($summary != null || $content != null) { $this->content = trim((string)$summary . (string)$content); $this->contentcleaned = purifyHTML(html_entity_decode($this->content)); } $this->updated = ($entry->entry->updated) ? toSQLDate($entry->entry->updated) : null; if ($entry->entry->published) { $this->published = toSQLDate($entry->entry->published); } elseif ($entry->entry->updated) { $this->published = toSQLDate($entry->entry->updated); } else { $this->published = gmdate(MOVIM_SQL_DATE); } if ($delay) { $this->delay = $delay; } // Tags parsing if ($entry->entry->category) { if ( $entry->entry->category->count() == 1 && isset($entry->entry->category->attributes()->term) && !empty(trim($entry->entry->category->attributes()->term)) ) { $tag = \App\Tag::firstOrCreateSafe([ 'name' => strtolower((string)$entry->entry->category->attributes()->term) ]); $this->tags[] = $tag->id; if ($tag->name == 'nsfw') { $this->nsfw = true; } } else { foreach ($entry->entry->category as $cat) { if (!empty(trim((string)$cat->attributes()->term))) { $tag = \App\Tag::firstOrCreateSafe([ 'name' => strtolower((string)$cat->attributes()->term) ]); if ($tag) { $this->tags[] = $tag->id; if ($tag->name == 'nsfw') { $this->nsfw = true; } } } } } } // Extract more tags if possible $tagsContent = getHashtags(htmlspecialchars($this->title ?? '')) + getHashtags(htmlspecialchars($this->contentraw ?? '')); foreach ($tagsContent as $tag) { $tag = \App\Tag::firstOrCreateSafe([ 'name' => strtolower((string)$tag) ]); $this->tags[] = $tag->id; } if (current(explode('.', $this->server)) == 'nsfw') { $this->nsfw = true; } if (!isset($this->commentserver)) { $this->commentserver = $this->server; } // We fill empty aid if ($this->isMicroblog() && empty($this->aid)) { $this->aid = $this->server; } // We check if this is a reply if ($entry->entry->{'in-reply-to'}) { $href = (string)$entry->entry->{'in-reply-to'}->attributes()->href; $arr = explode(';', $href); $this->replyserver = substr($arr[0], 5, -1); $this->replynode = substr($arr[1], 5); $this->replynodeid = substr($arr[2], 5); } $extra = false; // We try to extract a picture $xml = \simplexml_load_string('