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.

1080 lines
33 KiB

9 years ago
9 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
8 years ago
8 years ago
8 years ago
9 years ago
9 years ago
8 years ago
  1. <?php
  2. use Moxl\Xec\Action\Message\Publish;
  3. use Moxl\Xec\Action\Message\Reactions;
  4. use Moxl\Xec\Action\Muc\GetConfig;
  5. use Moxl\Xec\Action\Muc\SetConfig;
  6. use App\Configuration;
  7. use App\Message;
  8. use App\Reaction;
  9. use Moxl\Xec\Action\BOB\Request;
  10. use Respect\Validation\Validator;
  11. use Illuminate\Database\Capsule\Manager as DB;
  12. use Movim\Picture;
  13. use Movim\ChatStates;
  14. use Movim\ChatOwnState;
  15. include_once WIDGETS_PATH.'ContactActions/ContactActions.php';
  16. class Chat extends \Movim\Widget\Base
  17. {
  18. private $_pagination = 50;
  19. private $_wrapper = [];
  20. private $_mucPresences = [];
  21. private $_messageTypes = ['chat', 'headline', 'invitation', 'jingle_start', 'jingle_end'];
  22. public function load()
  23. {
  24. $this->addjs('chat.js');
  25. $this->addcss('chat.css');
  26. $this->registerEvent('invitation', 'onMessage');
  27. $this->registerEvent('carbons', 'onMessage');
  28. $this->registerEvent('message', 'onMessage');
  29. $this->registerEvent('receiptack', 'onMessageReceipt');
  30. $this->registerEvent('displayed', 'onMessage', 'chat');
  31. $this->registerEvent('mam_get_handle', 'onMAMRetrieved', 'chat');
  32. $this->registerEvent('composing', 'onComposing', 'chat');
  33. $this->registerEvent('paused', 'onPaused', 'chat');
  34. $this->registerEvent('subject', 'onConferenceSubject', 'chat');
  35. $this->registerEvent('muc_setsubject_handle', 'onConferenceSubject', 'chat');
  36. $this->registerEvent('muc_getconfig_handle', 'onRoomConfig', 'chat');
  37. $this->registerEvent('muc_setconfig_handle', 'onRoomConfigSaved', 'chat');
  38. $this->registerEvent('muc_setconfig_error', 'onRoomConfigError', 'chat');
  39. $this->registerEvent('presence_muc_handle', 'onMucConnected', 'chat');
  40. $this->registerEvent('message_publish_error', 'onPublishError', 'chat');
  41. $this->registerEvent('bob_request_handle', 'onSticker');
  42. $this->registerEvent('notification_counter_clear', 'onNotificationCounterClear');
  43. }
  44. public function onMessageReceipt($packet)
  45. {
  46. $this->onMessage($packet, false, true);
  47. }
  48. public function onNotificationCounterClear($params)
  49. {
  50. list($page, $first, $room) = array_pad($params, 3, null);
  51. if ($page === 'chat') {
  52. $this->prepareMessages($first, ($room === 'room'));
  53. }
  54. }
  55. public function onPublishError($packet)
  56. {
  57. Notification::toast(
  58. $packet->content ??
  59. $this->__('chat.publish_error')
  60. );
  61. }
  62. public function onMessage($packet, $history = false, $receipt = false)
  63. {
  64. $message = $packet->content;
  65. $from = null;
  66. $chatStates = ChatStates::getInstance();
  67. if ($message->isEmpty()) {
  68. return;
  69. }
  70. if ($message->user_id == $message->jidto
  71. && !$history
  72. && $message->seen == false
  73. && $message->jidfrom != $message->jidto) {
  74. $from = $message->jidfrom;
  75. $roster = $this->user->session->contacts()->where('jid', $from)->first();
  76. $contact = App\Contact::firstOrNew(['id' => $from]);
  77. if ($contact != null
  78. //&& $message->isTrusted()
  79. && !$message->isOTR()
  80. && $message->type != 'groupchat'
  81. && !$message->oldid) {
  82. $chatStates->clearState($from);
  83. Notification::append(
  84. 'chat|'.$from,
  85. $roster ? $roster->truename : $contact->truename,
  86. $message->body,
  87. $contact->getPhoto(),
  88. 4,
  89. $this->route('chat', $contact->jid)
  90. );
  91. }
  92. // If it's a groupchat message
  93. elseif ($message->type == 'groupchat'
  94. && $message->quoted
  95. && !$receipt) {
  96. $conference = $this->user->session
  97. ->conferences()->where('conference', $from)
  98. ->first();
  99. Notification::append(
  100. 'chat|'.$from,
  101. ($conference != null && $conference->name)
  102. ? $conference->name
  103. : $from,
  104. $message->resource.': '.$message->body,
  105. false,
  106. 4
  107. );
  108. } elseif ($message->type == 'groupchat') {
  109. $chatStates->clearState($from, $message->resource);
  110. }
  111. $this->onPaused($chatStates->getState($from));
  112. }
  113. if (!$message->isOTR()) {
  114. $this->rpc('Chat.appendMessagesWrapper', $this->prepareMessage($message, $from));
  115. }
  116. $this->event('chat_counter', $this->user->unreads());
  117. }
  118. public function onSticker($packet)
  119. {
  120. list($to, $cid) = array_values($packet->content);
  121. $this->ajaxGet($to);
  122. }
  123. public function onComposing(array $array)
  124. {
  125. $this->setState(
  126. $array[0],
  127. is_array($array[1]) && !empty($array[1])
  128. ? $this->prepareComposeList(array_keys($array[1]))
  129. : $this->__('message.composing')
  130. );
  131. }
  132. public function onPaused(array $array)
  133. {
  134. $this->setState(
  135. $array[0],
  136. is_array($array[1]) && !empty($array[1])
  137. ? $this->prepareComposeList(array_keys($array[1]))
  138. : ''
  139. );
  140. }
  141. public function onConferenceSubject($packet)
  142. {
  143. $this->ajaxGetRoom($packet->content->jidfrom);
  144. }
  145. public function onMAMRetrieved($packet)
  146. {
  147. $this->ajaxGetRoom($packet->content);
  148. }
  149. public function onMucConnected($packet)
  150. {
  151. $this->ajaxGetRoom($packet->content->jid, false, true);
  152. }
  153. public function onRoomConfigError($packet)
  154. {
  155. Notification::toast($packet->content);
  156. }
  157. public function onRoomConfig($packet)
  158. {
  159. list($config, $room) = array_values($packet->content);
  160. $view = $this->tpl();
  161. $xml = new \XMPPtoForm;
  162. $form = $xml->getHTML($config->x);
  163. $view->assign('form', $form);
  164. $view->assign('room', $room);
  165. Dialog::fill($view->draw('_chat_config_room'), true);
  166. }
  167. public function onRoomConfigSaved($packet)
  168. {
  169. Notification::toast($this->__('chatroom.config_saved'));
  170. }
  171. private function setState(string $jid, string $message)
  172. {
  173. $this->rpc('MovimTpl.fill', '#' . cleanupId($jid.'_state'), $message);
  174. }
  175. public function ajaxInit()
  176. {
  177. $view = $this->tpl();
  178. $date = $view->draw('_chat_date');
  179. $separator = $view->draw('_chat_separator');
  180. $this->rpc('Chat.setGeneralElements', $date, $separator);
  181. }
  182. public function ajaxClearCounter($jid)
  183. {
  184. $this->prepareMessages($jid, false, true);
  185. $this->event('chat_counter', $this->user->unreads());
  186. }
  187. /**
  188. * @brief Get a discussion
  189. * @param string $jid
  190. */
  191. public function ajaxGet($jid = null, $light = false)
  192. {
  193. if ($jid == null) {
  194. $this->rpc('MovimTpl.hidePanel');
  195. $this->rpc('Notification.current', 'chat');
  196. $this->rpc('MovimUtils.pushState', $this->route('chat'));
  197. $this->rpc('MovimTpl.fill', '#chat_widget', $this->prepareEmpty());
  198. } else {
  199. if ($light == false) {
  200. $this->rpc('MovimUtils.pushState', $this->route('chat', $jid));
  201. $this->rpc('MovimTpl.fill', '#chat_widget', $this->prepareChat($jid));
  202. $this->rpc('MovimTpl.showPanel');
  203. $this->rpc('Chat.focus');
  204. }
  205. $this->prepareMessages($jid);
  206. $this->rpc('Notification.current', 'chat|'.$jid);
  207. }
  208. }
  209. /**
  210. * @brief Get a chatroom
  211. * @param string $jid
  212. */
  213. public function ajaxGetRoom($room, $light = false, $noConnect = false)
  214. {
  215. if (!$this->validateJid($room)) {
  216. return;
  217. }
  218. $r = $this->user->session->conferences()->where('conference', $room)->first();
  219. if ($r) {
  220. if (!$r->connected && !$noConnect) {
  221. $this->rpc('Rooms_ajaxJoin', $r->conference, $r->nick);
  222. }
  223. if ($light == false) {
  224. $this->rpc('MovimUtils.pushState', $this->route('chat', [$room, 'room']));
  225. $this->rpc('MovimTpl.fill', '#chat_widget', $this->prepareChat($room, true));
  226. $this->rpc('MovimTpl.showPanel');
  227. $this->rpc('Chat.focus');
  228. }
  229. $this->prepareMessages($room, true);
  230. $this->rpc('Notification.current', 'chat|'.$room.'|room');
  231. } else {
  232. $this->rpc('Rooms_ajaxAdd', $room);
  233. }
  234. }
  235. /**
  236. * @brief Get a Drawer view of a contact
  237. */
  238. public function ajaxGetContact($jid)
  239. {
  240. $c = new ContactActions;
  241. $c->ajaxGetDrawer($jid);
  242. }
  243. /**
  244. * @brief Send a message
  245. *
  246. * @param string $to
  247. * @param string $message
  248. * @return void
  249. */
  250. public function ajaxHttpSendMessage($to, $message = false, $muc = false, $resource = false, $replace = false, $file = false)
  251. {
  252. $message = trim($message);
  253. if (filter_var($message, FILTER_VALIDATE_URL)) {
  254. $headers = requestHeaders($message);
  255. if ($headers['http_code'] == 200
  256. && isset($headers['content_type'])
  257. && typeIsPicture($headers['content_type'])
  258. && $headers['download_content_length'] > 100) {
  259. $file = new \stdClass;
  260. $file->name = $message;
  261. $file->type = $headers['content_type'];
  262. $file->size = $headers['download_content_length'];
  263. $file->uri = $message;
  264. }
  265. }
  266. $body = ($file != false && $file->type != 'xmpp')
  267. ? $file->uri
  268. : $message;
  269. if ($body == '' || $body == '/me') {
  270. return;
  271. }
  272. $oldid = null;
  273. if ($replace) {
  274. $oldid = $replace->id;
  275. $m = $replace;
  276. $m->id = generateUUID();
  277. \App\Message::where('id', $oldid)->update([
  278. 'id' => $m->id,
  279. 'replaceid' => $m->id
  280. ]);
  281. } else {
  282. $m = new \App\Message;
  283. $m->id = generateUUID();
  284. $m->replaceid = $m->id;
  285. $m->user_id = $this->user->id;
  286. $m->jidto = echapJid($to);
  287. $m->jidfrom = $this->user->id;
  288. $m->published = gmdate('Y-m-d H:i:s');
  289. }
  290. // TODO: make this boolean configurable
  291. $m->markable = true;
  292. $m->seen = true;
  293. $m->type = 'chat';
  294. $m->resource = $this->user->session->resource;
  295. if ($muc) {
  296. $m->type = 'groupchat';
  297. $m->resource = $this->user->session->username;
  298. $m->jidfrom = $to;
  299. }
  300. $m->body = $body;
  301. if ($resource != false) {
  302. $to = $to . '/' . $resource;
  303. }
  304. // We decode URL codes to send the correct message to the XMPP server
  305. $p = new Publish;
  306. $p->setTo($to);
  307. //$p->setHTML($m->html);
  308. $p->setContent($m->body);
  309. if ($replace != false) {
  310. $p->setReplace($oldid);
  311. }
  312. $p->setId($m->id);
  313. if ($muc) {
  314. $p->setMuc();
  315. }
  316. if ($file) {
  317. $m->file = (array)$file;
  318. $p->setFile($file);
  319. }
  320. (ChatOwnState::getInstance())->halt();
  321. $p->request();
  322. /* Is it really clean ? */
  323. if (!$p->getMuc()) {
  324. $m->oldid = $oldid;
  325. $m->body = htmlentities(trim($m->body), ENT_XML1, 'UTF-8');
  326. $m->save();
  327. $m = $m->fresh();
  328. $packet = new \Moxl\Xec\Payload\Packet;
  329. $packet->content = $m;
  330. // We refresh the Chats list
  331. $c = new Chats;
  332. $c->onMessage($packet);
  333. $this->onMessage($packet);
  334. }
  335. }
  336. /**
  337. * @brief Send a correction message
  338. *
  339. * @param string $to
  340. * @param string $message
  341. * @return void
  342. */
  343. public function ajaxHttpCorrect($to, $message)
  344. {
  345. $replace = $this->user->messages()
  346. ->where('jidto', $to)
  347. ->orderBy('published', 'desc')
  348. ->first();
  349. if ($replace) {
  350. $this->ajaxHttpSendMessage($to, $message, false, false, $replace);
  351. }
  352. }
  353. /**
  354. * @brief Send a reaction
  355. *
  356. * @
  357. */
  358. public function ajaxHttpSendReaction($mid, string $emoji)
  359. {
  360. $parentMessage = $this->user->messages()
  361. ->where('mid', $mid)
  362. ->first();
  363. $emojiHandler = \Movim\Emoji::getInstance();
  364. $emojiHandler->replace($emoji);
  365. if ($parentMessage && $emojiHandler->isSingleEmoji()) {
  366. // Try to load the MUC presence and resolve the resource
  367. $mucPresence = null;
  368. if ($parentMessage->type == 'groupchat') {
  369. $mucPresence = $this->user->session->presences()
  370. ->where('jid', $parentMessage->jidfrom)
  371. ->where('mucjid', $this->user->id)
  372. ->where('muc', true)
  373. ->first();
  374. if (!$mucPresence) return;
  375. }
  376. $jidfrom = ($parentMessage->type == 'groupchat')
  377. ? $mucPresence->resource
  378. : $this->user->id;
  379. $emojis = $parentMessage->reactions()
  380. ->where('jidfrom', $jidfrom)
  381. ->get();
  382. $r = new Reactions;
  383. $newEmojis = [];
  384. // This reaction was not published yet
  385. if ($emojis->where('emoji', $emoji)->count() == 0) {
  386. $reaction = new Reaction;
  387. $reaction->message_mid = $parentMessage->mid;
  388. $reaction->jidfrom = ($parentMessage->type == 'groupchat')
  389. ? $this->user->session->username
  390. : $this->user->id;
  391. $reaction->emoji = $emoji;
  392. if ($parentMessage->type != 'groupchat') {
  393. $reaction->save();
  394. }
  395. $newEmojis = $emojis->push($reaction);
  396. } else {
  397. if ($parentMessage->type != 'groupchat') {
  398. $parentMessage->reactions()
  399. ->where('jidfrom', $jidfrom)
  400. ->where('emoji', $emoji)
  401. ->delete();
  402. }
  403. $newEmojis = $emojis->filter(function ($value, $key) use ($emoji) {
  404. return $value->emoji != $emoji;
  405. });
  406. }
  407. $r->setTo($parentMessage->jidfrom != $parentMessage->user_id
  408. ? $parentMessage->jidfrom
  409. : $parentMessage->jidto)
  410. ->setId(\generateUUID())
  411. ->setParentId($parentMessage->replaceid)
  412. ->setReactions($newEmojis->pluck('emoji')->toArray());
  413. if ($parentMessage->type == 'groupchat') {
  414. $r->setMuc();
  415. }
  416. $r->request();
  417. if ($parentMessage->type != 'groupchat') {
  418. $packet = new \Moxl\Xec\Payload\Packet;
  419. $packet->content = $parentMessage;
  420. $this->onMessage($packet);
  421. }
  422. }
  423. }
  424. /**
  425. * @brief Get the last message sent
  426. *
  427. * @param string $to
  428. * @return void
  429. */
  430. public function ajaxLast($to)
  431. {
  432. $m = $this->user->messages()
  433. ->where('jidto', $to)
  434. ->orderBy('published', 'desc')
  435. ->first();
  436. if (!isset($m->sticker)
  437. && !isset($m->file)) {
  438. $this->rpc('Chat.setTextarea', htmlspecialchars_decode($m->body));
  439. }
  440. }
  441. /**
  442. * @brief Send a "composing" message
  443. *
  444. * @param string $to
  445. * @return void
  446. */
  447. public function ajaxSendComposing($to, $muc = false)
  448. {
  449. if (!$this->validateJid($to)) {
  450. return;
  451. }
  452. (ChatOwnState::getInstance())->composing($to, $muc);
  453. }
  454. /**
  455. * @brief Get the chat history
  456. *
  457. * @param string jid
  458. * @param string time
  459. */
  460. public function ajaxGetHistory($jid, $date, $muc = false, $prepend = true)
  461. {
  462. if (!$this->validateJid($jid)) {
  463. return;
  464. }
  465. $messages = $this->user->messages()
  466. ->where(function ($query) use ($jid) {
  467. $query->where('jidfrom', $jid)
  468. ->orWhere('jidto', $jid);
  469. })
  470. ->where('published', $prepend ? '<' : '>', date(SQL_DATE, strtotime($date)));
  471. $messages = $muc
  472. ? $messages->where('type', 'groupchat')->whereNull('subject')
  473. : $messages->whereIn('type', $this->_messageTypes);
  474. $messages = $messages->orderBy('published', 'desc')
  475. ->take($this->_pagination)
  476. ->get();
  477. if ($messages->count() > 0) {
  478. if ($prepend) {
  479. Notification::toast($this->__('message.history', $messages->count()));
  480. } else {
  481. $messages = $messages->reverse();
  482. }
  483. foreach ($messages as $message) {
  484. if (!$message->isOTR()) {
  485. $this->prepareMessage($message);
  486. }
  487. }
  488. $this->rpc('Chat.appendMessagesWrapper', $this->_wrapper, $prepend);
  489. $this->_wrapper = [];
  490. }
  491. }
  492. /**
  493. * @brief Configure a room
  494. *
  495. * @param string $room
  496. */
  497. public function ajaxGetRoomConfig($room)
  498. {
  499. if (!$this->validateJid($room)) {
  500. return;
  501. }
  502. $gc = new GetConfig;
  503. $gc->setTo($room)
  504. ->request();
  505. }
  506. /**
  507. * @brief Save the room configuration
  508. *
  509. * @param string $room
  510. */
  511. public function ajaxSetRoomConfig($data, $room)
  512. {
  513. if (!$this->validateJid($room)) {
  514. return;
  515. }
  516. $sc = new SetConfig;
  517. $sc->setTo($room)
  518. ->setData($data)
  519. ->request();
  520. }
  521. /**
  522. * @brief Set last displayed message
  523. */
  524. public function ajaxDisplayed($jid, $id)
  525. {
  526. if (!$this->validateJid($jid)) {
  527. return;
  528. }
  529. $message = $this->user->messages()->where('id', $id)->first();
  530. if ($message
  531. && $message->markable == true
  532. && $message->displayed == null) {
  533. $message->displayed = gmdate('Y-m-d H:i:s');
  534. $message->save();
  535. \Moxl\Stanza\Message::displayed($jid, $message->replaceid);
  536. }
  537. }
  538. /**
  539. * @brief Clear the history
  540. *
  541. * @param string $room
  542. */
  543. public function ajaxClearHistory($jid)
  544. {
  545. if (!$this->validateJid($jid)) {
  546. return;
  547. }
  548. $this->user->messages()->where(function ($query) use ($jid) {
  549. $query->where('jidfrom', $jid)
  550. ->orWhere('jidto', $jid);
  551. })->delete();
  552. $this->ajaxGet($jid);
  553. }
  554. public function prepareChat($jid, $muc = false)
  555. {
  556. $view = $this->tpl();
  557. $view->assign('jid', $jid);
  558. $view->assign('smiley', $this->call('ajaxSmiley'));
  559. $view->assign('emoji', prepareString('😀'));
  560. $view->assign('muc', $muc);
  561. $view->assign('anon', false);
  562. $view->assign(
  563. 'info',
  564. \App\Info::where('server', $this->user->session->host)
  565. ->where('node', '')
  566. ->first()
  567. );
  568. if ($muc) {
  569. $view->assign('room', $jid);
  570. $view->assign('conference', $this->user->session->conferences()
  571. ->where('conference', $jid)
  572. ->with('info')
  573. ->first());
  574. $mucinfo = \App\Info::where('server', explodeJid($jid)['server'])
  575. ->where('node', '')
  576. ->first();
  577. if ($mucinfo && !empty($mucinfo->abuseaddresses)) {
  578. $view->assign('info', $mucinfo);
  579. }
  580. } else {
  581. $view->assign('roster', $this->user->session->contacts()->where('jid', $jid)->first());
  582. $view->assign('contact', \App\Contact::firstOrNew(['id' => $jid]));
  583. }
  584. return $view->draw('_chat');
  585. }
  586. public function prepareMessages($jid, $muc = false, $seenOnly = false)
  587. {
  588. if (!$this->validateJid($jid)) {
  589. return;
  590. }
  591. $jid = echapJid($jid);
  592. $messagesQuery = $this->user->messages()->where(function ($query) use ($jid) {
  593. $query->where('jidfrom', $jid)
  594. ->orWhere('jidto', $jid);
  595. });
  596. $messagesQuery = $muc
  597. ? $messagesQuery->where('type', 'groupchat')->whereNull('subject')
  598. : $messagesQuery->whereIn('type', $this->_messageTypes);
  599. /**
  600. * The object need to be cloned there for MySQL, looks like the pagination/where is kept somewhere in between…
  601. **/
  602. $messagesRequest = clone $messagesQuery;
  603. $messagesCount = clone $messagesQuery;
  604. $messages = $messagesRequest->orderBy('published', 'desc')->take($this->_pagination)->get();
  605. $unreadsCount = $messagesCount->where('seen', false)->count();
  606. if ($unreadsCount > 0) {
  607. $messagesClear = clone $messagesQuery;
  608. $messagesClear->where('seen', false)->update(['seen' => true]);
  609. }
  610. if ($seenOnly) return;
  611. $messages = $messages->reverse();
  612. foreach ($messages as $message) {
  613. $this->prepareMessage($message);
  614. }
  615. $view = $this->tpl();
  616. $view->assign('jid', $jid);
  617. $view->assign('contact', \App\Contact::firstOrNew(['id' => $jid]));
  618. $view->assign('me', false);
  619. $view->assign('muc', $muc);
  620. $left = $view->draw('_chat_bubble');
  621. $view->assign('contact', \App\Contact::firstOrNew(['id' => $this->user->id]));
  622. $view->assign('me', true);
  623. $view->assign('muc', $muc);
  624. $right = $view->draw('_chat_bubble');
  625. $this->rpc('Chat.setSpecificElements', $left, $right);
  626. $this->rpc('Chat.appendMessagesWrapper', $this->_wrapper, false, true);
  627. $this->event($muc ? 'chat_open_room' : 'chat_open', $jid);
  628. $this->event('chat_counter', $this->user->unreads());
  629. $this->rpc('Chat.insertSeparator', $unreadsCount);
  630. }
  631. public function prepareMessage(&$message, $jid = null)
  632. {
  633. if ($jid != $message->jidto && $jid != $message->jidfrom && $jid != null) {
  634. return $this->_wrapper;
  635. }
  636. $message->jidto = echapJS($message->jidto);
  637. $message->jidfrom = echapJS($message->jidfrom);
  638. $emoji = \Movim\Emoji::getInstance();
  639. if (isset($message->html)) {
  640. $message->body = $message->html;
  641. } else {
  642. $message->addUrls();
  643. $message->body = $emoji->replace($message->body);
  644. $message->body = addHFR($message->body);
  645. }
  646. if (isset($message->subject) && $message->type == 'headline') {
  647. $message->body = $message->subject .': '. $message->body;
  648. }
  649. // Sticker message
  650. if (isset($message->sticker)) {
  651. $p = new Picture;
  652. $sticker = $p->get($message->sticker, false, false, 'png');
  653. $stickerSize = $p->getSize();
  654. if ($sticker == false
  655. && $message->jidfrom != $message->session) {
  656. $r = new Request;
  657. $r->setTo($message->jidfrom)
  658. ->setResource($message->resource)
  659. ->setCid($message->sticker)
  660. ->request();
  661. } else {
  662. $message->sticker = [
  663. 'url' => $sticker,
  664. 'width' => $stickerSize['width'],
  665. 'height' => $stickerSize['height']
  666. ];
  667. }
  668. }
  669. // Jumbo emoji
  670. if ($emoji->isSingleEmoji()
  671. && !isset($message->html)
  672. && in_array($message->type, ['chat', 'groupchat'])) {
  673. $message->sticker = [
  674. 'url' => $emoji->getLastSingleEmojiURL(),
  675. 'title' => ':'.$emoji->getLastSingleEmojiTitle().':',
  676. 'height' => 60,
  677. ];
  678. }
  679. // Attached file
  680. if (isset($message->file)) {
  681. // We proxify pictures links even if they are advertized as small ones
  682. if (\array_key_exists('type', $message->file)
  683. && typeIsPicture($message->file['type'])
  684. && $message->file['size'] <= SMALL_PICTURE_LIMIT) {
  685. $message->sticker = [
  686. 'thumb' => $this->route('picture', urlencode($message->file['uri'])),
  687. 'url' => $message->file['uri'],
  688. 'picture' => true
  689. ];
  690. }
  691. $url = parse_url($message->file['uri']);
  692. // Other image websites
  693. if (\array_key_exists('host', $url)) {
  694. switch ($url['host']) {
  695. case 'i.imgur.com':
  696. $matches = [];
  697. preg_match('/https:\/\/i.imgur.com\/([a-zA-Z0-9]{7})(.*)/', $message->file['uri'], $matches);
  698. if (!empty($matches)) {
  699. $message->sticker = [
  700. 'url' => $message->file['uri'],
  701. 'thumb' => 'https://i.imgur.com/' . $matches[1] . 'g' . $matches[2],
  702. 'picture' => true
  703. ];
  704. }
  705. break;
  706. }
  707. }
  708. // Build cards for the URIs
  709. $uri = explodeXMPPURI($message->file['uri']);
  710. switch ($uri['type']) {
  711. case 'post':
  712. $post = \App\Post::where('server', $uri['params'][0])
  713. ->where('node', $uri['params'][1])
  714. ->where('nodeid', $uri['params'][2])
  715. ->first();
  716. if ($post) {
  717. $p = new Post;
  718. $message->card = $p->prepareTicket($post);
  719. }
  720. break;
  721. }
  722. }
  723. // Reactions
  724. if ($message->reactions()->count()) {
  725. $message->reactionsHtml = $this->prepareReactions($message);
  726. }
  727. $message->rtl = isRTL($message->body);
  728. $message->publishedPrepared = prepareTime(strtotime($message->published));
  729. if ($message->delivered) {
  730. $message->delivered = prepareDate(strtotime($message->delivered), true);
  731. }
  732. if ($message->displayed) {
  733. $message->displayed = prepareDate(strtotime($message->displayed), true);
  734. }
  735. $date = prepareDate(strtotime($message->published), false, false, true);
  736. if (empty($date)) {
  737. $date = $this->__('date.today');
  738. }
  739. // We create the date wrapper
  740. if (!array_key_exists($date, $this->_wrapper)) {
  741. $this->_wrapper[$date] = [];
  742. }
  743. $messageDBSeen = $message->seen;
  744. $n = new Notification;
  745. if ($message->type == 'groupchat') {
  746. $message->color = stringToColor($message->session_id . $message->resource . $message->type);
  747. // Cache the resolved presences for a while
  748. $key = $message->jidfrom.$message->resource;
  749. if (!isset($this->mucPresences[$key])) {
  750. $this->mucPresences[$key] = $this->user->session->presences()
  751. ->where('jid', $message->jidfrom)
  752. ->where('resource', $message->resource)
  753. ->where('muc', true)
  754. ->first();
  755. }
  756. if ($this->mucPresences[$key] && $this->mucPresences[$key] !== true) {
  757. if ($url = $this->mucPresences[$key]->conferencePicture) {
  758. $message->icon_url = $url;
  759. }
  760. $message->moderator = ($this->mucPresences[$key]->mucrole == 'moderator');
  761. $message->mine = $message->seen = ($this->mucPresences[$key]->mucjid == $this->user->id);
  762. } else {
  763. $this->mucPresences[$key] = true;
  764. }
  765. $message->icon = firstLetterCapitalize($message->resource);
  766. if ($message->seen === false) {
  767. $message->seen = ('chat|'.$message->jidfrom.'|room' == $n->getCurrent());
  768. }
  769. } else {
  770. $message->seen = ('chat|'.$message->jidfrom == $n->getCurrent());
  771. }
  772. if ($message->seen === true
  773. && $messageDBSeen === false) {
  774. $this->user->messages()
  775. ->where('id', $message->id)
  776. ->update(['seen' => true]);
  777. }
  778. $msgkey = '<' . $message->jidfrom;
  779. $msgkey .= ($message->type == 'groupchat') ? $message->resource : '';
  780. $msgkey .= '>' . substr($message->published, 11, 5);
  781. $counter = count($this->_wrapper[$date]);
  782. $this->_wrapper[$date][$counter.$msgkey] = $message;
  783. if ($message->type == 'invitation') {
  784. $view = $this->tpl();
  785. $view->assign('message', $message);
  786. $message->body = $view->draw('_chat_invitation');
  787. }
  788. if ($message->type == 'jingle_start') {
  789. $view = $this->tpl();
  790. $view->assign('message', $message);
  791. $message->body = $view->draw('_chat_jingle_start');
  792. }
  793. if ($message->type == 'jingle_end') {
  794. $view = $this->tpl();
  795. $view->assign('message', $message);
  796. $view->assign('diff', false);
  797. $start = Message::where(
  798. [
  799. 'type' =>'jingle_start',
  800. 'thread'=> $message->thread
  801. ]
  802. )->first();
  803. if ($start) {
  804. $diff = (new DateTime($start->created_at))
  805. ->diff(new DateTime($message->created_at));
  806. $view->assign('diff', $diff);
  807. }
  808. $message->body = $view->draw('_chat_jingle_end');
  809. }
  810. return $this->_wrapper;
  811. }
  812. public function prepareReactions(Message $message)
  813. {
  814. $view = $this->tpl();
  815. $merged = [];
  816. $reactions = $message
  817. ->reactions()
  818. ->orderBy('created_at')
  819. ->get();
  820. foreach ($reactions as $reaction) {
  821. if (!array_key_exists($reaction->emoji, $merged)) {
  822. $merged[$reaction->emoji] = [];
  823. }
  824. $merged[$reaction->emoji][] = $reaction->jidfrom;
  825. }
  826. $view->assign('message', $message);
  827. $view->assign('reactions', $merged);
  828. $view->assign('me', $this->user->id);
  829. return $view->draw('_chat_reactions');
  830. }
  831. public function prepareEmpty()
  832. {
  833. $view = $this->tpl();
  834. $conferences = \App\Info::where('category', 'conference')
  835. ->whereNotIn('server', $this->user->session->conferences()->pluck('conference')->toArray())
  836. ->where('mucpublic', true)
  837. ->where('mucpersistent', true);
  838. $conferences = (Configuration::get()->restrictsuggestions)
  839. ? $conferences->where('server', 'like', '%@%.' . $this->user->session->host)
  840. : $conferences->where('server', 'like', '%@%');
  841. $conferences = $conferences->orderBy('occupants', 'desc')->take(8)->get();
  842. $chats = \App\Cache::c('chats');
  843. if ($chats == null) {
  844. $chats = [];
  845. }
  846. $chats[$this->user->id] = true;
  847. $top = $this->user->session->topContacts()
  848. ->join(DB::raw('(
  849. select min(value) as value, jid as pjid
  850. from presences
  851. group by jid) as presences
  852. '), 'presences.pjid', '=', 'rosters.jid')
  853. ->where('value', '<', 5)
  854. ->whereNotIn('rosters.jid', array_keys($chats))
  855. ->with('presence.capability')
  856. ->take(8)
  857. ->get();
  858. $view->assign('conferences', $conferences);
  859. $view->assign('top', $top);
  860. return $view->draw('_chat_empty');
  861. }
  862. private function prepareComposeList(array $list)
  863. {
  864. $view = $this->tpl();
  865. $view->assign('list', implode(', ', $list));
  866. return $view->draw('_chat_compose_list');
  867. }
  868. /**
  869. * @brief Validate the jid
  870. *
  871. * @param string $jid
  872. */
  873. private function validateJid($jid)
  874. {
  875. return (Validator::stringType()->noWhitespace()->length(6, 256)->validate($jid));
  876. }
  877. public function getSmileyPath($id)
  878. {
  879. return getSmileyPath($id);
  880. }
  881. public function display()
  882. {
  883. $this->view->assign('pagination', $this->_pagination);
  884. }
  885. }