You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

935 lines
28 KiB

8 years ago
8 years ago
8 years ago
8 years ago
  1. <?php
  2. namespace App;
  3. use Respect\Validation\Validator;
  4. use Awobaz\Compoships\Database\Eloquent\Model;
  5. use Carbon\Carbon;
  6. use Illuminate\Database\Capsule\Manager as DB;
  7. use Illuminate\Support\Collection;
  8. use Movim\Widget\Wrapper;
  9. use Moxl\Xec\Payload\Packet;
  10. use React\Promise\Promise;
  11. use SimpleXMLElement;
  12. class Post extends Model
  13. {
  14. protected $primaryKey = 'id';
  15. protected $guarded = [];
  16. public $with = [
  17. 'attachments',
  18. 'likes',
  19. 'comments',
  20. 'contact',
  21. 'links',
  22. 'userAffiliation'
  23. ];
  24. public $withCount = ['userViews'];
  25. private $titleLimit = 700;
  26. private $changed = false; // Detect if the set post was different from the cache
  27. public array $attachments = [];
  28. public array $resolvableAttachments = [];
  29. public $tags = [];
  30. public const MICROBLOG_NODE = 'urn:xmpp:microblog:0';
  31. public const COMMENTS_NODE = 'urn:xmpp:microblog:0:comments';
  32. public const STORIES_NODE = 'urn:xmpp:pubsub-social-feed:stories:0';
  33. public function contact()
  34. {
  35. return $this->hasOne('App\Contact', 'id', 'aid');
  36. }
  37. public function tags()
  38. {
  39. return $this->belongsToMany('App\Tag')->withTimestamps();
  40. }
  41. public function comments()
  42. {
  43. return $this->hasMany('App\Post', 'parent_id', 'id')
  44. ->orderBy('published')
  45. ->where('like', false);
  46. }
  47. public function parent()
  48. {
  49. return $this->hasOne('App\Post', 'id', 'parent_id');
  50. }
  51. public function info()
  52. {
  53. return $this->hasOne('App\Info', ['server', 'node'], ['server', 'node']);
  54. }
  55. public function userAffiliation()
  56. {
  57. return $this->hasOne('App\Affiliation', ['server', 'node'], ['server', 'node'])
  58. ->where('jid', me()->id);
  59. }
  60. public function userViews()
  61. {
  62. return $this->belongsToMany(User::class, 'post_user_views', 'post_id', 'user_id')->withTimestamps();
  63. }
  64. public function myViews()
  65. {
  66. return $this->userViews()->where('user_id', me()->id);
  67. }
  68. public function likes()
  69. {
  70. return $this->hasMany('App\Post', 'parent_id', 'id')
  71. ->whereIn('id', function ($query) {
  72. $query->select(DB::raw('min(id) as id'))
  73. ->from('posts')
  74. ->where('like', true)
  75. ->whereNotNull('aid')
  76. ->groupByRaw('aid, parent_id');
  77. });
  78. }
  79. public function links()
  80. {
  81. return $this->hasMany('App\Attachment')
  82. ->where('category', 'link')
  83. ->whereNotIn('href', function ($query) {
  84. $query->select('href')
  85. ->from('attachments')
  86. ->where('post_id', $this->id)
  87. ->where('category', 'picture')
  88. ->get();
  89. });
  90. }
  91. /**
  92. * Attachements
  93. */
  94. public function attachments()
  95. {
  96. return $this->hasMany('App\Attachment');
  97. }
  98. public function resolveAttachments(): Collection
  99. {
  100. return $this->relations['attachments'] ?? $this->attachments()->get();
  101. }
  102. public function getOpenlinkAttribute()
  103. {
  104. return $this->resolveAttachments()->firstWhere('category', 'open');
  105. }
  106. public function getEmbedsAttribute()
  107. {
  108. return $this->resolveAttachments()->where('category', 'embed');
  109. }
  110. public function getEmbedAttribute()
  111. {
  112. return $this->resolveAttachments()->firstWhere('category', 'embed');
  113. }
  114. public function getFilesAttribute()
  115. {
  116. return $this->resolveAttachments()->where('category', 'file');
  117. }
  118. public function getPicturesAttribute()
  119. {
  120. return $this->resolveAttachments()
  121. ->where('category', 'picture')
  122. ->where('type', '!=', 'content');
  123. }
  124. public function getPictureAttribute()
  125. {
  126. return $this->resolveAttachments()->firstWhere('category', 'picture');
  127. }
  128. public function getAttachmentAttribute()
  129. {
  130. return $this->resolveAttachments()
  131. ->whereIn('rel', ['enclosure', 'related'])
  132. ->orderBy('rel', 'desc')
  133. ->first(); // related first
  134. }
  135. public function save(array $options = [])
  136. {
  137. try {
  138. if (!$this->validAtom()) {
  139. \logError('Invalid Atom: ' . $this->server . '/' . $this->node . '/' . $this->nodeid);
  140. if ($this->created_at) {
  141. $this->delete();
  142. }
  143. return;
  144. }
  145. if (!$this->changed) return;
  146. parent::save($options);
  147. if (!$this->isComment()) {
  148. $this->healAttachments();
  149. $this->attachments()->delete();
  150. $this->attachments()->saveMany($this->attachments);
  151. $this->tags()->sync($this->tags);
  152. }
  153. } catch (\Exception $e) {
  154. /*
  155. * When an article is received by two accounts simultaenously
  156. * in different processes they can be saved using the insert state
  157. * in the DB causing an error
  158. */
  159. }
  160. }
  161. private function validAtom(): bool
  162. {
  163. return ($this->title != null && $this->updated != null);
  164. }
  165. public function scopeRestrictToMicroblog($query)
  166. {
  167. return $query->where('posts.node', Post::MICROBLOG_NODE);
  168. }
  169. public function scopeRestrictToCommunities($query)
  170. {
  171. return $query->where('posts.node', '!=', Post::MICROBLOG_NODE);
  172. }
  173. public function scopeWithoutComments($query)
  174. {
  175. return $query->whereNull('posts.parent_id');
  176. }
  177. public function scopeRestrictUserHost($query)
  178. {
  179. $configuration = Configuration::get();
  180. if ($configuration->restrictsuggestions) {
  181. $query->whereIn('id', function ($query) {
  182. $host = me()->session->host;
  183. $query->select('id')
  184. ->from('posts')
  185. ->where('server', 'like', '%.' . $host)
  186. ->orWhere('server', 'like', '@' . $host);
  187. });
  188. }
  189. }
  190. public function scopeRestrictReported($query)
  191. {
  192. $query->whereNotIn('aid', function ($query) {
  193. $query->select('reported_id')
  194. ->from('reported_user')
  195. ->where('user_id', me()->id);
  196. });
  197. }
  198. public function scopeRestrictNSFW($query)
  199. {
  200. $query->where('nsfw', false);
  201. if (me()->nsfw) {
  202. $query->orWhere('nsfw', true);
  203. }
  204. }
  205. public function scopeRecents($query)
  206. {
  207. $query->join(
  208. DB::raw('(
  209. select max(published) as published, server, node
  210. from posts
  211. group by server, node) as recents
  212. '),
  213. function ($join) {
  214. $join->on('posts.node', '=', 'recents.node');
  215. $join->on('posts.published', '=', 'recents.published');
  216. }
  217. );
  218. }
  219. protected function withStoriesScope($query)
  220. {
  221. return $query->unionAll(
  222. DB::table('posts')
  223. ->where('node', Post::STORIES_NODE)
  224. ->whereIn('posts.server', function ($query) {
  225. $query->from('rosters')
  226. ->select('jid')
  227. ->where('session_id', SESSION_ID)
  228. ->where('subscription', 'both');
  229. })
  230. );
  231. }
  232. protected function withContactsFollowScope($query)
  233. {
  234. return $query->unionAll(
  235. DB::table('posts')
  236. ->whereIn('server', function ($query) {
  237. $query->select('server')
  238. ->from('subscriptions')
  239. ->where('jid', me()->id);
  240. })
  241. ->where('node', Post::MICROBLOG_NODE)
  242. );
  243. }
  244. protected function withMineScope($query, string $node = Post::MICROBLOG_NODE)
  245. {
  246. return $query->unionAll(
  247. DB::table('posts')
  248. ->where('node', $node)
  249. ->where('server', me()->id)
  250. );
  251. }
  252. public function scopeWithMine($query)
  253. {
  254. return $this->withMineScope($query);
  255. }
  256. protected function withCommunitiesFollowScope($query)
  257. {
  258. return $query->unionAll(
  259. DB::table('posts')
  260. ->whereIn(DB::raw('(server, node)'), function ($query) {
  261. $query->select('server', 'node')
  262. ->from('subscriptions')
  263. ->where('jid', me()->id)
  264. ->where('node', '!=', Post::MICROBLOG_NODE);
  265. })
  266. );
  267. }
  268. public function scopeMyStories($query, ?int $id = null)
  269. {
  270. $query = $query->whereIn('id', function ($query) {
  271. $filters = DB::table('posts')->where('id', -1);
  272. $filters = \App\Post::withMineScope($filters, Post::STORIES_NODE);
  273. $filters = \App\Post::withStoriesScope($filters, Post::STORIES_NODE);
  274. $query->select('id')->from(
  275. $filters,
  276. 'posts'
  277. );
  278. })
  279. ->where('published', '>', Carbon::now()->subDay())
  280. ->orderBy('published', 'desc');
  281. if ($id != null) $query = $query->where('id', $id);
  282. return $query;
  283. }
  284. public function getColorAttribute(): string
  285. {
  286. if ($this->contact) {
  287. return $this->contact->color;
  288. }
  289. if ($this->aid) {
  290. return stringToColor($this->aid);
  291. }
  292. return stringToColor($this->node);
  293. }
  294. public function getPreviousAttribute(): ?Post
  295. {
  296. return \App\Post::where('server', $this->server)
  297. ->where('node', $this->node)
  298. ->where('published', '<', $this->published)
  299. ->where('open', true)
  300. ->orderBy('published', 'desc')
  301. ->first();
  302. }
  303. public function getNextAttribute(): ?Post
  304. {
  305. return \App\Post::where('server', $this->server)
  306. ->where('node', $this->node)
  307. ->where('published', '>', $this->published)
  308. ->orderBy('published')
  309. ->where('open', true)
  310. ->first();
  311. }
  312. public function getTruenameAttribute()
  313. {
  314. if ($this->contact) {
  315. return $this->contact->truename;
  316. }
  317. return $this->aid
  318. ? explodeJid($this->aid)['username']
  319. : '';
  320. }
  321. public function getDecodedContentRawAttribute()
  322. {
  323. return htmlspecialchars_decode($this->contentraw, ENT_XML1 | ENT_COMPAT);
  324. }
  325. private function extractContent(SimpleXMLElement $contents): ?string
  326. {
  327. $content = null;
  328. foreach ($contents as $c) {
  329. switch ($c->attributes()->type) {
  330. case 'html':
  331. $d = htmlspecialchars_decode((string)$c);
  332. $dom = new \DOMDocument('1.0', 'utf-8');
  333. $dom->loadHTML('<div>' . $d . '</div>', LIBXML_NOERROR);
  334. return (string)$dom->saveHTML($dom->documentElement->lastChild->lastChild);
  335. break;
  336. case 'xhtml':
  337. $import = null;
  338. $dom = new \DOMDocument('1.0', 'utf-8');
  339. if ($c->children() instanceof \DOMElement) {
  340. $import = @dom_import_simplexml($c->children());
  341. }
  342. if ($import == null) {
  343. $import = dom_import_simplexml($c);
  344. }
  345. $element = $dom->importNode($import, true);
  346. $dom->appendChild($element);
  347. return (string)$dom->saveHTML();
  348. break;
  349. case 'text':
  350. if (trim($c) != '') {
  351. $this->contentraw = trim($c);
  352. }
  353. break;
  354. default:
  355. $content = (string)$c;
  356. break;
  357. }
  358. }
  359. return $content;
  360. }
  361. private function extractTitle($titles): ?string
  362. {
  363. $title = null;
  364. foreach ($titles as $t) {
  365. switch ($t->attributes()->type) {
  366. case 'html':
  367. case 'xhtml':
  368. $title = strip_tags(
  369. ($t->children()->getName() == 'div' && (string)$t->children()->attributes()->xmlns == 'http://www.w3.org/1999/xhtml')
  370. ? html_entity_decode((string)$t->children()->asXML())
  371. : (string)$t->children()->asXML()
  372. );
  373. break;
  374. case 'text':
  375. if (trim($t) != '') {
  376. $title = trim($t);
  377. }
  378. break;
  379. default:
  380. $title = (string)$t;
  381. break;
  382. }
  383. }
  384. return $title;
  385. }
  386. public function set($entry, $delay = false)
  387. {
  388. $this->nodeid = (string)$entry->attributes()->id;
  389. $hash = hash('sha256', $entry->asXML());
  390. // Detect if things changed from the cached version
  391. if ($hash == $this->contenthash) {
  392. return \React\Promise\resolve();
  393. }
  394. $this->contenthash = $hash;
  395. $this->changed = true;
  396. // Ensure that the author is the publisher
  397. if (
  398. $entry->entry->author && $entry->entry->author->uri
  399. && 'xmpp:' . baseJid((string)$entry->attributes()->publisher) == (string)$entry->entry->author->uri
  400. ) {
  401. $this->aid = substr((string)$entry->entry->author->uri, 5);
  402. $this->aname = ($entry->entry->author->name)
  403. ? (string)$entry->entry->author->name
  404. : null;
  405. $this->aemail = ($entry->entry->author->email)
  406. ? (string)$entry->entry->author->email
  407. : null;
  408. } else {
  409. $this->aid = null;
  410. }
  411. if (empty($this->aname)) {
  412. $this->aname = null;
  413. }
  414. // Content
  415. $this->title = $entry->entry->title
  416. ? $this->extractTitle($entry->entry->title)
  417. : null;
  418. $summary = ($entry->entry->summary && (string)$entry->entry->summary != '')
  419. ? '<p class="summary">' . (string)$entry->entry->summary . '</p>'
  420. : null;
  421. $content = $entry->entry->content
  422. ? $this->extractContent($entry->entry->content)
  423. : null;
  424. $this->content = $this->contentcleaned = null;
  425. if ($summary != null || $content != null) {
  426. $this->content = trim((string)$summary . (string)$content);
  427. $this->contentcleaned = purifyHTML(html_entity_decode($this->content));
  428. }
  429. $this->updated = ($entry->entry->updated)
  430. ? toSQLDate($entry->entry->updated)
  431. : null;
  432. if ($entry->entry->published) {
  433. $this->published = toSQLDate($entry->entry->published);
  434. } elseif ($entry->entry->updated) {
  435. $this->published = toSQLDate($entry->entry->updated);
  436. } else {
  437. $this->published = gmdate(MOVIM_SQL_DATE);
  438. }
  439. if ($delay) {
  440. $this->delay = $delay;
  441. }
  442. // Tags parsing
  443. if ($entry->entry->category) {
  444. if (
  445. $entry->entry->category->count() == 1
  446. && isset($entry->entry->category->attributes()->term)
  447. && !empty(trim($entry->entry->category->attributes()->term))
  448. ) {
  449. $tag = \App\Tag::firstOrCreateSafe([
  450. 'name' => strtolower((string)$entry->entry->category->attributes()->term)
  451. ]);
  452. $this->tags[] = $tag->id;
  453. if ($tag->name == 'nsfw') {
  454. $this->nsfw = true;
  455. }
  456. } else {
  457. foreach ($entry->entry->category as $cat) {
  458. if (!empty(trim((string)$cat->attributes()->term))) {
  459. $tag = \App\Tag::firstOrCreateSafe([
  460. 'name' => strtolower((string)$cat->attributes()->term)
  461. ]);
  462. if ($tag) {
  463. $this->tags[] = $tag->id;
  464. if ($tag->name == 'nsfw') {
  465. $this->nsfw = true;
  466. }
  467. }
  468. }
  469. }
  470. }
  471. }
  472. // Extract more tags if possible
  473. $tagsContent = getHashtags(htmlspecialchars($this->title ?? ''))
  474. + getHashtags(htmlspecialchars($this->contentraw ?? ''));
  475. foreach ($tagsContent as $tag) {
  476. $tag = \App\Tag::firstOrCreateSafe([
  477. 'name' => strtolower((string)$tag)
  478. ]);
  479. $this->tags[] = $tag->id;
  480. }
  481. if (current(explode('.', $this->server)) == 'nsfw') {
  482. $this->nsfw = true;
  483. }
  484. if (!isset($this->commentserver)) {
  485. $this->commentserver = $this->server;
  486. }
  487. // We fill empty aid
  488. if ($this->isMicroblog() && empty($this->aid)) {
  489. $this->aid = $this->server;
  490. }
  491. // We check if this is a reply
  492. if ($entry->entry->{'in-reply-to'}) {
  493. $href = (string)$entry->entry->{'in-reply-to'}->attributes()->href;
  494. $arr = explode(';', $href);
  495. $this->replyserver = substr($arr[0], 5, -1);
  496. $this->replynode = substr($arr[1], 5);
  497. $this->replynodeid = substr($arr[2], 5);
  498. }
  499. $extra = false;
  500. // We try to extract a picture
  501. $xml = \simplexml_load_string('<div>' . $this->contentcleaned . '</div>');
  502. if ($xml) {
  503. $results = $xml->xpath('//img/@src');
  504. if (is_array($results) && !empty($results)) {
  505. $extra = (string)$results[0];
  506. } else {
  507. $results = $xml->xpath('//video/@poster');
  508. if (is_array($results) && !empty($results)) {
  509. $extra = (string)$results[0];
  510. }
  511. }
  512. $results = $xml->xpath('//a');
  513. if (is_array($results) && !empty($results)) {
  514. foreach ($results as $link) {
  515. $link->addAttribute('target', '_blank');
  516. }
  517. }
  518. }
  519. $this->like = $this->isLike();
  520. $this->open = false;
  521. if ($this->isComment()) {
  522. $p = \App\Post::where('commentserver', $this->server)
  523. ->where('commentnodeid', substr($this->node, 30))
  524. ->first();
  525. if ($p) {
  526. $this->parent_id = $p->id;
  527. }
  528. }
  529. $this->setAttachments($entry->entry->link, $extra);
  530. }
  531. private function setAttachments($links, $extra = false)
  532. {
  533. $picture = false;
  534. foreach ($links as $attachment) {
  535. $enc = (array)$attachment->attributes();
  536. $enc = $enc['@attributes'];
  537. $att = new Attachment;
  538. if (empty($enc['href'])) {
  539. continue;
  540. }
  541. $att->rel = $enc['rel'] ?? 'alternate';
  542. $att->href = $enc['href'];
  543. $att->category = 'other';
  544. if (isset($enc['title'])) {
  545. $att->title = $enc['title'];
  546. }
  547. if (isset($enc['description'])) {
  548. $att->description = $enc['description'];
  549. }
  550. switch ($att->rel) {
  551. case 'enclosure':
  552. if (isset($enc['type'])) {
  553. $att->category = 'file';
  554. $att->type = $enc['type'];
  555. if (typeIsPicture($enc['type'])) {
  556. $att->category = 'picture';
  557. $picture = true;
  558. }
  559. }
  560. break;
  561. case 'alternate':
  562. if (Validator::url()->isValid($enc['href'])) {
  563. $this->open = true;
  564. $att->category = 'open';
  565. }
  566. break;
  567. case 'related':
  568. $att->category = 'link';
  569. $att->logo = (isset($enc['logo'])) ? $enc['logo'] : null;
  570. break;
  571. }
  572. if (in_array($att->rel, ['enclosure', 'related', 'alternate'])) {
  573. $atte = new Attachment;
  574. $atte->rel = $att->rel;
  575. $atte->category = 'embed';
  576. // Youtube
  577. if (preg_match('%(?:youtube(?:-nocookie)?\.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|youtu\.be/)([^"&?/ ]{11})%i', $enc['href'], $match)) {
  578. $atte->href = 'https://www.youtube.com/embed/' . $match[1];
  579. $this->attachments[] = $atte;
  580. // RedGif
  581. } elseif (preg_match('/(?:https:\/\/)?(?:www.)?redgifs.com\/watch\/([a-zA-Z]+)$/', $enc['href'], $match)) {
  582. $atte->href = 'https://www.redgifs.com/ifr/' . $match[1];
  583. $this->attachments[] = $atte;
  584. $this->resolveUrl($enc['href']);
  585. // PeerTube
  586. } elseif (
  587. preg_match('/https:\/\/?(.*)\/w\/(\w{22})/', $enc['href'], $match)
  588. || preg_match('/https:\/\/?(.*)\/videos\/watch\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/', $enc['href'], $match)
  589. ) {
  590. $atte->href = 'https://' . $match[1] . '/videos/embed/' . $match[2];
  591. $this->attachments[] = $atte;
  592. // Reddit
  593. } elseif (
  594. in_array(parse_url($enc['href'], PHP_URL_HOST), ['old.reddit.com', 'reddit.com', 'www.reddit.com'])
  595. && substr(parse_url($enc['href'], PHP_URL_PATH), 0, 8) == '/gallery'
  596. ) {
  597. $this->resolveUrl($enc['href']);
  598. }
  599. }
  600. $this->attachments[] = $att;
  601. if ((string)$attachment->attributes()->title == 'comments') {
  602. $url = parse_url(urldecode((string)$attachment->attributes()->href));
  603. if ($url) {
  604. $this->commentserver = $url['path'];
  605. $this->commentnodeid = substr($url['query'], 36);
  606. }
  607. }
  608. }
  609. if ($picture == false && $extra) {
  610. $attachment = new Attachment;
  611. $attachment->rel = 'enclosure';
  612. $attachment->href = $extra;
  613. $attachment->type = 'content';
  614. $attachment->category = 'picture';
  615. $this->attachments[] = $attachment;
  616. }
  617. }
  618. private function resolveUrl(string $url)
  619. {
  620. array_push(
  621. $this->resolvableAttachments,
  622. new Promise(function () use ($url) {
  623. \requestResolverWorker($url)->then(function ($extractor) {
  624. try {
  625. $atte = new Attachment;
  626. $atte->rel = 'enclosure';
  627. $atte->href = $extractor->image;
  628. $atte->type = 'media/jpeg';
  629. $atte->category = 'picture';
  630. $atte->post_id = $this->id;
  631. $atte->save();
  632. } catch (\Throwable $th) {
  633. //
  634. }
  635. Wrapper::getInstance()->iterate('post_resolved', (new Packet)->pack($this->id));
  636. });
  637. })
  638. );
  639. }
  640. private function healAttachments()
  641. {
  642. $enclosures = [];
  643. foreach (array_filter($this->attachments, fn($a) => $a->rel == 'enclosure') as $attachment) {
  644. array_push($enclosures, $attachment->href);
  645. }
  646. foreach (array_filter($this->attachments, fn($a) => $a->rel != 'enclosure') as $key => $attachment) {
  647. if (in_array($attachment->href, $enclosures)) {
  648. unset($this->attachments[$key]);
  649. }
  650. }
  651. // Remove duplicates...
  652. foreach ($this->attachments as $key => $attachment) {
  653. foreach ($this->attachments as $keyCheck => $attachmentCheck) {
  654. if (
  655. $key != $keyCheck
  656. && $attachment->href == $attachmentCheck->href
  657. && $attachment->category == $attachmentCheck->category
  658. && $attachment->rel == $attachmentCheck->rel
  659. ) {
  660. unset($this->attachments[$key]);
  661. }
  662. }
  663. }
  664. }
  665. public function getUUID(): string
  666. {
  667. if (substr($this->nodeid, 10) == 'urn:uuid:') {
  668. return $this->nodeid;
  669. }
  670. return 'urn:uuid:' . generateUUID(hash('sha256', $this->server . $this->node . $this->nodeid, true));
  671. }
  672. public function getRef(): string
  673. {
  674. return 'xmpp:' . $this->server . '?;' .
  675. http_build_query([
  676. 'node' => $this->node,
  677. 'item' => $this->nodeid
  678. ], arg_separator: ';');
  679. }
  680. public function getLink(?bool $public = false): string
  681. {
  682. if ($public) {
  683. return $this->isMicroblog()
  684. ? \Movim\Route::urlize('blog', [$this->server, $this->nodeid])
  685. : \Movim\Route::urlize('community', [$this->server, $this->node, $this->nodeid]);
  686. }
  687. return \Movim\Route::urlize('post', [$this->server, $this->node, $this->nodeid]);
  688. }
  689. // Works only for the microblog posts
  690. public function getParent(): ?Post
  691. {
  692. return \App\Post::find($this->parent_id);
  693. }
  694. public function isMine(User $me, ?bool $force = false): bool
  695. {
  696. if ($force) {
  697. return ($this->aid == $me->id);
  698. }
  699. return ($this->aid == $me->id
  700. || $this->server == $me->id);
  701. }
  702. public function isMicroblog(): bool
  703. {
  704. return ($this->node == "urn:xmpp:microblog:0");
  705. }
  706. public function isEdited(): bool
  707. {
  708. return $this->published != $this->updated;
  709. }
  710. public function isEditable(): bool
  711. {
  712. return ($this->contentraw != null || $this->links != null);
  713. }
  714. public function isShort(): bool
  715. {
  716. return $this->contentcleaned == null || (strlen($this->contentcleaned) < 700);
  717. }
  718. public function isBrief(): bool
  719. {
  720. return ($this->content == null && strlen($this->title) < $this->titleLimit);
  721. }
  722. public function isReply(): bool
  723. {
  724. return isset($this->replynodeid);
  725. }
  726. public function isLike(): bool
  727. {
  728. return ($this->title == '♥');
  729. }
  730. public function isRTL(): bool
  731. {
  732. return (isRTL($this->contentraw ?? '') || isRTL($this->title ?? ''));
  733. }
  734. public function isStory(): bool
  735. {
  736. return $this->node == Post::STORIES_NODE;
  737. }
  738. public function isComment(): bool
  739. {
  740. return (str_starts_with($this->node, Post::COMMENTS_NODE));
  741. }
  742. public function hasCommentsNode(): bool
  743. {
  744. return (isset($this->commentserver)
  745. && isset($this->commentnodeid));
  746. }
  747. public function getSummary()
  748. {
  749. if ($this->isBrief()) {
  750. return truncate(html_entity_decode($this->title), 140);
  751. }
  752. return truncate(stripTags(html_entity_decode($this->contentcleaned ?? '')), 140);
  753. }
  754. public function getContent(bool $addHashTagLinks = false): string
  755. {
  756. if ($this->contentcleaned == null) return '';
  757. return ($addHashTagLinks)
  758. ? addHashtagsLinks($this->contentcleaned)
  759. : $this->contentcleaned;
  760. }
  761. public function getReply()
  762. {
  763. if (!$this->replynodeid) {
  764. return;
  765. }
  766. return \App\Post::where('server', $this->replyserver)
  767. ->where('node', $this->replynode)
  768. ->where('nodeid', $this->replynodeid)
  769. ->first();
  770. }
  771. public function isLiked()
  772. {
  773. return ($this->likes()->where('aid', me()->id)->count() > 0);
  774. }
  775. public function isRecycled()
  776. {
  777. return false;
  778. }
  779. }