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.

878 lines
30 KiB

8 months ago
8 months ago
8 months ago
7 years ago
4 years ago
3 years ago
3 years ago
4 years ago
5 months ago
4 years ago
8 months ago
4 years ago
4 years ago
  1. <?php
  2. namespace App;
  3. use DOMDocument;
  4. use DOMXPath;
  5. use Movim\Model;
  6. use Movim\Image;
  7. use Movim\Session;
  8. use Illuminate\Database\QueryException;
  9. use Illuminate\Database\Capsule\Manager as DB;
  10. use Illuminate\Support\Collection;
  11. use Movim\XMPPUri;
  12. use Moxl\Xec\Action\BOB\Request;
  13. use Moxl\Xec\Action\Pubsub\GetItem;
  14. class Message extends Model
  15. {
  16. protected $primaryKey = ['user_id', 'jidfrom', 'id'];
  17. public $incrementing = false;
  18. public $mucpm; // Only used in Message Payloads to detect composer/paused PM messages
  19. protected $guarded = [];
  20. protected $with = ['reactions', 'parent.from', 'resolvedUrl', 'replace', 'file'];
  21. protected $attributes = [
  22. 'type' => 'chat'
  23. ];
  24. protected $casts = [
  25. 'quoted' => 'boolean',
  26. 'markable' => 'boolean'
  27. ];
  28. private ?Collection $messageFiles = null;
  29. public static $inlinePlaceholder = 'inline-img:';
  30. public const MESSAGE_TYPE = [
  31. 'chat',
  32. 'headline',
  33. 'invitation',
  34. 'jingle_end',
  35. 'jingle_finish',
  36. 'jingle_incoming',
  37. 'jingle_outgoing',
  38. 'jingle_reject',
  39. 'jingle_retract',
  40. ];
  41. public const MESSAGE_TYPE_MUC = [
  42. 'groupchat',
  43. 'muc_admin',
  44. 'muc_member',
  45. 'muc_outcast',
  46. 'muc_owner',
  47. 'muji_propose',
  48. 'muji_retract',
  49. ];
  50. public static function boot()
  51. {
  52. parent::boot();
  53. static::saved(function (Message $message) {
  54. if ($message->messageFiles != null && $message->messageFiles->isNotEmpty()) {
  55. $mid = Message::where('id', $message->id)
  56. ->where('user_id', me()->id)
  57. ->where('jidfrom', $message->jidfrom)
  58. ->first()
  59. ->mid;
  60. MessageFile::where('message_mid', $mid)->delete();
  61. $message->messageFiles->each(function ($file) use ($mid) {
  62. $file->message_mid = $mid;
  63. $file->save();
  64. });
  65. }
  66. $message->resolvePost();
  67. });
  68. }
  69. public function parent()
  70. {
  71. return $this->belongsTo('App\Message', 'parentmid', 'mid');
  72. }
  73. public function replace()
  74. {
  75. return $this->belongsTo('App\Message', 'replaceid', 'originid')->without('replace');
  76. }
  77. public function resolvedUrl()
  78. {
  79. return $this->belongsTo('App\Url', 'urlid', 'id');
  80. }
  81. public function from()
  82. {
  83. return $this->belongsTo('App\Contact', 'jidfrom', 'id');
  84. }
  85. public function user()
  86. {
  87. return $this->belongsTo('App\User');
  88. }
  89. public function post()
  90. {
  91. return $this->belongsTo('App\Post', 'postid', 'id');
  92. }
  93. public function scopeJid($query, string $jid)
  94. {
  95. $jidFromToMessages = DB::table('messages')
  96. ->where('user_id', me()->id)
  97. ->where('jidfrom', $jid)
  98. ->unionAll(
  99. DB::table('messages')
  100. ->where('user_id', me()->id)
  101. ->where('jidto', $jid)
  102. );
  103. return $query->select('*')->from(
  104. $jidFromToMessages,
  105. 'messages'
  106. )->where('user_id', me()->id);
  107. }
  108. public function reactions()
  109. {
  110. return $this->hasMany('App\Reaction', 'message_mid', 'mid');
  111. }
  112. public function file()
  113. {
  114. return $this->hasOne('App\MessageFile', 'message_mid', 'mid');
  115. }
  116. public function files()
  117. {
  118. return $this->hasMany('App\MessageFile', 'message_mid', 'mid');
  119. }
  120. public function getStickerImageAttribute(): ?Image
  121. {
  122. $image = new Image;
  123. $image->setKey($this->sticker_cid_hash);
  124. if ($image->load()) {
  125. return $image;
  126. }
  127. return null;
  128. }
  129. public function getInlinesAttribute(): ?array
  130. {
  131. return array_key_exists('inlines', $this->attributes) && $this->attributes['inlines'] !== null
  132. ? unserialize($this->attributes['inlines'])
  133. : null;
  134. }
  135. public function getOmemoheaderAttribute(): ?array
  136. {
  137. return array_key_exists('omemoheader', $this->attributes) && $this->attributes['omemoheader'] !== null
  138. ? unserialize($this->attributes['omemoheader'])
  139. : null;
  140. }
  141. public function getJidfromAttribute()
  142. {
  143. return \unechap($this->attributes['jidfrom']);
  144. }
  145. public function getJidAttribute()
  146. {
  147. return $this->attributes['jidfrom'] == me()->id
  148. ? \unechap($this->attributes['jidto'])
  149. : \unechap($this->attributes['jidfrom']);
  150. }
  151. public static function findByStanza(?\SimpleXMLElement $stanza = null, ?\SimpleXMLElement $parent = null): Message
  152. {
  153. $jidfrom = baseJid((string)$stanza->attributes()->from);
  154. if (
  155. $stanza->attributes()->xmlns
  156. && $stanza->attributes()->xmlns == 'urn:xmpp:mam:2'
  157. ) {
  158. return self::firstOrNew([
  159. 'user_id' => me()->id,
  160. 'stanzaid' => (string)$stanza->attributes()->id,
  161. 'jidfrom' => baseJid((string)$stanza->forwarded->message->attributes()->from)
  162. ]);
  163. } elseif (
  164. $stanza->{'stanza-id'} && $stanza->{'stanza-id'}->attributes()->id
  165. && ($stanza->{'stanza-id'}->attributes()->by == $jidfrom
  166. || $stanza->{'stanza-id'}->attributes()->by == me()->id
  167. )
  168. ) {
  169. return self::firstOrNew([
  170. 'user_id' => me()->id,
  171. 'stanzaid' => (string)$stanza->{'stanza-id'}->attributes()->id,
  172. 'jidfrom' => $jidfrom
  173. ]);
  174. } else {
  175. $message = new Message;
  176. $message->user_id = me()->id;
  177. $message->id = 'm_' . generateUUID();
  178. $message->jidfrom = $jidfrom;
  179. return $message;
  180. }
  181. }
  182. public static function getLast(string $to, bool $muc = false): ?Message
  183. {
  184. $m = null;
  185. if ($muc) {
  186. // Resolve the current presence
  187. $presence = me()->session->presences()
  188. ->where('jid', $to)
  189. ->where('muc', true)
  190. ->where('mucjid', me()->id)
  191. ->first();
  192. if ($presence) {
  193. $m = me()->messages()
  194. ->where('type', 'groupchat')
  195. ->where('jidfrom', $to)
  196. ->where('jidto', me()->id)
  197. ->where('resource', $presence->resource)
  198. ->orderBy('published', 'desc')
  199. ->first();
  200. }
  201. } else {
  202. $m = me()->messages()
  203. ->where('jidto', $to)
  204. ->orderBy('published', 'desc')
  205. ->first();
  206. }
  207. return $m;
  208. }
  209. public function isLast(): bool
  210. {
  211. $last = Message::getLast($this->isMuc() ? $this->jidfrom : $this->jidto, $this->isMuc());
  212. return ($last && $this->mid == $last->mid);
  213. }
  214. public static function eventMessageFactory(string $type, string $from, string $thread): Message
  215. {
  216. $userid = me()->id;
  217. $message = new \App\Message;
  218. $message->user_id = $userid;
  219. $message->id = 'm_' . generateUUID();
  220. $message->jidto = $userid;
  221. $message->jidfrom = $from;
  222. $message->published = gmdate('Y-m-d H:i:s');
  223. $message->thread = $thread;
  224. $message->type = $type;
  225. return $message;
  226. }
  227. public function clearUnreads()
  228. {
  229. if ($this->jidfrom == $this->user_id) {
  230. $this->user->messages()
  231. ->where('jidfrom', $this->jidto)
  232. ->where('seen', false)
  233. ->update(['seen' => true]);
  234. }
  235. }
  236. public function set($stanza, $parent = false)
  237. {
  238. $this->messageFiles = collect();
  239. // We reset the URL resolution to refresh it once the message is displayed
  240. $this->resolved = false;
  241. $jidTo = explodeJid((string)$stanza->attributes()->to);
  242. $jidFrom = explodeJid((string)$stanza->attributes()->from);
  243. $this->user_id = me()->id;
  244. if (!$this->id) {
  245. $this->id = 'm_' . generateUUID();
  246. }
  247. if ($stanza->attributes()->id) {
  248. $this->messageid = (string)$stanza->attributes()->id;
  249. }
  250. if (!$this->jidto) {
  251. $this->jidto = $jidTo['jid'];
  252. }
  253. if (!$this->jidfrom) {
  254. $this->jidfrom = $jidFrom['jid'];
  255. }
  256. // If the message is from me
  257. if ($this->jidfrom == $this->user_id) {
  258. $this->seen = true;
  259. }
  260. if (isset($jidFrom['resource'])) {
  261. $this->resource = $jidFrom['resource'];
  262. }
  263. if ($stanza->delay) {
  264. $this->published = gmdate('Y-m-d H:i:s', strtotime($stanza->delay->attributes()->stamp));
  265. } elseif ($parent && $parent->delay) {
  266. $this->published = gmdate('Y-m-d H:i:s', strtotime($parent->delay->attributes()->stamp));
  267. } elseif (!isset($stanza->replace) || $this->published === null) {
  268. $this->published = gmdate('Y-m-d H:i:s');
  269. }
  270. $this->type = 'chat';
  271. if ($stanza->attributes()->type) {
  272. $this->type = (string)$stanza->attributes()->type;
  273. }
  274. // https://xmpp.org/extensions/xep-0359.html#stanza-id
  275. if (
  276. $stanza->{'origin-id'}
  277. && (string)$stanza->{'origin-id'}->attributes()->xmlns == 'urn:xmpp:sid:0'
  278. ) {
  279. $this->originid = (string)$stanza->{'origin-id'}->attributes()->id;
  280. }
  281. // https://xmpp.org/extensions/xep-0359.html#origin-id for groupchat only
  282. if (
  283. $this->isMuc()
  284. && $stanza->{'stanza-id'}
  285. && $stanza->{'stanza-id'}->attributes()->id
  286. && (string)$stanza->{'stanza-id'}->attributes()->xmlns == 'urn:xmpp:sid:0'
  287. && ($stanza->{'stanza-id'}->attributes()->by == $this->jidfrom
  288. || $stanza->{'stanza-id'}->attributes()->by == me()->id
  289. )
  290. ) {
  291. if ($this->isMuc()) {
  292. $session = Session::instance();
  293. // Cache the state in Session for performances purpose
  294. $sessionKey = $this->jidfrom . '_stanza_id';
  295. $conferenceStanzaIdEnabled = $session->get($sessionKey, null);
  296. if ($conferenceStanzaIdEnabled == null) {
  297. $conference = $this->user->session->conferences()
  298. ->where('conference', $this->jidfrom)
  299. ->first();
  300. $session->set($sessionKey, $conference && $conference->info && $conference->info->hasStanzaId());
  301. }
  302. if ($session->get($sessionKey, false)) {
  303. $this->stanzaid = (string)$stanza->{'stanza-id'}->attributes()->id;
  304. }
  305. } else {
  306. $this->stanzaid = (string)$stanza->{'stanza-id'}->attributes()->id;
  307. }
  308. }
  309. // If it's a MUC message, we assume that the server already handled it
  310. if ($this->isMuc()) {
  311. $this->delivered = gmdate('Y-m-d H:i:s');
  312. }
  313. if (
  314. $this->type !== 'groupchat'
  315. && $stanza->x
  316. && (string)$stanza->x->attributes()->xmlns == 'http://jabber.org/protocol/muc#user'
  317. ) {
  318. $this->mucpm = true;
  319. if ($parent && (string)$parent->attributes()->xmlns == 'urn:xmpp:forward:0') {
  320. $this->jidto = (string)$stanza->attributes()->to;
  321. } elseif (isset($jidFrom['resource'])) {
  322. $this->jidfrom = $jidFrom['jid'] . '/' . $jidFrom['resource'];
  323. }
  324. }
  325. # XEP-0444: Message Reactions
  326. if (
  327. isset($stanza->reactions)
  328. && $stanza->reactions->attributes()->xmlns == 'urn:xmpp:reactions:0'
  329. ) {
  330. $parentMessage = $this->resolveParentMessage($this->jidfrom, (string)$stanza->reactions->attributes()->id);
  331. if ($parentMessage) {
  332. $resource = $this->isMuc()
  333. ? $this->resource
  334. : $this->jidfrom;
  335. $parentMessage
  336. ->reactions()
  337. ->where('jidfrom', $resource)
  338. ->delete();
  339. $emojis = [];
  340. $now = \Carbon\Carbon::now();
  341. $emoji = \Movim\Emoji::getInstance();
  342. foreach ($stanza->reactions->reaction as $children) {
  343. $emoji->replace((string)$children);
  344. if ($emoji->isSingleEmoji()) {
  345. $reaction = new Reaction;
  346. $reaction->message_mid = $parentMessage->mid;
  347. $reaction->emoji = (string)$children;
  348. $reaction->jidfrom = $resource;
  349. $reaction->created_at = $now;
  350. $reaction->updated_at = $now;
  351. \array_push($emojis, $reaction->toArray());
  352. }
  353. }
  354. try {
  355. Reaction::insert($emojis);
  356. } catch (QueryException $e) {
  357. // Duplicate ?
  358. logError($e);
  359. }
  360. return $parentMessage;
  361. }
  362. return null;
  363. } elseif ($stanza->body || $stanza->subject) {
  364. if ($stanza->body) {
  365. $this->body = (string)$stanza->body;
  366. }
  367. if ($stanza->subject) {
  368. $this->subject = (string)$stanza->subject;
  369. }
  370. if ($stanza->thread) {
  371. $this->thread = (string)$stanza->thread;
  372. }
  373. // XEP-0333: Chat Markers
  374. $this->markable = (bool)($stanza->markable && $stanza->markable->attributes()->xmlns == 'urn:xmpp:chat-markers:0');
  375. // Reply can be handled by XEP-0461: Message Replies or by the threadid Jabber mechanism
  376. if ($stanza->reply && $stanza->reply->attributes()->xmlns == 'urn:xmpp:reply:0') {
  377. $parentMessage = $this->resolveParentMessage($this->jidfrom, (string)$stanza->reply->attributes()->id);
  378. if (
  379. $parentMessage && $parentMessage->mid != $this->mid
  380. && $parentMessage->originid != $this->originid
  381. ) {
  382. $this->parentmid = $parentMessage->mid;
  383. }
  384. if (
  385. $stanza->fallback && $stanza->fallback->attributes()->xmlns == 'urn:xmpp:fallback:0'
  386. && $stanza->fallback->attributes()->for == 'urn:xmpp:reply:0'
  387. ) {
  388. $this->body = mb_substr(
  389. htmlspecialchars_decode($this->body, ENT_XML1),
  390. (int)$stanza->fallback->body->attributes()->end
  391. );
  392. }
  393. }
  394. if ($this->isMuc()) {
  395. $presence = $this->user->session?->presences()
  396. ->where('jid', $this->jidfrom)
  397. ->where('mucjid', $this->user->id)
  398. ->first();
  399. if (
  400. $presence
  401. && $this->body != null
  402. && strpos($this->body, $presence->resource) !== false
  403. && $this->resource != $presence->resource
  404. ) {
  405. $this->quoted = true;
  406. }
  407. }
  408. if (
  409. $stanza->html
  410. && (string)$stanza->html->attributes()->xmlns = 'http://jabber.org/protocol/xhtml-im'
  411. ) {
  412. $head = '<head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head>';
  413. $dom = new DOMDocument('1.0', 'UTF-8');
  414. $dom->loadHTML($head . (string)$stanza->html->body);
  415. $xpath = new DOMXPath($dom);
  416. $imgs = $xpath->query("//img[starts-with(@src,'cid:')]");
  417. if ($imgs && $imgs->count() >= 1) {
  418. $texts = $xpath->query('//p/text()');
  419. // Message with inline images
  420. if (($imgs->count() > 1) || ($texts && $texts->count() > 0)) {
  421. $inlines = [];
  422. foreach ($imgs as $img) {
  423. $key = generateKey(12);
  424. $cid = getCid($img->getAttribute('src'));
  425. $inlines[$key] = [
  426. 'hash' => $cid['hash'],
  427. 'algorythm' => $cid['algorythm'],
  428. 'alt' => $img->getAttribute('alt'),
  429. ];
  430. $img->replaceWith(self::$inlinePlaceholder . $key);
  431. }
  432. $this->attributes['inlines'] = serialize($inlines);
  433. $this->body = (string)$dom->textContent;
  434. }
  435. // One sticker only
  436. elseif ($imgs->count() == 1) {
  437. $cid = getCid($imgs->item(0)->getAttribute('src'));
  438. if ($cid) {
  439. $this->sticker_cid_hash = $cid['hash'];
  440. $this->sticker_cid_algorythm = $cid['algorythm'];
  441. }
  442. }
  443. }
  444. }
  445. // XEP-0385: Stateless Inline Media Sharing (SIMS)
  446. if (
  447. $stanza->reference
  448. && (string)$stanza->reference->attributes()->xmlns == 'urn:xmpp:reference:0'
  449. ) {
  450. $messageFile = new MessageFile;
  451. if (
  452. $stanza->reference->{'media-sharing'}
  453. && (string)$stanza->reference->{'media-sharing'}->attributes()->xmlns == 'urn:xmpp:sims:1'
  454. ) {
  455. $file = $stanza->reference->{'media-sharing'}->file;
  456. if (isset($file)) {
  457. if (preg_match('/\w+\/[-+.\w]+/', $file->{'media-type'}) == 1) {
  458. $messageFile->type = (string)$file->{'media-type'};
  459. }
  460. $messageFile->size = (int)$file->size;
  461. $messageFile->name = (string)$file->name;
  462. }
  463. if ($stanza->reference->{'media-sharing'}->sources) {
  464. $source = $stanza->reference->{'media-sharing'}->sources->reference;
  465. if (!filter_var((string)$source->attributes()->uri, FILTER_VALIDATE_URL) === false) {
  466. $messageFile->url = (string)$source->attributes()->uri;
  467. }
  468. }
  469. if (
  470. $stanza->reference->{'media-sharing'}->file->thumbnail
  471. && (string)$stanza->reference->{'media-sharing'}->file->thumbnail->attributes()->xmlns == 'urn:xmpp:thumbs:1'
  472. ) {
  473. $thumbnailAttributes = $stanza->reference->{'media-sharing'}->file->thumbnail->attributes();
  474. if (!filter_var((string)$thumbnailAttributes->uri, FILTER_VALIDATE_URL) === false) {
  475. $messageFile->thumbnail_width = (int)$thumbnailAttributes->width;
  476. $messageFile->thumbnail_height = (int)$thumbnailAttributes->height;
  477. $messageFile->thumbnail_type = (string)$thumbnailAttributes->{'media-type'};
  478. $messageFile->thumbnail_url = (string)$thumbnailAttributes->uri;
  479. }
  480. if (substr((string)$thumbnailAttributes->uri, 0, 28) == 'data:image/thumbhash;base64,') {
  481. $messageFile->thumbnail_width = (int)$thumbnailAttributes->width;
  482. $messageFile->thumbnail_height = (int)$thumbnailAttributes->height;
  483. $messageFile->thumbnail_type = (string)$thumbnailAttributes->{'media-type'};
  484. $messageFile->thumbnail_url = substr((string)$thumbnailAttributes->uri, 28);
  485. }
  486. }
  487. if (
  488. $messageFile->url
  489. && $messageFile->type
  490. && $messageFile->size
  491. && $messageFile->name
  492. ) {
  493. if (empty($messageFile->name)) {
  494. $messageFile->name =
  495. pathinfo(parse_url($messageFile->uri, PHP_URL_PATH), PATHINFO_BASENAME)
  496. . ' (' . parse_url($messageFile->uri, PHP_URL_HOST) . ')';
  497. }
  498. $this->picture = $messageFile->isPicture;
  499. $this->messageFiles->push($messageFile);
  500. }
  501. } else {
  502. $this->posturi = (string)$stanza->reference->attributes()->uri;
  503. }
  504. }
  505. if (
  506. $stanza->encryption
  507. && (string)$stanza->encryption->attributes()->xmlns == 'urn:xmpp:eme:0'
  508. ) {
  509. $this->encrypted = true;
  510. }
  511. if (
  512. $stanza->replace
  513. && (string)$stanza->replace->attributes()->xmlns == 'urn:xmpp:message-correct:0'
  514. ) {
  515. // Here the replaceid could be a bad one, we will handle it later
  516. $this->replaceid = (string)$stanza->replace->attributes()->id;
  517. }
  518. if (isset($stanza->x->invite)) {
  519. $this->type = 'invitation';
  520. $this->subject = $this->jidfrom;
  521. $this->jidfrom = baseJid((string)$stanza->x->invite->attributes()->from);
  522. }
  523. } elseif (
  524. isset($stanza->x)
  525. && $stanza->x->attributes()->xmlns == 'jabber:x:conference'
  526. ) {
  527. $this->type = 'invitation';
  528. $this->body = (string)$stanza->x->attributes()->reason;
  529. $this->subject = (string)$stanza->x->attributes()->jid;
  530. }
  531. # XEP-0384 OMEMO Encryption
  532. if (
  533. isset($stanza->encrypted)
  534. && $stanza->encrypted->attributes()->xmlns == 'eu.siacs.conversations.axolotl'
  535. ) {
  536. $omemoHeader = new MessageOmemoHeader;
  537. $omemoHeader->set($stanza);
  538. $this->attributes['omemoheader'] = (string)$omemoHeader;
  539. $this->attributes['bundleid'] = (int)$omemoHeader->sid;
  540. }
  541. return $this;
  542. }
  543. /**
  544. * @desc Resolve the Post from its URI, require a saved message
  545. */
  546. public function resolvePost()
  547. {
  548. if (!$this->posturi || !empty($this->postid)) return;
  549. $xmppUri = new XMPPUri($this->posturi);
  550. if ($post = $xmppUri->getPost()) {
  551. $this->postid = $post->id;
  552. } elseif($xmppUri->getServer() && $xmppUri->getNode() && $xmppUri->getNodeItemId()) {
  553. $getItem = new GetItem;
  554. $getItem->setTo($xmppUri->getServer())
  555. ->setNode($xmppUri->getNode())
  556. ->setId($xmppUri->getNodeItemId())
  557. ->setMessagemid($this->mid)
  558. ->request();
  559. }
  560. }
  561. /**
  562. * @desc Prepare and return the body with inline images, and request them if missing
  563. */
  564. public function getInlinedBodyAttribute(?bool $alt = false, bool $triggerRequest = false): ?string
  565. {
  566. if (!array_key_exists('body', $this->attributes)) return null;
  567. $body = $this->attributes['body'];
  568. if (is_array($this->getInlinesAttribute())) {
  569. foreach ($this->getInlinesAttribute() as $key => $inline) {
  570. if ($alt == true) {
  571. $body = str_replace(
  572. Message::$inlinePlaceholder . $key,
  573. $inline['alt'],
  574. $body
  575. );
  576. continue;
  577. }
  578. $url = Image::getOrCreate($inline['hash']);
  579. if ($url) {
  580. $dom = new \DOMDocument('1.0', 'UTF-8');
  581. $img = $dom->createElement('img');
  582. $img->setAttribute('class', 'inline');
  583. $img->setAttribute('src', $url);
  584. $img->setAttribute('alt', $inline['alt']);
  585. $img->setAttribute('title', $inline['alt']);
  586. $dom->append($img);
  587. $body = str_replace(
  588. Message::$inlinePlaceholder . $key,
  589. $dom->saveHTML($dom->documentElement),
  590. $body
  591. );
  592. } else {
  593. $body = str_replace(
  594. Message::$inlinePlaceholder . $key,
  595. $inline['alt'],
  596. $body
  597. );
  598. if ($triggerRequest) {
  599. $r = new Request;
  600. $r->setTo($this->attributes['jidfrom'])
  601. ->setResource($this->attributes['resource'])
  602. ->setHash($inline['hash'])
  603. ->setAlgorythm($inline['algorythm'])
  604. ->request();
  605. }
  606. }
  607. }
  608. }
  609. return $body;
  610. }
  611. public function isEmpty(): bool
  612. {
  613. return (empty($this->body)
  614. && empty($this->sticker)
  615. && !$this->file
  616. );
  617. }
  618. public function isMuc(): bool
  619. {
  620. return ($this->type == 'groupchat');
  621. }
  622. public function isSubject(): bool
  623. {
  624. return !empty($this->subject);
  625. }
  626. public function isMine(): bool
  627. {
  628. if ($this->isMuc()) {
  629. return $this->user->session->presences()
  630. ->where('jid', $this->jidfrom)
  631. ->where('resource', $this->resource)
  632. ->where('mucjid', $this->user_id)
  633. ->where('muc', true)
  634. ->count() > 0;
  635. }
  636. return ($this->user_id == $this->jidfrom);
  637. }
  638. public function isClassic(): bool
  639. {
  640. return in_array($this->type, ['chat', 'groupchat']);
  641. }
  642. public function retract()
  643. {
  644. $this->retracted = true;
  645. $this->oldid = null;
  646. $this->body = $this->html = 'retracted';
  647. }
  648. public function addUrls()
  649. {
  650. if (is_string($this->body)) {
  651. $this->body = addUrls($this->body);
  652. }
  653. }
  654. public function resolveColor(): string
  655. {
  656. $this->color = stringToColor($this->resource);
  657. return $this->color;
  658. }
  659. public function valid()
  660. {
  661. return
  662. strlen($this->attributes['jidto']) < 256
  663. && strlen($this->attributes['jidfrom']) < 256
  664. && (!isset($this->attributes['resource']) || strlen($this->attributes['resource']) < 256)
  665. && (!isset($this->attributes['thread']) || strlen($this->attributes['thread']) < 128)
  666. && (!isset($this->attributes['replaceid']) || strlen($this->attributes['replaceid']) < 64)
  667. && (!isset($this->attributes['originid']) || strlen($this->attributes['originid']) < 255)
  668. && (!isset($this->attributes['id']) || strlen($this->attributes['id']) < 64)
  669. && (!isset($this->attributes['oldid']) || strlen($this->attributes['oldid']) < 64);
  670. }
  671. // toArray is already used
  672. public function toRawArray()
  673. {
  674. $array = [
  675. 'user_id' => $this->attributes['user_id'] ?? null,
  676. 'id' => $this->attributes['id'] ?? null,
  677. 'oldid' => $this->attributes['oldid'] ?? null,
  678. 'jidto' => $this->attributes['jidto'] ?? null,
  679. 'jidfrom' => $this->attributes['jidfrom'] ?? null,
  680. 'resource' => $this->attributes['resource'] ?? null,
  681. 'type' => $this->attributes['type'] ?? null,
  682. 'subject' => $this->attributes['subject'] ?? null,
  683. 'thread' => $this->attributes['thread'] ?? null,
  684. 'body' => $this->attributes['body'] ?? null,
  685. 'html' => $this->attributes['html'] ?? null,
  686. 'published' => $this->attributes['published'] ?? null,
  687. 'delivered' => $this->attributes['deliver'] ?? null,
  688. 'displayed' => $this->attributes['displayed'] ?? null,
  689. 'quoted' => $this->attributes['quoted'] ?? false,
  690. 'markable' => $this->attributes['markable'] ?? false,
  691. 'sticker' => $this->attributes['sticker'] ?? null,
  692. 'created_at' => $this->attributes['created_at'] ?? null,
  693. 'updated_at' => $this->attributes['updated_at'] ?? null,
  694. 'replaceid' => $this->attributes['replaceid'] ?? null,
  695. 'seen' => $this->attributes['seen'] ?? false,
  696. 'encrypted' => $this->attributes['encrypted'] ?? false,
  697. 'originid' => $this->attributes['originid'] ?? null,
  698. 'retracted' => $this->attributes['retracted'] ?? false,
  699. 'resolved' => $this->attributes['resolved'] ?? false,
  700. 'picture' => $this->attributes['picture'] ?? false,
  701. 'parentmid' => $this->attributes['parentmid'] ?? null,
  702. 'inline' => $this->attributes['inlines'] ?? null,
  703. ];
  704. // Generate a proper mid
  705. if (
  706. empty($this->attributes['mid'])
  707. || ($this->attributes['mid'] && $this->attributes['mid'] == 1)
  708. ) {
  709. $array['mid'] = $this->getNextStatementId();
  710. } else {
  711. $array['mid'] = $this->attributes['mid'];
  712. }
  713. return $array;
  714. }
  715. public function getNextStatementId()
  716. {
  717. $next_id = DB::select("select nextval('messages_mid_seq'::regclass)");
  718. return intval($next_id['0']->nextval);
  719. }
  720. // https://xmpp.org/extensions/xep-0444.html#business-id
  721. // https://xmpp.org/extensions/xep-0461.html#business-id
  722. private function resolveParentMessage(string $from, string $id): ?Message
  723. {
  724. $parentMessage = null;
  725. if ($this->isMuc()) {
  726. $parentMessage = $this->user->messages()->jid($from)
  727. ->where('stanzaid', $id)
  728. ->first();
  729. } else {
  730. $parentMessage = $this->user->messages()->jid($from)
  731. ->where('messageid', $id)
  732. ->first();
  733. // Rare case, origin-id
  734. if (!$parentMessage) {
  735. $parentMessage = $this->user->messages()->jid($from)
  736. ->where('originid', $id)
  737. ->first();
  738. }
  739. }
  740. return $parentMessage;
  741. }
  742. }