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.

1587 lines
50 KiB

5 years ago
9 years ago
8 years ago
8 years ago
8 years ago
4 years ago
8 years ago
4 years ago
8 years ago
8 years ago
8 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
4 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\Contact;
  7. use App\Message;
  8. use App\MessageFile;
  9. use App\MessageOmemoHeader;
  10. use App\Reaction;
  11. use App\Url;
  12. use Moxl\Xec\Action\BOB\Request;
  13. use Moxl\Xec\Action\Disco\Request as DiscoRequest;
  14. use Respect\Validation\Validator;
  15. use Illuminate\Database\Capsule\Manager as DB;
  16. use Movim\ChatStates;
  17. use Movim\ChatOwnState;
  18. use Movim\EmbedLight;
  19. use Movim\Image;
  20. class Chat extends \Movim\Widget\Base
  21. {
  22. private $_pagination = 50;
  23. private $_wrapper = [];
  24. private $_messageTypes = ['chat', 'headline', 'invitation', 'jingle_incoming', 'jingle_outgoing', 'jingle_end'];
  25. private $_mucPresences = [];
  26. public function load()
  27. {
  28. $this->addjs('chat.js');
  29. $this->addcss('chat.css');
  30. $this->registerEvent('carbons', 'onMessage');
  31. $this->registerEvent('message', 'onMessage');
  32. $this->registerEvent('presence', 'onPresence', 'chat');
  33. $this->registerEvent('retracted', 'onRetracted');
  34. $this->registerEvent('receiptack', 'onMessageReceipt');
  35. $this->registerEvent('displayed', 'onMessage', 'chat');
  36. $this->registerEvent('mam_get_handle', 'onMAMRetrieved');
  37. $this->registerEvent('mam_get_handle_muc', 'onMAMMucRetrieved', 'chat');
  38. $this->registerEvent('mam_get_handle_contact', 'onMAMContactRetrieved', 'chat');
  39. $this->registerEvent('chatstate', 'onChatState', 'chat');
  40. //$this->registerEvent('subject', 'onConferenceSubject', 'chat'); Spam the UI during authentication
  41. $this->registerEvent('muc_setsubject_handle', 'onConferenceSubject', 'chat');
  42. $this->registerEvent('muc_getconfig_handle', 'onRoomConfig', 'chat');
  43. $this->registerEvent('muc_setconfig_handle', 'onRoomConfigSaved', 'chat');
  44. $this->registerEvent('muc_setconfig_error', 'onRoomConfigError', 'chat');
  45. $this->registerEvent('presence_muc_handle', 'onMucConnected', 'chat');
  46. $this->registerEvent('message_publish_error', 'onPublishError', 'chat');
  47. $this->registerEvent('chat_counter', 'onCounter', 'chat');
  48. $this->registerEvent('jingle_message', 'onJingleMessage');
  49. $this->registerEvent('bob_request_handle', 'onSticker');
  50. $this->registerEvent('notification_counter_clear', 'onNotificationCounterClear');
  51. }
  52. public function onPresence($packet)
  53. {
  54. if ($packet->content && $jid = $packet->content->jid) {
  55. $arr = explode('|', (new Notification)->getCurrent());
  56. if (isset($arr[1]) && $jid == $arr[1] && !$packet->content->muc) {
  57. $this->ajaxGetHeader($jid);
  58. }
  59. }
  60. }
  61. public function onJingleMessage($packet)
  62. {
  63. $this->onMessage($packet, false, false);
  64. }
  65. public function onMessageReceipt($packet)
  66. {
  67. $this->onMessage($packet, false, true);
  68. }
  69. public function onRetracted($packet)
  70. {
  71. $this->onMessage($packet, false, true);
  72. }
  73. public function onCounter($count)
  74. {
  75. $this->rpc('MovimTpl.fill', '#chatheadercounter', $this->prepareChatCounter($count));
  76. }
  77. private function prepareChatCounter(int $count = 0)
  78. {
  79. $view = $this->tpl();
  80. $view->assign('count', $count);
  81. return $view->draw('_chat_counter');
  82. }
  83. public function onNotificationCounterClear($params)
  84. {
  85. list($page, $jid) = array_pad($params, 3, null);
  86. if ($page === 'chat') {
  87. // Check if the jid is a connected chatroom
  88. $presence = $this->user->session->presences()
  89. ->where('jid', $jid)
  90. ->where('mucjid', $this->user->id)
  91. ->first();
  92. $this->prepareMessages($jid, ($presence), true);
  93. }
  94. }
  95. public function onPublishError($packet)
  96. {
  97. Toast::send(
  98. $packet->content ??
  99. $this->__('chat.publish_error')
  100. );
  101. }
  102. public function onMessage($packet, $history = false, $receipt = false)
  103. {
  104. $message = $packet->content;
  105. $from = null;
  106. $chatStates = ChatStates::getInstance();
  107. $rawbody = $message->body;
  108. if ($message->isEmpty() && !in_array($message->type, ['jingle_incoming', 'jingle_outgoing', 'jingle_end'])) {
  109. return;
  110. }
  111. if ($message->file) {
  112. $rawbody = '📄 ' . $this->__('avatar.file');
  113. if (typeIsPicture($message->file['type'])) {
  114. $rawbody = '🖼️ ' . $this->__('chats.picture');
  115. }
  116. if (typeIsVideo($message->file['type'])) {
  117. $rawbody = '🎞️ ' . $this->__('chats.video');
  118. }
  119. }
  120. if ($message->user_id == $message->jidto
  121. && !$history
  122. && !$message->isEmpty()
  123. && $message->seen == false
  124. && $message->jidfrom != $message->jidto) {
  125. $from = $message->jidfrom;
  126. $contact = App\Contact::firstOrNew(['id' => $from]);
  127. $conference = $message->type == 'groupchat'
  128. ? $this->user->session
  129. ->conferences()->where('conference', $from)
  130. ->first()
  131. : null;
  132. if ($contact != null
  133. && $message->type != 'groupchat'
  134. && !$message->retracted
  135. && !$message->oldid) {
  136. $roster = $this->user->session->contacts()->where('jid', $from)->first();
  137. $chatStates->clearState($from);
  138. $name = $roster ? $roster->truename : $contact->truename;
  139. // Specific case where the message is a MUC PM
  140. $jid = explodeJid($message->jidfrom);
  141. if ($jid['username'] == $name && $jid['resource'] == $message->resource) {
  142. $name = $message->resource;
  143. }
  144. Notification::rpcCall('Notification.incomingMessage');
  145. Notification::append(
  146. 'chat|'.$from,
  147. $name,
  148. $message->encrypted && is_array($message->omemoheader)
  149. ? "🔒 " . substr($message->omemoheader['payload'], 0, strlen($message->omemoheader['payload'])/2)
  150. : $rawbody,
  151. $contact->getPhoto(),
  152. 4,
  153. $this->route('chat', $contact->jid)
  154. );
  155. }
  156. // If it's a groupchat message
  157. elseif ($message->type == 'groupchat'
  158. && !$message->retracted
  159. && $conference
  160. && (($conference->notify == 1 && $message->quoted) // When quoted
  161. || $conference->notify == 2) // Always
  162. && !$receipt) {
  163. Notification::rpcCall('Notification.incomingMessage');
  164. Notification::append(
  165. 'chat|'.$from,
  166. ($conference != null && $conference->name)
  167. ? $conference->name
  168. : $from,
  169. $message->resource.': '.$rawbody,
  170. $conference->getPhoto(),
  171. 4,
  172. $this->route('chat', [$contact->jid, 'room'])
  173. );
  174. } elseif ($message->type == 'groupchat') {
  175. if ($conference && $conference->notify == 0) {
  176. $message->seen = true;
  177. $message->save();
  178. }
  179. $chatStates->clearState($from, $message->resource);
  180. }
  181. $this->onChatState($chatStates->getState($from));
  182. }
  183. $this->rpc('Chat.appendMessagesWrapper', $this->prepareMessage($message, $from));
  184. $this->event('chat_counter', $this->user->unreads());
  185. }
  186. public function onSticker($packet)
  187. {
  188. list($to, $cid) = array_values($packet->content);
  189. $this->ajaxGet($to);
  190. }
  191. public function onChatState(array $array, $first = true)
  192. {
  193. if (isset($array[1])) {
  194. $this->setState(
  195. $array[0],
  196. is_array($array[1]) && !empty($array[1])
  197. ? $this->prepareComposeList(array_keys($array[1]))
  198. : $this->__('message.composing'),
  199. $first
  200. );
  201. } else {
  202. $this->setState($array[0], '', $first);
  203. }
  204. }
  205. public function onConferenceSubject($packet)
  206. {
  207. $this->ajaxGetRoom($packet->content->jidfrom, false, true);
  208. }
  209. public function onMAMRetrieved()
  210. {
  211. Toast::send($this->__('chat.mam_retrieval'));
  212. }
  213. public function onMAMMucRetrieved($packet)
  214. {
  215. $this->ajaxGetRoom($packet->content, true, true);
  216. }
  217. public function onMAMContactRetrieved($packet)
  218. {
  219. $this->ajaxGet($packet->content, true);
  220. }
  221. public function onMucConnected($packet)
  222. {
  223. list($content, $notify) = $packet->content;
  224. if ($notify) {
  225. $this->ajaxGetRoom($content->jid, false, true);
  226. }
  227. }
  228. public function onRoomConfigError($packet)
  229. {
  230. Toast::send($packet->content);
  231. }
  232. public function onRoomConfig($packet)
  233. {
  234. list($config, $room) = array_values($packet->content);
  235. $view = $this->tpl();
  236. $xml = new \XMPPtoForm;
  237. $form = $xml->getHTML($config->x);
  238. $view->assign('form', $form);
  239. $view->assign('room', $room);
  240. Dialog::fill($view->draw('_chat_config_room'), true);
  241. }
  242. public function onRoomConfigSaved($packet)
  243. {
  244. $r = new DiscoRequest;
  245. $r->setTo($packet->content)
  246. ->request();
  247. Toast::send($this->__('chatroom.config_saved'));
  248. }
  249. private function setState(string $jid, string $message, $first = true)
  250. {
  251. if ($first) {
  252. $this->rpc('MovimUtils.removeClass', '#' . cleanupId($jid.'_state'), 'first');
  253. }
  254. $this->rpc('MovimTpl.fill', '#' . cleanupId($jid.'_state'), $message);
  255. }
  256. public function ajaxInit()
  257. {
  258. $view = $this->tpl();
  259. $date = $view->draw('_chat_date');
  260. $separator = $view->draw('_chat_separator');
  261. $this->rpc('Chat.setGeneralElements', $date, $separator);
  262. $this->rpc('Chat.setConfig',
  263. $this->_pagination,
  264. $this->__('message.error'),
  265. $this->__('chat.action_impossible_encrypted')
  266. );
  267. }
  268. public function ajaxClearCounter(string $jid)
  269. {
  270. $this->prepareMessages($jid, false, true, false);
  271. }
  272. /**
  273. * Get the header
  274. */
  275. public function ajaxGetHeader(string $jid, bool $muc = false)
  276. {
  277. $this->rpc(
  278. 'MovimTpl.fill',
  279. '#' . cleanupId($jid.'_header'),
  280. $this->prepareHeader($jid, $muc)
  281. );
  282. $chatStates = ChatStates::getInstance();
  283. $this->onChatState($chatStates->getState($jid), false);
  284. }
  285. public function ajaxHttpGetEmpty()
  286. {
  287. $this->ajaxGet();
  288. }
  289. /**
  290. * @brief Get a discussion
  291. * @param string $jid
  292. */
  293. public function ajaxGet(string $jid = null, ?bool $light = false)
  294. {
  295. if ($jid == null) {
  296. $this->rpc('MovimTpl.hidePanel');
  297. $this->rpc('Notification.current', 'chat');
  298. $this->rpc('MovimUtils.pushState', $this->route('chat'));
  299. if ($light == false) {
  300. $this->rpc('MovimTpl.fill', '#chat_widget', $this->prepareEmpty());
  301. }
  302. } else {
  303. if ($light == false) {
  304. $this->rpc('MovimUtils.pushState', $this->route('chat', $jid));
  305. $this->rpc('MovimTpl.fill', '#chat_widget', $this->prepareChat($jid));
  306. $chatStates = ChatStates::getInstance();
  307. $this->onChatState($chatStates->getState($jid), false);
  308. $this->rpc('MovimTpl.showPanel');
  309. $this->rpc('Chat.focus');
  310. }
  311. $this->rpc('Chat.setObservers');
  312. $this->prepareMessages($jid);
  313. $this->rpc('Notification.current', 'chat|'.$jid);
  314. $this->rpc('Chat.scrollToSeparator');
  315. // OMEMO
  316. $this->rpc(
  317. 'Chat.setBundlesIds',
  318. $jid,
  319. $this->user->bundles()
  320. ->where('jid', $jid)
  321. ->select(['bundleid', 'jid'])
  322. ->get()
  323. ->mapToGroups(function ($tuple) {
  324. return [$tuple['jid'] => $tuple['bundleid']];
  325. })
  326. ->toArray()
  327. );
  328. }
  329. }
  330. /**
  331. * @brief Get a chatroom
  332. * @param string $jid
  333. */
  334. public function ajaxGetRoom(string $room, $light = false, $noConnect = false)
  335. {
  336. if (!$this->validateJid($room)) {
  337. return;
  338. }
  339. $conference = $this->user->session->conferences()->where('conference', $room)->with('members')->first();
  340. if ($conference) {
  341. if (!$conference->connected && !$noConnect) {
  342. $this->rpc('Rooms_ajaxJoin', $conference->conference, $conference->nick);
  343. }
  344. if ($light == false) {
  345. $this->rpc('MovimUtils.pushState', $this->route('chat', [$room, 'room']));
  346. $this->rpc('MovimTpl.fill', '#chat_widget', $this->prepareChat($room, true));
  347. $chatStates = ChatStates::getInstance();
  348. $this->onChatState($chatStates->getState($room), false);
  349. $this->rpc('MovimTpl.showPanel');
  350. $this->rpc('Chat.focus');
  351. }
  352. $this->rpc('Chat.setObservers');
  353. $this->prepareMessages($room, true);
  354. $this->rpc('Notification.current', 'chat|'.$room);
  355. $this->rpc('Chat.scrollToSeparator');
  356. // OMEMO
  357. if ($conference->isGroupChat()) {
  358. $this->rpc('Chat.setGroupChatMembers', $conference->members->pluck('jid')->toArray());
  359. $this->rpc(
  360. 'Chat.setBundlesIds',
  361. $room,
  362. $this->user->bundles()
  363. ->whereIn('jid', function ($query) use ($room) {
  364. $query->select('jid')
  365. ->from('members')
  366. ->where('conference', $room);
  367. })
  368. ->select(['bundleid', 'jid'])
  369. ->get()
  370. ->mapToGroups(function ($tuple) {
  371. return [$tuple['jid'] => $tuple['bundleid']];
  372. })
  373. ->toArray()
  374. );
  375. }
  376. } else {
  377. $this->rpc('RoomsUtils_ajaxAdd', $room);
  378. }
  379. }
  380. /**
  381. * @brief Send a message
  382. */
  383. public function ajaxHttpDaemonSendMessage(
  384. string $to,
  385. string $message,
  386. bool $muc = false,
  387. $file = null,
  388. ?int $replyToMid = 0,
  389. ?bool $mucReceipts = false,
  390. $omemo = null
  391. ) {
  392. $messageFile = null;
  393. $messageOMEMOHeader = null;
  394. if ($file) {
  395. $messageFile = new MessageFile;
  396. $messageFile->import($file);
  397. if (!$messageFile->valid) $messageFile = null;
  398. } else {
  399. try {
  400. $url = new Url;
  401. $cache = $url->resolve(trim($message));
  402. if ($cache && $url->file !== null) {
  403. $messageFile = $url->file;
  404. }
  405. } catch (\Exception $e) {}
  406. }
  407. if ($omemo) {
  408. $messageOMEMOHeader = new MessageOMEMOHeader;
  409. $messageOMEMOHeader->import($omemo);
  410. }
  411. $this->sendMessage($to, $message, $muc, null, $messageFile, $replyToMid, $mucReceipts, $messageOMEMOHeader);
  412. }
  413. /**
  414. * @brief Send a resolved message
  415. *
  416. * @param string $to
  417. * @param string $message
  418. * @return void
  419. */
  420. public function sendMessage(string $to, string $message = '', bool $muc = false,
  421. ?Message $replace = null, ?MessageFile $file = null, ?int $replyToMid = 0,
  422. ?bool $mucReceipts = false, ?MessageOMEMOHeader $messageOMEMOHeader = null)
  423. {
  424. $tempId = null;
  425. if ($messageOMEMOHeader) {
  426. $tempId = $message;
  427. $message = 'Encrypted OMEMO message sent';
  428. }
  429. $body = ($file != null && $file->type != 'xmpp/uri')
  430. ? $file->uri
  431. : $message;
  432. if ($body == '' || $body == '/me') {
  433. return;
  434. }
  435. $m = new \App\Message;
  436. $m->id = generateUUID();
  437. $m->thread = generateUUID();
  438. $m->originid = $m->id;
  439. $m->replaceid = $replace ? $replace->originid : null;
  440. $m->user_id = $this->user->id;
  441. $m->jidto = echapJid($to);
  442. $m->jidfrom = $this->user->id;
  443. $m->published = gmdate('Y-m-d H:i:s');
  444. $reply = null;
  445. if ($replyToMid !== 0) {
  446. $reply = $this->user->messages()
  447. ->where('mid', $replyToMid)
  448. ->first();
  449. if ($reply) {
  450. // See https://xmpp.org/extensions/xep-0201.html#new
  451. $m->thread = $reply->thread;
  452. $m->parentmid = $reply->mid;
  453. }
  454. }
  455. $m->markable = true;
  456. $m->seen = true;
  457. $m->type = 'chat';
  458. $m->resource = $this->user->session->resource;
  459. if ($muc) {
  460. $m->type = 'groupchat';
  461. $m->resource = $this->user->session->username;
  462. $m->jidfrom = $to;
  463. }
  464. // We decode URL codes to send the correct message to the XMPP server
  465. $p = new Publish;
  466. $p->setTo($to);
  467. $p->setReplace($m->replaceid);
  468. $p->setId($m->id);
  469. $p->setThreadid($m->thread);
  470. $p->setOriginid($m->originid);
  471. if ($muc) {
  472. $p->setMuc();
  473. if ($mucReceipts) {
  474. $p->setMucReceipts();
  475. }
  476. }
  477. if ($file) {
  478. $m->file = (array)$file;
  479. $p->setFile($file);
  480. }
  481. if ($reply) {
  482. $quotable = false;
  483. // https://xmpp.org/extensions/xep-0461.html#business-id
  484. if ($reply->type == 'groupchat' && substr($reply->id, 0, 2) != 'm_') {
  485. // stanza-id only
  486. $p->setReplyid($reply->id);
  487. $quotable = true;
  488. } elseif ($reply->type != 'groupchat' && $reply->originid) {
  489. $p->setReplyid($reply->originid);
  490. $quotable = true;
  491. } elseif ($reply->type != 'groupchat' && substr($reply->id, 0, 2) != 'm_') {
  492. $p->setReplyid($reply->id);
  493. $quotable = true;
  494. }
  495. if ($quotable) {
  496. $p->setReplyto($reply->jidfrom.'/'.$reply->resource);
  497. $p->setReplyquotedbodylength(
  498. mb_strlen(htmlspecialchars($reply->body, ENT_NOQUOTES)) + 2 // 2 = > quote character
  499. );
  500. // Prepend quoted message body
  501. $quotedBody = preg_replace('/^/m', '> ', $reply->body) . "\n";
  502. $p->setContent($quotedBody . $body);
  503. } else {
  504. $p->setContent($body);
  505. }
  506. } else {
  507. $p->setContent($body);
  508. }
  509. $m->body = $body;
  510. if ($messageOMEMOHeader) {
  511. $m->encrypted = true;
  512. $m->omemoheader = (string)$messageOMEMOHeader;
  513. $m->bundleid = $messageOMEMOHeader->sid;
  514. $p->setMessageOMEMO($messageOMEMOHeader);
  515. }
  516. (ChatOwnState::getInstance())->halt();
  517. $p->request();
  518. // We sent the published id back
  519. if ($tempId) {
  520. $this->rpc('Chat.sentId', $tempId, $m->id);
  521. }
  522. /* Is it really clean ? */
  523. if (!$p->getMuc()) {
  524. $m->body = htmlentities(trim($m->body), ENT_XML1, 'UTF-8');
  525. $m->save();
  526. $m = $m->fresh();
  527. $packet = new \Moxl\Xec\Payload\Packet;
  528. $packet->content = $m;
  529. // We refresh the Chats list
  530. $c = new Chats;
  531. $c->onMessage($packet);
  532. $this->onMessage($packet);
  533. }
  534. }
  535. /**
  536. * @brief Send a correction message
  537. *
  538. * @param string $to
  539. * @param string $message
  540. * @return void
  541. */
  542. public function ajaxHttpDaemonCorrect(string $to, int $mid, string $message = '')
  543. {
  544. $replace = $this->user->messages()
  545. ->where('mid', $mid)
  546. ->first();
  547. if ($replace) {
  548. $this->sendMessage($to, $message, $replace->isMuc(), $replace);
  549. }
  550. }
  551. /**
  552. * @brief Send a reaction
  553. */
  554. public function ajaxHttpDaemonSendReaction(string $mid, string $emoji)
  555. {
  556. $parentMessage = $this->user->messages()
  557. ->where('mid', $mid)
  558. ->first();
  559. $emojiHandler = \Movim\Emoji::getInstance();
  560. $emojiHandler->replace($emoji);
  561. if ($parentMessage && $emojiHandler->isSingleEmoji()) {
  562. // Try to load the MUC presence and resolve the resource
  563. $mucPresence = null;
  564. if ($parentMessage->type == 'groupchat') {
  565. $mucPresence = $this->user->session->presences()
  566. ->where('jid', $parentMessage->jidfrom)
  567. ->where('mucjid', $this->user->id)
  568. ->where('muc', true)
  569. ->first();
  570. if (!$mucPresence) return;
  571. }
  572. $jidfrom = ($parentMessage->type == 'groupchat')
  573. ? $mucPresence->resource
  574. : $this->user->id;
  575. $emojis = $parentMessage->reactions()
  576. ->where('jidfrom', $jidfrom)
  577. ->get();
  578. $r = new Reactions;
  579. $newEmojis = [];
  580. // This reaction was not published yet
  581. if ($emojis->where('emoji', $emoji)->count() == 0) {
  582. $reaction = new Reaction;
  583. $reaction->message_mid = $parentMessage->mid;
  584. $reaction->jidfrom = ($parentMessage->type == 'groupchat')
  585. ? $this->user->session->username
  586. : $this->user->id;
  587. $reaction->emoji = $emoji;
  588. if ($parentMessage->type != 'groupchat') {
  589. $reaction->save();
  590. }
  591. $newEmojis = $emojis->push($reaction);
  592. } else {
  593. if ($parentMessage->type != 'groupchat') {
  594. $parentMessage->reactions()
  595. ->where('jidfrom', $jidfrom)
  596. ->where('emoji', $emoji)
  597. ->delete();
  598. }
  599. $newEmojis = $emojis->filter(function ($value, $key) use ($emoji) {
  600. return $value->emoji != $emoji;
  601. });
  602. }
  603. $r->setTo($parentMessage->jidfrom != $parentMessage->user_id
  604. ? $parentMessage->jidfrom
  605. : $parentMessage->jidto)
  606. ->setId(\generateUUID())
  607. ->setParentId(!$parentMessage->isMuc() && $parentMessage->originid
  608. ? $parentMessage->originid
  609. : $parentMessage->id)
  610. ->setReactions($newEmojis->pluck('emoji')->toArray());
  611. if ($parentMessage->type == 'groupchat') {
  612. $r->setMuc();
  613. }
  614. $r->request();
  615. if ($parentMessage->type != 'groupchat') {
  616. $packet = new \Moxl\Xec\Payload\Packet;
  617. $packet->content = $parentMessage;
  618. $this->onMessage($packet);
  619. }
  620. }
  621. }
  622. /**
  623. * @brief Refresh a message
  624. */
  625. public function ajaxRefreshMessage(string $mid)
  626. {
  627. $message = $this->user->messages()
  628. ->where('mid', $mid)
  629. ->first();
  630. if ($message) {
  631. $this->rpc('Chat.appendMessagesWrapper', $this->prepareMessage($message, null));
  632. }
  633. }
  634. /**
  635. * @brief Get the last message sent
  636. *
  637. * @param string $to
  638. * @return void
  639. */
  640. public function ajaxLast($to, $muc = false)
  641. {
  642. if ($muc) {
  643. // Resolve the current presence
  644. $presence = $this->user->session->presences()
  645. ->where('jid', $to)
  646. ->where('muc', true)
  647. ->where('mucjid', $this->user->id)
  648. ->first();
  649. if ($presence) {
  650. $m = $this->user->messages()
  651. ->where('type', 'groupchat')
  652. ->where('jidfrom', $to)
  653. ->where('jidto', $this->user->id)
  654. ->where('resource', $presence->resource)
  655. ->orderBy('published', 'desc')
  656. ->first();
  657. }
  658. } else {
  659. $m = $this->user->messages()
  660. ->where('jidto', $to)
  661. ->orderBy('published', 'desc')
  662. ->first();
  663. }
  664. if (!$m) return;
  665. // We might get an already edited message, be sure to load the id of the original one
  666. $mid = $m->mid;
  667. if ($m && !empty($m->replaceid)) {
  668. $originalMessage = $this->user->messages()
  669. ->where('originid', $m->replaceid)
  670. ->first();
  671. if ($originalMessage) {
  672. $mid = $originalMessage->mid;
  673. }
  674. }
  675. if ($m
  676. && !isset($m->sticker)
  677. && !isset($m->file)
  678. && !empty($m->body)) {
  679. $this->rpc('Chat.setTextarea', htmlspecialchars_decode($m->body), $mid);
  680. }
  681. }
  682. /**
  683. * @brief Get a sent message
  684. *
  685. * @param string $mid
  686. * @return void
  687. */
  688. public function ajaxEdit($mid)
  689. {
  690. $m = $this->user->messages()
  691. ->where('mid', $mid)
  692. ->first();
  693. if ($m
  694. && !isset($m->sticker)
  695. && !isset($m->file)) {
  696. $this->rpc('Chat.setTextarea', htmlspecialchars_decode($m->body), $mid);
  697. }
  698. }
  699. /**
  700. * @brief Reply to a message
  701. *
  702. * @param string $mid
  703. * @return void
  704. */
  705. public function ajaxHttpDaemonReply($mid)
  706. {
  707. $m = $this->user->messages()
  708. ->where('mid', $mid)
  709. ->first();
  710. if (($m->id && substr($m->id, 0, 2) != 'm_') || isset($m->thread)) {
  711. $view = $this->tpl();
  712. $view->assign('message', $m);
  713. $this->rpc('MovimTpl.fill', '#reply', $view->draw('_chat_reply'));
  714. $this->rpc('Chat.focus');
  715. }
  716. }
  717. /**
  718. * Clear the Reply box
  719. */
  720. public function ajaxClearReply()
  721. {
  722. $this->rpc('MovimTpl.fill', '#reply', '');
  723. }
  724. /**
  725. * @brief Send a "composing" message
  726. *
  727. * @param string $to
  728. * @return void
  729. */
  730. public function ajaxSendComposing($to, $muc = false)
  731. {
  732. if (!$this->validateJid($to)) {
  733. return;
  734. }
  735. (ChatOwnState::getInstance())->composing($to, $muc);
  736. }
  737. /**
  738. * @brief Get the chat history
  739. *
  740. * @param string jid
  741. * @param string time
  742. */
  743. public function ajaxGetHistory($jid, $date, $muc = false, $prepend = true)
  744. {
  745. if (!$this->validateJid($jid) && isset($date)) {
  746. return;
  747. }
  748. $messages = \App\Message::jid($jid)
  749. ->where('published', $prepend ? '<' : '>', date(MOVIM_SQL_DATE, strtotime($date)));
  750. $messages = $muc
  751. ? $messages->where('type', 'groupchat')->whereNull('subject')
  752. : $messages->whereIn('type', $this->_messageTypes);
  753. $messages = $messages->orderBy('published', 'desc')
  754. ->withCount('reactions')
  755. ->take($this->_pagination)
  756. ->get();
  757. if ($messages->count() > 0) {
  758. if ($prepend) {
  759. Toast::send($this->__('message.history', $messages->count()));
  760. } else {
  761. $messages = $messages->reverse();
  762. }
  763. foreach ($messages as $message) {
  764. $this->prepareMessage($message);
  765. }
  766. $this->rpc('Chat.appendMessagesWrapper', $this->_wrapper, $prepend);
  767. $this->_wrapper = [];
  768. }
  769. }
  770. /**
  771. * @brief Configure a room
  772. *
  773. * @param string $room
  774. */
  775. public function ajaxGetRoomConfig($room)
  776. {
  777. if (!$this->validateJid($room)) {
  778. return;
  779. }
  780. $gc = new GetConfig;
  781. $gc->setTo($room)
  782. ->request();
  783. }
  784. /**
  785. * @brief Save the room configuration
  786. *
  787. * @param string $room
  788. */
  789. public function ajaxSetRoomConfig($data, $room)
  790. {
  791. if (!$this->validateJid($room)) {
  792. return;
  793. }
  794. $sc = new SetConfig;
  795. $sc->setTo($room)
  796. ->setData($data)
  797. ->request();
  798. }
  799. /**
  800. * @brief Set last displayed message
  801. */
  802. public function ajaxDisplayed($jid, $id)
  803. {
  804. if (!$this->validateJid($jid)) {
  805. return;
  806. }
  807. $message = $this->user->messages()->where('id', $id)->first();
  808. if ($message
  809. && $message->markable == true
  810. && $message->displayed == null) {
  811. $message->displayed = gmdate('Y-m-d H:i:s');
  812. $message->save();
  813. \Moxl\Stanza\Message::displayed(
  814. $jid,
  815. $message->originid ?? $message->id,
  816. $message->type
  817. );
  818. }
  819. }
  820. /**
  821. * @brief Ask to clear the history
  822. *
  823. * @param string $jid
  824. */
  825. public function ajaxClearHistory($jid)
  826. {
  827. $view = $this->tpl();
  828. $view->assign('jid', $jid);
  829. $view->assign('count', \App\Message::jid($jid)->count());
  830. Dialog::fill($view->draw('_chat_clear'));
  831. }
  832. /**
  833. * @brief Clear the history
  834. *
  835. * @param string $jid
  836. */
  837. public function ajaxClearHistoryConfirm($jid)
  838. {
  839. if (!$this->validateJid($jid)) {
  840. return;
  841. }
  842. \App\Message::whereIn('id', function ($query) use ($jid) {
  843. $jidFromToMessages = DB::table('messages')
  844. ->where('user_id', $this->user->id)
  845. ->where('jidfrom', $jid)
  846. ->unionAll(DB::table('messages')
  847. ->where('user_id', $this->user->id)
  848. ->where('jidto', $jid)
  849. );
  850. $query->select('id')->from(
  851. $jidFromToMessages,
  852. 'messages'
  853. )->where('user_id', $this->user->id);
  854. })->delete();
  855. $this->ajaxGet($jid);
  856. }
  857. public function prepareChat($jid, $muc = false)
  858. {
  859. $view = $this->tpl();
  860. $view->assign('jid', $jid);
  861. $view->assign('muc', $muc);
  862. $view->assign('emoji', prepareString('😀'));
  863. if ($muc) {
  864. $view->assign('conference', $this->user->session->conferences()
  865. ->where('conference', $jid)
  866. ->with('info')
  867. ->first());
  868. }
  869. return $view->draw('_chat');
  870. }
  871. public function prepareMessages($jid, $muc = false, $seenOnly = false, $event = true)
  872. {
  873. if (!$this->validateJid($jid)) {
  874. return;
  875. }
  876. $jid = echapJid($jid);
  877. $messagesQuery = \App\Message::jid($jid);
  878. $messagesQuery = $muc
  879. ? $messagesQuery->where('type', 'groupchat')->whereNull('subject')
  880. : $messagesQuery->whereIn('type', $this->_messageTypes);
  881. /**
  882. * The object need to be cloned there for MySQL, looks like the pagination/where is kept somewhere in between…
  883. **/
  884. $messagesRequest = clone $messagesQuery;
  885. $messagesCount = clone $messagesQuery;
  886. $messages = $messagesRequest->withCount('reactions')->orderBy('published', 'desc')->take($this->_pagination)->get();
  887. $unreadsCount = $messagesCount->where('seen', false)->count();
  888. if ($unreadsCount > 0) {
  889. $messagesClear = clone $messagesQuery;
  890. // Two queries as Eloquent doesn't seems to map correctly the parameters
  891. \App\Message::whereIn('mid', $messagesClear->where('seen', false)->pluck('mid'))->update(['seen' => true]);
  892. }
  893. // Prepare the muc presences if possible
  894. $firstMessage = $messages->first();
  895. if ($firstMessage && $firstMessage->type == 'groupchat') {
  896. $this->_mucPresences = $this->user->session->presences()
  897. ->where('jid', $firstMessage->jidfrom)
  898. ->where('muc', true)
  899. ->whereIn('resource', $messages->pluck('resource')->unique())
  900. ->get()
  901. ->keyBy(function($presence) {
  902. return $presence->jid.$presence->resource;
  903. });
  904. }
  905. if (!$seenOnly) {
  906. $messages = $messages->reverse();
  907. foreach ($messages as $message) {
  908. $this->prepareMessage($message);
  909. }
  910. $view = $this->tpl();
  911. $view->assign('jid', $jid);
  912. $view->assign('contact', \App\Contact::firstOrNew(['id' => $jid]));
  913. $view->assign('me', false);
  914. $view->assign('muc', $muc);
  915. $left = $view->draw('_chat_bubble');
  916. $view->assign('contact', \App\Contact::firstOrNew(['id' => $this->user->id]));
  917. $view->assign('me', true);
  918. $view->assign('muc', $muc);
  919. $right = $view->draw('_chat_bubble');
  920. $this->rpc('Chat.setSpecificElements', $left, $right);
  921. $this->rpc('Chat.appendMessagesWrapper', $this->_wrapper, false);
  922. }
  923. if ($messages->count() == 0 && !$muc) {
  924. //$chats = new Chats;
  925. //$chats->ajaxGetHistory($jid);
  926. }
  927. if ($event) {
  928. $this->event($muc ? 'chat_open_room' : 'chat_open', $jid);
  929. }
  930. $this->event('chat_counter', $this->user->unreads());
  931. $this->rpc('Chat.insertSeparator', $unreadsCount);
  932. }
  933. public function prepareMessage(&$message, $jid = null)
  934. {
  935. if ($jid != $message->jidto && $jid != $message->jidfrom && $jid != null) {
  936. return $this->_wrapper;
  937. }
  938. $message->jidto = echapJS($message->jidto);
  939. $message->jidfrom = echapJS($message->jidfrom);
  940. $emoji = \Movim\Emoji::getInstance();
  941. // URL messages
  942. if (!empty($message->body)) {
  943. $message->url = filter_var(trim($message->body), FILTER_VALIDATE_URL);
  944. }
  945. // If the message doesn't contain a file but is a URL, we try to resolve it
  946. if (!$message->file && $message->url && $message->resolved == false) {
  947. $this->rpc('Chat.resolveMessage', (int)$message->mid);
  948. }
  949. if ($message->retracted) {
  950. $message->body = '<i class="material-icons">delete</i> '.__('message.retracted');
  951. } elseif ($message->encrypted) {
  952. $message->body = __('message.encrypted');
  953. } elseif (isset($message->html) && !isset($message->file)) {
  954. $message->body = $message->html;
  955. } else {
  956. $message->addUrls();
  957. $message->body = $emoji->replace($message->body);
  958. }
  959. if (isset($message->subject) && $message->type == 'headline') {
  960. $message->body = $message->subject .': '. $message->body;
  961. }
  962. // XEP-0393
  963. // $message->body = (preg_replace ('/```((.|\n)*?)(```|\z)/', "<pre>$1</pre>", $message->body));
  964. $message->body = (preg_replace ('/(?<=^|[\s,\*,_,~])(`(?!\s).+?(?<!\s)`)/', "$1</code>", $message->body));
  965. $message->body = (preg_replace ('/(?<=^|[\s,_,`,~])(\*(?!\s).+?(?<!\s)\*)/', "<b>$1</b>", $message->body));
  966. $message->body = (preg_replace ('/(?<=^|[\s,\*,`,~])(_(?!\s).+?(?<!\s)_)/', "<em>$1</em>", $message->body));
  967. $message->body = (preg_replace ('/(?<=^|[\s,\*,_,`])(~(?!\s).+?(?<!\s)~)/', "<s>$1</s>", $message->body));
  968. // Sticker message
  969. if (isset($message->sticker)) {
  970. $sticker = Image::getOrCreate($message->sticker, false, false, 'png');
  971. if ($sticker == false
  972. && $message->jidfrom != $message->session) {
  973. $r = new Request;
  974. $r->setTo($message->jidfrom)
  975. ->setResource($message->resource)
  976. ->setCid($message->sticker)
  977. ->request();
  978. } else {
  979. $p = new Image;
  980. $p->setKey($message->sticker);
  981. $p->load('png');
  982. $stickerSize = $p->getGeometry();
  983. $message->sticker = [
  984. 'url' => $sticker,
  985. 'width' => $stickerSize['width'],
  986. 'height' => $stickerSize['height']
  987. ];
  988. }
  989. }
  990. // Jumbo emoji
  991. if ($emoji->isSingleEmoji()
  992. && !isset($message->html)
  993. && in_array($message->type, ['chat', 'groupchat'])) {
  994. $message->sticker = [
  995. 'url' => $emoji->getLastSingleEmojiURL(),
  996. 'title' => ':'.$emoji->getLastSingleEmojiTitle().':',
  997. 'height' => 60,
  998. ];
  999. }
  1000. // Attached file
  1001. if (isset($message->file)) {
  1002. // We proxify pictures links even if they are advertized as small ones
  1003. if (\array_key_exists('type', $message->file)
  1004. && typeIsPicture($message->file['type'])
  1005. && $message->file['size'] <= SMALL_PICTURE_LIMIT*4) {
  1006. $message->sticker = [
  1007. 'thumb' => $this->route('picture', urlencode($message->file['uri'])),
  1008. 'url' => $message->file['uri'],
  1009. 'picture' => true
  1010. ];
  1011. }
  1012. $url = parse_url($message->file['uri']);
  1013. // Other image websites
  1014. if (\array_key_exists('host', $url)) {
  1015. $file = $message->file;
  1016. $file['host'] = $url['host'];
  1017. $message->file = $file;
  1018. switch ($url['host']) {
  1019. case 'i.imgur.com':
  1020. $thumb = getImgurThumbnail($message->file['uri']);
  1021. if ($thumb) {
  1022. $message->sticker = [
  1023. 'url' => $message->file['uri'],
  1024. 'thumb' => $thumb,
  1025. 'picture' => true
  1026. ];
  1027. }
  1028. break;
  1029. }
  1030. }
  1031. // Build cards for the URIs
  1032. $uri = explodeXMPPURI($message->file['uri']);
  1033. switch ($uri['type']) {
  1034. case 'post':
  1035. $post = \App\Post::where('server', $uri['params'][0])
  1036. ->where('node', $uri['params'][1])
  1037. ->where('nodeid', $uri['params'][2])
  1038. ->first();
  1039. if ($post) {
  1040. $p = new Post;
  1041. $message->card = $p->prepareTicket($post);
  1042. }
  1043. break;
  1044. }
  1045. }
  1046. if ($message->resolvedUrl && !$message->file
  1047. && !$message->card && !$message->sticker) {
  1048. $resolved = $message->resolvedUrl->cache;
  1049. if ($resolved) {
  1050. $message->card = $this->prepareEmbed($resolved);
  1051. }
  1052. }
  1053. // Parent
  1054. if ($message->parent) {
  1055. if ($message->parent->file) {
  1056. $message->parent->body = '<i class="material-icons">insert_drive_file</i> '.__('avatar.file');
  1057. if (typeIsPicture($message->parent->file['type'])) {
  1058. $message->parent->body = '<i class="material-icons">image</i> '.__('chats.picture');
  1059. }
  1060. if (typeIsVideo($message->parent->file['type'])) {
  1061. $message->parent->body = '<i class="material-icons">local_movies</i> '.__('chats.video');
  1062. }
  1063. }
  1064. if ($message->parent->type == 'groupchat') {
  1065. $message->parent->resolveColor();
  1066. $message->parent->fromName = $message->parent->resource;
  1067. } else {
  1068. // TODO optimize
  1069. $roster = $this->user->session->contacts()
  1070. ->where('jid', $message->parent->jidfrom)
  1071. ->first();
  1072. $contactFromName = $message->parent->from
  1073. ? $message->parent->from->truename
  1074. : $message->parent->jidfrom;
  1075. $message->parent->fromName = $roster
  1076. ? $roster->truename
  1077. : $contactFromName;
  1078. }
  1079. } else {
  1080. // Let's try to support "quoted" messages
  1081. $quote = '&gt; ';
  1082. $parent = '';
  1083. $remains = '';
  1084. $endOfQuote = false;
  1085. foreach (explode(PHP_EOL, $message->body) as $line) {
  1086. if (substr($line, 0, strlen($quote)) == $quote && $endOfQuote == false) {
  1087. $parent .= substr($line, strlen($quote))."\n";
  1088. } else {
  1089. $endOfQuote = true;
  1090. $remains .= $line."\n";
  1091. }
  1092. }
  1093. if ($parent !== '') {
  1094. $message->parentQuote = $parent;
  1095. $message->body = $remains;
  1096. }
  1097. }
  1098. // reactions_count if cached, if not, reload it from the DB
  1099. if ($message->reactions_count ?? $message->reactions()->count()) {
  1100. $message->reactionsHtml = $this->prepareReactions($message);
  1101. }
  1102. $message->rtl = isRTL($message->body);
  1103. $message->publishedPrepared = prepareTime(strtotime($message->published));
  1104. if ($message->delivered) {
  1105. $message->delivered = prepareDate(strtotime($message->delivered), true);
  1106. }
  1107. if ($message->displayed) {
  1108. $message->displayed = prepareDate(strtotime($message->displayed), true);
  1109. }
  1110. $date = prepareDate(strtotime($message->published), false, false, true);
  1111. if (empty($date)) {
  1112. $date = $this->__('date.today');
  1113. }
  1114. // We create the date wrapper
  1115. if (!array_key_exists($date, $this->_wrapper)) {
  1116. $this->_wrapper[$date] = [];
  1117. }
  1118. $messageDBSeen = $message->seen;
  1119. $n = new Notification;
  1120. if ($message->type == 'groupchat') {
  1121. $message->resolveColor();
  1122. // Cache the resolved presences for a while
  1123. $key = $message->jidfrom.$message->resource;
  1124. if (!isset($this->_mucPresences[$key])) {
  1125. $this->_mucPresences[$key] = $this->user->session->presences()
  1126. ->where('jid', $message->jidfrom)
  1127. ->where('resource', $message->resource)
  1128. ->where('muc', true)
  1129. ->first();
  1130. }
  1131. if ($this->_mucPresences[$key] && $this->_mucPresences[$key] !== true) {
  1132. if ($url = $this->_mucPresences[$key]->conferencePicture) {
  1133. $message->icon_url = $url;
  1134. }
  1135. $message->moderator = ($this->_mucPresences[$key]->mucrole == 'moderator');
  1136. $message->mucjid = $this->_mucPresences[$key]->mucjid;
  1137. $message->mine = $message->seen = ($this->_mucPresences[$key]->mucjid == $this->user->id);
  1138. } else {
  1139. $this->_mucPresences[$key] = true;
  1140. }
  1141. $message->icon = firstLetterCapitalize($message->resource);
  1142. }
  1143. // Handle faulty replacing messages
  1144. if ($message->replace
  1145. && ($message->replace->jidfrom != $message->jidfrom
  1146. || $message->replace->resource != $message->resource)
  1147. ) {
  1148. unset($message->replace);
  1149. unset($message->replaceid);
  1150. }
  1151. if($message->seen === false) {
  1152. $message->seen = ('chat|'.$message->jidfrom == $n->getCurrent());
  1153. }
  1154. if ($message->seen === true
  1155. && $messageDBSeen === false) {
  1156. $this->user->messages()
  1157. ->where('id', $message->id)
  1158. ->update(['seen' => true]);
  1159. }
  1160. $msgkey = '<' . $message->jidfrom;
  1161. $msgkey .= ($message->type == 'groupchat' && $message->resource != null)
  1162. ? cleanupId($message->resource, true)
  1163. : '';
  1164. $msgkey .= '>' . substr($message->published, 11, 5);
  1165. $counter = count($this->_wrapper[$date]);
  1166. $this->_wrapper[$date][$counter.$msgkey] = $message;
  1167. if ($message->type == 'invitation') {
  1168. $view = $this->tpl();
  1169. $view->assign('message', $message);
  1170. $message->body = ($message->jidfrom == $this->user->id)
  1171. ? $view->draw('_chat_invitation_self')
  1172. : $view->draw('_chat_invitation');
  1173. }
  1174. if ($message->type == 'jingle_incoming') {
  1175. $view = $this->tpl();
  1176. $view->assign('message', $message);
  1177. $message->body = $view->draw('_chat_jingle_incoming');
  1178. }
  1179. if ($message->type == 'jingle_outgoing') {
  1180. $view = $this->tpl();
  1181. $view->assign('message', $message);
  1182. $message->body = $view->draw('_chat_jingle_outgoing');
  1183. }
  1184. if ($message->type == 'jingle_end') {
  1185. $view = $this->tpl();
  1186. $view->assign('message', $message);
  1187. $view->assign('diff', false);
  1188. $start = Message::where('thread', $message->thread)
  1189. ->whereIn('type', ['jingle_incoming', 'jingle_outgoing'])
  1190. ->first();
  1191. if ($start) {
  1192. $diff = (new DateTime($start->created_at))
  1193. ->diff(new DateTime($message->created_at));
  1194. $view->assign('diff', $diff);
  1195. }
  1196. $message->body = $view->draw('_chat_jingle_end');
  1197. }
  1198. return $this->_wrapper;
  1199. }
  1200. public function prepareEmbed(EmbedLight $embed, bool $withLink = false)
  1201. {
  1202. $tpl = $this->tpl();
  1203. $tpl->assign('embed', $embed);
  1204. $tpl->assign('withlink', $withLink);
  1205. return $tpl->draw('_chat_embed');
  1206. }
  1207. public function prepareReactions(Message $message)
  1208. {
  1209. $view = $this->tpl();
  1210. $merged = [];
  1211. $reactions = $message
  1212. ->reactions()
  1213. ->orderBy('created_at')
  1214. ->get();
  1215. foreach ($reactions as $reaction) {
  1216. if (!array_key_exists($reaction->emoji, $merged)) {
  1217. $merged[$reaction->emoji] = [];
  1218. }
  1219. $merged[$reaction->emoji][] = $reaction->jidfrom;
  1220. }
  1221. $view->assign('message', $message);
  1222. $view->assign('reactions', $merged);
  1223. $view->assign('me', $this->user->id);
  1224. return $view->draw('_chat_reactions');
  1225. }
  1226. public function prepareHeader($jid, $muc = false)
  1227. {
  1228. $view = $this->tpl();
  1229. $view->assign('jid', $jid);
  1230. $view->assign('muc', $muc);
  1231. $view->assign(
  1232. 'info',
  1233. \App\Info::where('server', $this->user->session->host)
  1234. ->where('node', '')
  1235. ->first()
  1236. );
  1237. $view->assign('anon', false);
  1238. $view->assign('counter',
  1239. $this->prepareChatCounter(
  1240. $this->user->unreads(null, false, true)
  1241. )
  1242. );
  1243. if ($muc) {
  1244. $view->assign('conference', $this->user->session->conferences()
  1245. ->where('conference', $jid)
  1246. ->with('info')
  1247. ->first());
  1248. $mucinfo = \App\Info::where('server', explodeJid($jid)['server'])
  1249. ->where('node', '')
  1250. ->first();
  1251. if ($mucinfo && !empty($mucinfo->abuseaddresses)) {
  1252. $view->assign('info', $mucinfo);
  1253. }
  1254. } else {
  1255. $view->assign('roster', $this->user->session->contacts()->where('jid', $jid)->first());
  1256. $view->assign('contact', \App\Contact::firstOrNew(['id' => $jid]));
  1257. }
  1258. return $view->draw('_chat_header');
  1259. }
  1260. public function prepareEmpty()
  1261. {
  1262. $view = $this->tpl();
  1263. $chats = \App\Cache::c('chats');
  1264. if ($chats == null) {
  1265. $chats = [];
  1266. }
  1267. $chats[$this->user->id] = true;
  1268. $top = $this->user->session->topContacts()
  1269. ->join(DB::raw('(
  1270. select min(value) as value, jid as pjid
  1271. from presences
  1272. group by jid) as presences
  1273. '), 'presences.pjid', '=', 'rosters.jid')
  1274. ->where('value', '<', 5)
  1275. ->whereNotIn('rosters.jid', array_keys($chats))
  1276. ->with('presence.capability')
  1277. ->take(15)
  1278. ->get();
  1279. $view->assign('top', $top);
  1280. return $view->draw('_chat_empty');
  1281. }
  1282. public function ajaxHttpGetExplore($page = 0)
  1283. {
  1284. $this->rpc('MovimTpl.fill', '#chat_explore', $this->prepareExplore($page));
  1285. }
  1286. public function prepareExplore($page = 0)
  1287. {
  1288. $view = $this->tpl();
  1289. $pagination = 8;
  1290. $users = Contact::public()
  1291. ->notInRoster($this->user->session->id)
  1292. ->orderByPresence()
  1293. ->where('id', '!=', $this->user->id)
  1294. ->skip($page * $pagination)
  1295. ->take($pagination + 1)
  1296. ->get();
  1297. $view->assign('presencestxt', getPresencesTxt());
  1298. $view->assign('users', $users);
  1299. $view->assign('pagination', $pagination);
  1300. $view->assign('page', $page);
  1301. return $view->draw('_chat_explore');
  1302. }
  1303. private function prepareComposeList(array $list)
  1304. {
  1305. $view = $this->tpl();
  1306. $view->assign('list', implode(', ', $list));
  1307. return $view->draw('_chat_compose_list');
  1308. }
  1309. /**
  1310. * @brief Validate the jid
  1311. *
  1312. * @param string $jid
  1313. */
  1314. private function validateJid($jid)
  1315. {
  1316. return (Validator::stringType()->noWhitespace()->length(6, 256)->validate($jid));
  1317. }
  1318. public function getSmileyPath($id)
  1319. {
  1320. return getSmileyPath($id);
  1321. }
  1322. }