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.

882 lines
31 KiB

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