addjs('chat.js'); $this->addcss('chat.css'); $this->registerEvent('carbons', 'onMessage'); $this->registerEvent('message', 'onMessage'); $this->registerEvent('presence', 'onPresence', 'chat'); $this->registerEvent('retracted', 'onRetracted'); $this->registerEvent('moderated', 'onRetracted'); $this->registerEvent('receiptack', 'onMessageReceipt'); $this->registerEvent('displayed', 'onMessage', 'chat'); $this->registerEvent('mam_get_handle', 'onMAMRetrieved'); $this->registerEvent('mam_get_handle_muc', 'onMAMMucRetrieved', 'chat'); $this->registerEvent('mam_get_handle_contact', 'onMAMContactRetrieved', 'chat'); $this->registerEvent('chatstate', 'onChatState', 'chat'); //$this->registerEvent('subject', 'onConferenceSubject', 'chat'); Spam the UI during authentication $this->registerEvent('muc_setsubject_handle', 'onConferenceSubject', 'chat'); $this->registerEvent('muc_getconfig_handle', 'onRoomConfig', 'chat'); $this->registerEvent('muc_setconfig_handle', 'onRoomConfigSaved', 'chat'); $this->registerEvent('muc_setconfig_error', 'onRoomConfigError', 'chat'); $this->registerEvent('presence_muc_handle', 'onMucConnected', 'chat'); $this->registerEvent('message_publish_error', 'onPublishError', 'chat'); $this->registerEvent('chat_counter', 'onCounter', 'chat'); $this->registerEvent('jingle_message', 'onJingleMessage'); $this->registerEvent('muc_event_message', 'onMucEventMessage'); $this->registerEvent('bob_request_handle', 'onSticker'); $this->registerEvent('notification_counter_clear', 'onNotificationCounterClear'); } public function onPresence($packet) { if ($packet->content && $jid = $packet->content->jid) { $arr = explode('|', (new Notif)->getCurrent()); if (isset($arr[1]) && $jid == $arr[1] && !$packet->content->muc) { $this->ajaxGetHeader($jid); } } } public function onJingleMessage($packet) { $this->onMessage($packet, false, false); } public function onMucEventMessage($packet) { $this->onMessage($packet, false, false); } public function onMessageReceipt($packet) { $this->onMessage($packet, false, true); } public function onRetracted($packet) { $this->onMessage($packet, false, true); } public function onCounter($count) { $this->rpc('MovimUtils.setDataItem', '#chatheadercounter', 'counter', $count); } public function onNotificationCounterClear($params) { list($page, $jid) = array_pad($params, 3, null); if ($page === 'chat') { // Check if the jid is a connected chatroom $presence = $this->user->session->presences() ->where('jid', $jid) ->where('mucjid', $this->user->id) ->first(); $this->prepareMessages($jid, ($presence), true); } } public function onPublishError($packet) { Toast::send( $packet->content ?? $this->__('chat.publish_error') ); } public function onMessage($packet, $history = false, $receipt = false) { $message = $packet->content; $from = null; $chatStates = ChatStates::getInstance(); $rawbody = $message->body; if ( $message->isEmpty() && !in_array($message->type, [ 'jingle_incoming', 'jingle_outgoing', 'jingle_end', 'muc_owner', 'jingle_retract', 'jingle_reject', 'muc_admin', 'muc_outcast', 'muc_member' ]) ) { return; } if ($message->file) { $rawbody = '๐ ' . $this->__('avatar.file'); if (typeIsPicture($message->file['type'])) { $rawbody = '๐ผ๏ธ ' . $this->__('chats.picture'); } elseif (typeIsAudio($message->file['type'])) { $rawbody = '๐ต ' . $this->__('chats.audio'); } elseif (typeIsVideo($message->file['type'])) { $rawbody = '๐๏ธ ' . $this->__('chats.video'); } } if ( $message->user_id == $message->jidto && !$history && !$message->isEmpty() && $message->seen == false && $message->jidfrom != $message->jidto ) { $from = $message->jidfrom; $contact = Contact::firstOrNew(['id' => $from]); $conference = $message->isMuc() ? $this->user->session ->conferences()->where('conference', $from) ->first() : null; if ( $contact != null && !$message->isMuc() && !$message->retracted && !$message->oldid ) { $roster = $this->user->session->contacts()->where('jid', $from)->first(); $chatStates->clearState($from); $name = $roster ? $roster->truename : $contact->truename; // Specific case where the message is a MUC PM $jid = explodeJid($message->jidfrom); if ($jid['username'] == $name && $jid['resource'] == $message->resource) { $name = $message->resource; } Notif::rpcCall('Notif.incomingMessage'); Notif::append( 'chat|' . $from, $name, $message->encrypted && is_array($message->omemoheader) ? "๐ " . substr($message->omemoheader['payload'], 0, strlen($message->omemoheader['payload']) / 2) : $rawbody, $contact->getPicture(), 4, $this->route('chat', $contact->jid), null, 'Search.chat(\'' . echapJS($contact->jid) . '\')' ); } // If it's a groupchat message elseif ( $message->isMuc() && !$message->retracted && $conference && (($conference->notify == 1 && $message->quoted) // When quoted || $conference->notify == 2) // Always && !$receipt ) { Notif::rpcCall('Notif.incomingMessage'); Notif::append( 'chat|' . $from, ($conference != null && $conference->name) ? $conference->name : $from, $message->resource . ': ' . $rawbody, $conference->getPicture(), 4, $this->route('chat', [$contact->jid, 'room']) ); } elseif ($message->isMuc()) { if ($conference && $conference->notify == 0) { $message->seen = true; $message->save(); } $chatStates->clearState($from, $message->resource); } $this->onChatState($chatStates->getState($from)); } $this->rpc('Chat.appendMessagesWrapper', $this->prepareMessage($message, $from)); $this->event('chat_counter', $this->user->unreads()); } public function onSticker($packet) { list($to, $cid) = array_values($packet->content); $this->ajaxGet($to); } public function onChatState(array $array, $first = true) { if (isset($array[1])) { $this->setState( $array[0], is_array($array[1]) && !empty($array[1]) ? $this->prepareComposeList(array_keys($array[1])) : $this->__('message.composing'), $first ); } else { $this->setState($array[0], '', $first); } } public function onConferenceSubject($packet) { $this->ajaxGetRoom($packet->content->jidfrom, false, true); } public function onMAMRetrieved() { Toast::send($this->__('chat.mam_retrieval')); } public function onMAMMucRetrieved($packet) { $this->ajaxGetRoom($packet->content, true, true); } public function onMAMContactRetrieved($packet) { $this->ajaxGet($packet->content, true); } public function onMucConnected($packet) { list($content, $notify) = $packet->content; if ($notify) { $this->ajaxGetRoom($content->jid, false, true); } } public function onRoomConfigError($packet) { Toast::send($packet->content); } public function onRoomConfig($packet) { list($config, $room) = array_values($packet->content); $view = $this->tpl(); $xml = new XMPPtoForm; $form = $xml->getHTML($config->x); $view->assign('form', $form); $view->assign('room', $room); Dialog::fill($view->draw('_chat_config_room'), true); } public function onRoomConfigSaved($packet) { $r = new DiscoRequest; $r->setTo($packet->content) ->request(); Toast::send($this->__('chatroom.config_saved')); } private function setState(string $jid, string $message, $first = true) { if ($first) { $this->rpc('MovimUtils.removeClass', '#' . cleanupId($jid . '_state'), 'first'); } $this->rpc('MovimTpl.fill', '#' . cleanupId($jid . '_state'), $message); } public function ajaxInit() { $view = $this->tpl(); $date = $view->draw('_chat_date'); $separator = $view->draw('_chat_separator'); $this->rpc('Chat.setGeneralElements', $date, $separator); $this->rpc( 'Chat.setConfig', $this->_pagination, $this->__('message.error'), $this->__('chat.action_impossible_encrypted') ); } public function ajaxClearCounter(string $jid) { $this->prepareMessages($jid, false, true, false); } /** * Get the header */ public function ajaxGetHeader(string $jid, bool $muc = false) { $this->rpc( 'MovimTpl.fill', '#' . cleanupId($jid . '_header'), $this->prepareHeader($jid, $muc) ); $chatStates = ChatStates::getInstance(); $this->onChatState($chatStates->getState($jid), false); } public function ajaxHttpGetEmpty() { $this->ajaxGet(); } /** * @brief Get a discussion * @param string $jid */ public function ajaxGet(string $jid = null, ?bool $light = false) { if ($jid == null) { $this->rpc('MovimTpl.hidePanel'); $this->rpc('Notif.current', 'chat'); $this->rpc('MovimUtils.pushSoftState', $this->route('chat')); if ($light == false) { $this->rpc('MovimTpl.fill', '#chat_widget', $this->prepareEmpty()); } } else { if ($light == false) { $this->rpc('MovimUtils.pushSoftState', $this->route('chat', $jid)); $this->rpc('MovimTpl.fill', '#chat_widget', $this->prepareChat($jid)); $chatStates = ChatStates::getInstance(); $this->onChatState($chatStates->getState($jid), false); $this->rpc('MovimTpl.showPanel'); $this->rpc('Chat.focus'); (new \Dictaphone)->ajaxHttpGet(); } $this->rpc('Chat.setObservers'); $this->prepareMessages($jid); $this->rpc('Notif.current', 'chat|' . $jid); $this->rpc('Chat.scrollToSeparator'); if ($this->user->hasOMEMO()) { $this->rpc( 'Chat.setBundlesIds', $jid, $this->user->bundles() ->where('jid', $jid) ->select(['bundleid', 'jid']) ->get() ->mapToGroups(fn ($tuple) => [$tuple['jid'] => $tuple['bundleid']]) ->toArray() ); } } } /** * @brief Get a chatroom * @param string $jid */ public function ajaxGetRoom(string $room, $light = false, $noConnect = false) { if (!validateJid($room)) { return; } $conference = $this->user->session->conferences()->where('conference', $room)->with('members')->first(); if ($conference) { if (!$conference->connected && !$noConnect) { $this->rpc('Rooms_ajaxJoin', $conference->conference, $conference->nick); } if ($light == false) { $this->rpc('MovimUtils.pushSoftState', $this->route('chat', [$room, 'room'])); $this->rpc('MovimTpl.fill', '#chat_widget', $this->prepareChat($room, true)); $chatStates = ChatStates::getInstance(); $this->onChatState($chatStates->getState($room), false); $this->rpc('MovimTpl.showPanel'); $this->rpc('Chat.focus'); (new \Dictaphone)->ajaxHttpGet(); } $this->rpc('Chat.setObservers'); $this->prepareMessages($room, true); $this->rpc('Notif.current', 'chat|' . $room); $this->rpc('Chat.scrollToSeparator'); if ($this->user->hasOMEMO() && $conference->isGroupChat()) { $this->rpc('Chat.setGroupChatMembers', $conference->members->pluck('jid')->toArray()); $this->rpc( 'Chat.setBundlesIds', $room, $this->user->bundles() ->whereIn('jid', function ($query) use ($room) { $query->select('jid') ->from('members') ->where('conference', $room); }) ->select(['bundleid', 'jid']) ->get() ->mapToGroups(fn ($tuple) => [$tuple['jid'] => $tuple['bundleid']]) ->toArray() ); } } else { $this->rpc('RoomsUtils_ajaxAdd', $room); } } /** * @brief Send a message */ public function ajaxHttpDaemonSendMessage( string $to, string $message, bool $muc = false, $file = null, ?int $replyToMid = 0, ?bool $mucReceipts = false, $omemo = null ) { $messageFile = null; $messageOMEMOHeader = null; if ($file) { $messageFile = new MessageFile; $messageFile->import($file); if (!$messageFile->valid) $messageFile = null; } else { try { $url = new Url; $cache = $url->resolve(trim($message)); if ($cache && $url->file !== null) { $messageFile = $url->file; } } catch (\Exception $e) { } } if ($omemo) { $messageOMEMOHeader = new MessageOMEMOHeader; $messageOMEMOHeader->import($omemo); } $this->sendMessage($to, $message, $muc, null, $messageFile, $replyToMid, $mucReceipts, $messageOMEMOHeader); } /** * @brief Send a resolved message * * @param string $to * @param string $message * @return void */ public function sendMessage( string $to, string $message = '', bool $muc = false, ?Message $replace = null, ?MessageFile $file = null, ?int $replyToMid = 0, ?bool $mucReceipts = false, ?MessageOMEMOHeader $messageOMEMOHeader = null ) { $tempId = null; if ($messageOMEMOHeader) { $tempId = $message; $message = 'Encrypted OMEMO message sent'; } $body = ($file != null && $file->type != 'xmpp/uri') ? $file->uri : $message; if ($body == '' || $body == '/me') { return; } $m = new \App\Message; $m->id = generateUUID(); $m->originid = $m->id; $m->messageid = $m->id; $m->replaceid = $replace ? $replace->originid : null; $m->user_id = $this->user->id; $m->jidto = echapJid($to); $m->jidfrom = $this->user->id; $m->published = gmdate('Y-m-d H:i:s'); $reply = null; // If the replaced message is quoting another one ensure that we keep the quote if ($replace && $replace->parentmid) { $replyToMid = $replace->parentmid; } if ($replyToMid !== 0) { $reply = $this->user->messages() ->where('mid', $replyToMid) ->first(); if ($reply) { // See https://xmpp.org/extensions/xep-0201.html#new $m->thread = $reply->thread; $m->parentmid = $reply->mid; } } $m->markable = true; $m->seen = true; $m->type = 'chat'; $m->resource = $this->user->session->resource; if ($muc) { $m->type = 'groupchat'; $m->resource = $this->user->username; $m->jidfrom = $to; } // We decode URL codes to send the correct message to the XMPP server $p = new Publish; $p->setTo($to); $p->setReplace($m->replaceid); $p->setId($m->id); $p->setOriginid($m->originid); if ($muc) { $p->setMuc(); if ($mucReceipts) { $p->setMucReceipts(); } } if ($file) { $m->file = (array)$file; $p->setFile($file); } if ($reply) { $quotable = false; // https://xmpp.org/extensions/xep-0461.html#business-id if ($reply->isMuc()) { // stanza-id only $p->setReplyid($reply->stanzaid); $quotable = true; } elseif ($reply->messageid) { $p->setReplyid($reply->messageid); $quotable = true; } if ($quotable) { $p->setReplyto($reply->jidfrom . '/' . $reply->resource); $matches = []; preg_match_all('/^/m', $reply->body, $matches); $p->setReplyquotedbodylength( mb_strlen($reply->body) + (2 * count($matches[0])) + 1 ); // Prepend quoted message body $quotedBody = preg_replace('/^/m', "> ", $reply->body); $p->setContent($quotedBody . "\n" . $body); } else { $p->setContent($body); } } else { $p->setContent($body); } $m->body = $body; if ($messageOMEMOHeader) { $m->encrypted = true; $m->omemoheader = (string)$messageOMEMOHeader; $m->bundleid = $messageOMEMOHeader->sid; $p->setMessageOMEMO($messageOMEMOHeader); } (ChatOwnState::getInstance())->halt(); $p->request(); // We sent the published id back if ($tempId) { $this->rpc('Chat.sentId', $tempId, $m->id); } /* Is it really clean ? */ if (!$p->getMuc()) { $m->body = htmlentities(trim($m->body), ENT_XML1, 'UTF-8'); $m->save(); $m = $m->fresh(); $packet = new \Moxl\Xec\Payload\Packet; $packet->content = $m; // We refresh the Chats list $c = new Chats; $c->onMessage($packet); $this->onMessage($packet); } } /** * @brief Send a correction message * * @param string $to * @param string $message * @return void */ public function ajaxHttpDaemonCorrect(string $to, int $mid, string $message = '') { $replace = $this->user->messages() ->where('mid', $mid) ->first(); if ($replace) { $this->sendMessage($to, $message, $replace->isMuc(), $replace); } } /** * @brief Send a reaction */ public function ajaxHttpDaemonSendReaction(string $mid, string $emoji) { $parentMessage = $this->user->messages() ->where('mid', $mid) ->first(); $emojiHandler = \Movim\Emoji::getInstance(); $emojiHandler->replace($emoji); if ($parentMessage && $emojiHandler->isSingleEmoji()) { // Try to load the MUC presence and resolve the resource $mucPresence = null; if ($parentMessage->isMuc()) { $mucPresence = $this->user->session->presences() ->where('jid', $parentMessage->jidfrom) ->where('mucjid', $this->user->id) ->where('muc', true) ->first(); if (!$mucPresence) return; } $jidfrom = ($parentMessage->isMuc()) ? $mucPresence->resource : $this->user->id; $emojis = $parentMessage->reactions() ->where('jidfrom', $jidfrom) ->get(); $r = new Reactions; $newEmojis = []; // This reaction was not published yet if ($emojis->where('emoji', $emoji)->count() == 0) { $now = \Carbon\Carbon::now(); $reaction = new Reaction; $reaction->message_mid = $parentMessage->mid; $reaction->jidfrom = ($parentMessage->isMuc()) ? $this->user->username : $this->user->id; $reaction->created_at = $now; $reaction->updated_at = $now; $reaction->emoji = $emoji; if (!$parentMessage->isMuc()) { $reaction->save(); } $newEmojis = $emojis->push($reaction); } else { if (!$parentMessage->isMuc()) { $parentMessage->reactions() ->where('jidfrom', $jidfrom) ->where('emoji', $emoji) ->delete(); } $newEmojis = $emojis->filter(fn ($value, $key) => $value->emoji != $emoji); } $r->setTo($parentMessage->jidfrom != $parentMessage->user_id ? $parentMessage->jidfrom : $parentMessage->jidto) ->setId(\generateUUID()) // https://xmpp.org/extensions/xep-0444.html#business-id ->setParentId(!$parentMessage->isMuc() && $parentMessage->messageid ? $parentMessage->messageid : $parentMessage->stanzaid) ->setReactions($newEmojis->pluck('emoji')->toArray()); if ($parentMessage->isMuc()) { $r->setMuc(); } $r->request(); if (!$parentMessage->isMuc()) { $packet = new \Moxl\Xec\Payload\Packet; $packet->content = $parentMessage; $this->onMessage($packet); } } } /** * @brief Refresh a message */ public function ajaxRefreshMessage(string $mid) { $message = $this->user->messages() ->where('mid', $mid) ->first(); if ($message) { $this->rpc('Chat.appendMessagesWrapper', $this->prepareMessage($message, null)); } } /** * @brief Get the last message sent * * @param string $to * @return void */ public function ajaxLast(string $to, $muc = false) { $m = Message::getLast($to, $muc); if (!$m) return; // We might get an already edited message, be sure to load the id of the original one $mid = $m->mid; if ($m && !empty($m->replaceid)) { $originalMessage = $this->user->messages() ->where('originid', $m->replaceid) ->first(); if ($originalMessage) { $mid = $originalMessage->mid; } } if ( $m && !isset($m->sticker) && !isset($m->file) && !empty($m->body) ) { $this->rpc('Chat.setTextarea', htmlspecialchars_decode($m->body), $mid); } } /** * @brief Get a sent message * * @param string $mid * @return void */ public function ajaxEdit($mid) { $m = $this->user->messages() ->where('mid', $mid) ->first(); if ( $m && !isset($m->sticker) && !isset($m->file) ) { $this->rpc('Chat.setTextarea', htmlspecialchars_decode($m->body), $mid); } } /** * @brief Reply to a message * * @param string $mid * @return void */ public function ajaxHttpDaemonReply($mid) { $message = $this->user->messages() ->where('mid', $mid) ->first(); if ( $message->isClassic() && (($message->isMuc() && $message->stanzaid) || (!$message->isMuc() && $message->messageid) || isset($message->thread)) ) { $view = $this->tpl(); $view->assign('message', $message); $this->rpc('MovimTpl.fill', '#reply', $view->draw('_chat_reply')); $this->rpc('Chat.focus'); } } /** * Clear the Reply box */ public function ajaxClearReply() { $this->rpc('MovimTpl.fill', '#reply', ''); } /** * @brief Send a "composing" message * * @param string $to * @return void */ public function ajaxSendComposing($to, $muc = false) { if (!validateJid($to)) { return; } (ChatOwnState::getInstance())->composing($to, $muc); } /** * @brief Get the chat history * * @param string jid * @param string time */ public function ajaxGetHistory($jid, $date, $muc = false, $prepend = true) { if (!validateJid($jid) || !isset($date)) { return; } $messages = \App\Message::jid($jid) ->where('published', $prepend ? '<' : '>', date(MOVIM_SQL_DATE, strtotime($date))); $messages = $muc ? $messages->whereIn('type', $this->_messageTypesMuc)->whereNull('subject') : $messages->whereIn('type', $this->_messageTypes); $messages = $messages->orderBy('published', 'desc') ->withCount('reactions') ->take($this->_pagination) ->get(); if ($messages->count() > 0) { if ($prepend) { Toast::send($this->__('message.history', $messages->count())); } else { $messages = $messages->reverse(); } foreach ($messages as $message) { $this->prepareMessage($message); } $this->rpc('Chat.appendMessagesWrapper', $this->_wrapper, $prepend); $this->_wrapper = []; } } /** * @brief Configure a room * * @param string $room */ public function ajaxGetRoomConfig($room) { if (!validateJid($room)) { return; } $gc = new GetConfig; $gc->setTo($room) ->request(); } /** * @brief Save the room configuration * * @param string $room */ public function ajaxSetRoomConfig(stdClass $data, $room) { if (!validateJid($room)) { return; } $sc = new SetConfig; $sc->setTo($room) ->setData(formToArray($data)) ->request(); } /** * @brief Set last displayed message */ public function ajaxDisplayed($jid, $id) { if (!validateJid($jid)) { return; } $message = $this->user->messages()->where('id', $id)->first(); if ( $message && $message->markable == true && $message->displayed == null ) { $message->displayed = gmdate('Y-m-d H:i:s'); $message->save(); if (!$message->isMuc()) { \Moxl\Stanza\Message::displayed( $jid, $message->messageid, $message->type ); } // https://xmpp.org/extensions/xep-0333.html#rules-muc elseif ($message->stanzaid) { \Moxl\Stanza\Message::displayed( $jid, $message->stanzaid, $message->type ); } } } /** * @brief Ask to clear the history * * @param string $jid */ public function ajaxClearHistory($jid) { $view = $this->tpl(); $view->assign('jid', $jid); $view->assign('count', \App\Message::jid($jid)->count()); Dialog::fill($view->draw('_chat_clear')); } /** * @brief Clear the history * * @param string $jid */ public function ajaxClearHistoryConfirm($jid) { if (!validateJid($jid)) { return; } \App\Message::whereIn('id', function ($query) use ($jid) { $jidFromToMessages = DB::table('messages') ->where('user_id', $this->user->id) ->where('jidfrom', $jid) ->unionAll( DB::table('messages') ->where('user_id', $this->user->id) ->where('jidto', $jid) ); $query->select('id')->from( $jidFromToMessages, 'messages' )->where('user_id', $this->user->id); })->delete(); $this->ajaxGet($jid); } public function prepareChat($jid, $muc = false) { $view = $this->tpl(); $view->assign('jid', $jid); $view->assign('muc', $muc); $view->assign('emoji', prepareString('๐')); if ($muc) { $view->assign('conference', $this->user->session->conferences() ->where('conference', $jid) ->with('info') ->first()); } return $view->draw('_chat'); } public function prepareMessages($jid, $muc = false, $seenOnly = false, $event = true) { if (!validateJid($jid)) { return; } $jid = echapJid($jid); $messagesQuery = \App\Message::jid($jid); $messagesQuery = $muc ? $messagesQuery->whereIn('type', $this->_messageTypesMuc)->whereNull('subject') : $messagesQuery->whereIn('type', $this->_messageTypes); /** * The object need to be cloned there for MySQL, looks like the pagination/where is kept somewhere in betweenโฆ **/ $messagesRequest = clone $messagesQuery; $messagesCount = clone $messagesQuery; $messages = $messagesRequest->withCount('reactions')->orderBy('published', 'desc')->take($this->_pagination)->get(); $unreadsCount = $messagesCount->where('seen', false)->count(); if ($unreadsCount > 0) { $messagesClear = clone $messagesQuery; // Two queries as Eloquent doesn't seems to map correctly the parameters \App\Message::whereIn('mid', $messagesClear->where('seen', false)->pluck('mid'))->update(['seen' => true]); } // Prepare the muc presences if possible $firstMessage = $messages->first(); if ($firstMessage && $firstMessage->isMuc()) { $this->_mucPresences = $this->user->session->presences() ->where('jid', $firstMessage->jidfrom) ->where('muc', true) ->whereIn('resource', $messages->pluck('resource')->unique()) ->get() ->keyBy(function ($presence) { return $presence->jid . $presence->resource; }); } if (!$seenOnly) { $messages = $messages->reverse(); foreach ($messages as $message) { $this->prepareMessage($message); } $view = $this->tpl(); $view->assign('jid', $jid); $view->assign('contact', Contact::firstOrNew(['id' => $jid])); $view->assign('me', false); $view->assign('muc', $muc); $left = $view->draw('_chat_bubble'); $view->assign('contact', Contact::firstOrNew(['id' => $this->user->id])); $view->assign('me', true); $view->assign('muc', $muc); $right = $view->draw('_chat_bubble'); $this->rpc('Chat.setSpecificElements', $left, $right); $this->rpc('Chat.appendMessagesWrapper', $this->_wrapper, false); } if ($messages->count() == 0 && !$muc) { //$chats = new Chats; //$chats->ajaxGetHistory($jid); } if ($event) { $this->event($muc ? 'chat_open_room' : 'chat_open', $jid); } $this->event('chat_counter', $this->user->unreads()); if ($unreadsCount > 0) { $this->rpc('Chat.insertSeparator', $unreadsCount); } } public function prepareMessage(&$message, $jid = null) { if ($jid != $message->jidto && $jid != $message->jidfrom && $jid != null) { return $this->_wrapper; } $message->jidto = echapJS($message->jidto); $message->jidfrom = echapJS($message->jidfrom); $emoji = \Movim\Emoji::getInstance(); // URL messages if (!empty($message->body)) { $message->url = filter_var(trim($message->body), FILTER_VALIDATE_URL); } // If the message doesn't contain a file but is a URL, we try to resolve it if (!$message->file && $message->url && $message->resolved == false) { $this->rpc('Chat.resolveMessage', (int)$message->mid); } if ($message->retracted) { $message->body = 'delete ' . __('message.retracted'); } elseif ($message->encrypted) { $message->body = __('message.encrypted'); } elseif (isset($message->html) && !isset($message->file)) { $message->body = $message->html; } elseif (!isset($message->file)) { $message->addUrls(); if (is_string($message->body)) { $message->body = $emoji->replace($message->body); } } if (isset($message->subject) && $message->type == 'headline') { $message->body = $message->subject . ': ' . $message->body; } // XEP-0393 if (!empty($message->body)) { // $message->body = (preg_replace ('/```((.|\n)*?)(```|\z)/', "
$1", $message->body)); $message->body = (preg_replace('/(?<=^|[\s,\*,_,~])(`(?!\s).+?(?", $message->body)); $message->body = (preg_replace('/(?<=^|[\s,_,`,~])(\*(?!\s).+?(?$1", $message->body)); $message->body = (preg_replace('/(?<=^|[\s,\*,`,~])(_(?!\s).+?(?$1", $message->body)); $message->body = (preg_replace('/(?<=^|[\s,\*,_,`])(~(?!\s).+?(?$1", $message->body)); } // Sticker message if (isset($message->sticker)) { $sticker = Image::getOrCreate($message->sticker, false, false, 'png'); if ( $sticker == false && $message->jidfrom != $message->session ) { $r = new Request; $r->setTo($message->jidfrom) ->setResource($message->resource) ->setCid($message->sticker) ->request(); } else { $p = new Image; $p->setKey($message->sticker); $p->load('png'); $stickerSize = $p->getGeometry(); $message->sticker = [ 'url' => $sticker, 'width' => $stickerSize['width'], 'height' => $stickerSize['height'] ]; } $message->body = ''; } // Jumbo emoji if ( $emoji->isSingleEmoji() && !isset($message->html) && $message->isClassic() ) { $message->sticker = [ 'url' => $emoji->getLastSingleEmojiURL(), 'title' => ':' . $emoji->getLastSingleEmojiTitle() . ':', 'height' => 60, ]; $message->body = ''; } // Attached file if (isset($message->file)) { // Build cards for the URIs $uri = explodeXMPPURI($message->file['uri']); switch ($uri['type']) { case 'post': $post = \App\Post::where('server', $uri['params'][0]) ->where('node', $uri['params'][1]) ->where('nodeid', $uri['params'][2]) ->first(); if ($post) { $p = new Post; $message->card = $p->prepareTicket($post); } break; } $message->body = ''; } if ( $message->resolvedUrl && !$message->file && !$message->card && !$message->sticker ) { $resolved = $message->resolvedUrl->cache; if ($resolved) { $message->card = $this->prepareEmbed($resolved); } } // Parent if ($message->parent) { if ($message->parent->file) { $message->parent->body = 'insert_drive_file ' . __('avatar.file'); if (typeIsPicture($message->parent->file['type'])) { $message->parent->body = 'image ' . __('chats.picture'); } elseif (typeIsAudio($message->parent->file['type'])) { $message->parent->body = 'equalizer ' . __('chats.audio'); } elseif (typeIsVideo($message->parent->file['type'])) { $message->parent->body = 'local_movies ' . __('chats.video'); } } if ($message->parent->isMuc()) { $message->parent->resolveColor(); $message->parent->fromName = $message->parent->resource; } else { // TODO optimize $roster = $this->user->session->contacts() ->where('jid', $message->parent->jidfrom) ->first(); $contactFromName = $message->parent->from ? $message->parent->from->truename : $message->parent->jidfrom; $message->parent->fromName = $roster ? $roster->truename : $contactFromName; } } elseif ($message->body) { // Let's try to support "quoted" messages $quote = '> '; $parent = ''; $remains = ''; $endOfQuote = false; foreach (explode(PHP_EOL, $message->body) as $line) { if (substr($line, 0, strlen($quote)) == $quote && $endOfQuote == false) { $parent .= substr($line, strlen($quote)) . "\n"; } else { $endOfQuote = true; $remains .= $line . "\n"; } } if ($parent !== '') { $message->parentQuote = $parent; $message->body = trim($remains); } } // reactions_count if cached, if not, reload it from the DB if ($message->reactions_count ?? $message->reactions()->count()) { $message->reactionsHtml = $this->prepareReactions($message); } if ($message->body) { $message->rtl = isRTL($message->body); } $message->publishedPrepared = prepareTime(strtotime($message->published)); if ($message->delivered) { $message->delivered = prepareDate(strtotime($message->delivered), true); } if ($message->displayed) { $message->displayed = prepareDate(strtotime($message->displayed), true); } $date = prepareDate(strtotime($message->published), false, false, true); if (empty($date)) { $date = $this->__('date.today'); } // We create the date wrapper if (!array_key_exists($date, $this->_wrapper)) { $this->_wrapper[$date] = []; } $messageDBSeen = $message->seen; $n = new Notif; if ($message->isMuc()) { $message->resolveColor(); // Cache the resolved presences for a while $key = $message->jidfrom . $message->resource; if (!isset($this->_mucPresences[$key])) { $this->_mucPresences[$key] = $this->user->session->presences() ->where('jid', $message->jidfrom) ->where('resource', $message->resource) ->where('muc', true) ->first(); } if ($this->_mucPresences[$key] && $this->_mucPresences[$key] !== true) { $message->moderator = ($this->_mucPresences[$key]->mucrole == 'moderator'); $message->mucjid = $this->_mucPresences[$key]->mucjid; $message->mine = $message->seen = ($this->_mucPresences[$key]->mucjid == $this->user->id); $message->icon_url = $this->_mucPresences[$key]->conferencePicture; } else { // No presence, we set a placeholder avatar as a fallback $message->icon_url = avatarPlaceholder($message->resource . 'groupchat'); $this->_mucPresences[$key] = true; } } // Only used for message replacement $message->originid = $message->originid == null && !$message->isMuc() ? $message->messageid : null; // Handle faulty replacing messages if ( $message->replace && ( ($message->replace->jidfrom != $message->jidfrom || $message->replace->resource != $message->resource) || ($message->isMuc() && !$this->_mucPresences[$message->jidfrom . $message->resource]) ) ) { unset($message->replace); unset($message->replaceid); } if ($message->seen === false) { $message->seen = ('chat|' . $message->jidfrom == $n->getCurrent()); } if ( $message->seen === true && $messageDBSeen === false ) { $this->user->messages() ->where('id', $message->id) ->update(['seen' => true]); } $msgkey = '<' . $message->jidfrom; $msgkey .= ($message->isMuc() && $message->resource != null) ? cleanupId($message->resource, true) : ''; $msgkey .= '>' . substr($message->published, 11, 5); $counter = count($this->_wrapper[$date]); $this->_wrapper[$date][$counter . $msgkey] = $message; if ($message->type == 'invitation') { $view = $this->tpl(); $view->assign('message', $message); $message->body = ($message->jidfrom == $this->user->id) ? $view->draw('_chat_invitation_self') : $view->draw('_chat_invitation'); } if (in_array($message->type, [ 'muc_owner', 'muc_admin', 'muc_outcast', 'jingle_reject', 'muc_member', 'jingle_incoming', 'jingle_outgoing', 'jingle_retract' ])) { $view = $this->tpl(); $view->assign('message', $message); $message->body = $view->draw('_chat_' . $message->type); } if ($message->type == 'jingle_end') { $view = $this->tpl(); $view->assign('message', $message); $view->assign('diff', false); $start = Message::where('thread', $message->thread) ->whereIn('type', ['jingle_incoming', 'jingle_outgoing']) ->first(); if ($start) { $diff = (new DateTime($start->created_at)) ->diff(new DateTime($message->created_at)); $view->assign('diff', $diff); } $message->body = $view->draw('_chat_jingle_end'); } return $this->_wrapper; } public function prepareEmbed(EmbedLight $embed, bool $withLink = false) { $tpl = $this->tpl(); $tpl->assign('embed', $embed); $tpl->assign('withlink', $withLink); return $tpl->draw('_chat_embed'); } public function prepareReactions(Message $message) { $view = $this->tpl(); $merged = []; $reactions = $message ->reactions() ->orderBy('created_at') ->get(); foreach ($reactions as $reaction) { if (!array_key_exists($reaction->emoji, $merged)) { $merged[$reaction->emoji] = []; } $merged[$reaction->emoji][] = $reaction->jidfrom; } $view->assign('message', $message); $view->assign('reactions', $merged); $view->assign('me', $this->user->id); return $view->draw('_chat_reactions'); } public function prepareHeader($jid, $muc = false) { $view = $this->tpl(); $view->assign('jid', $jid); $view->assign('muc', $muc); $view->assign( 'info', \App\Info::where('server', $this->user->session->host) ->where('node', '') ->first() ); $view->assign('anon', false); $view->assign('counter', $this->user->unreads(null, false, true)); if ($muc) { $view->assign('conference', $this->user->session->conferences() ->where('conference', $jid) ->with('info') ->first()); $mucinfo = \App\Info::where('server', explodeJid($jid)['server']) ->where('node', '') ->first(); if ($mucinfo && !empty($mucinfo->abuseaddresses)) { $view->assign('info', $mucinfo); } } else { $view->assign('roster', $this->user->session->contacts()->where('jid', $jid)->first()); $view->assign('contact', Contact::firstOrNew(['id' => $jid])); } return $view->draw('_chat_header'); } public function prepareEmpty() { $view = $this->tpl(); $chats = \App\Cache::c('chats'); if ($chats == null) { $chats = []; } $chats[$this->user->id] = true; $top = $this->user->session->topContacts() ->join(DB::raw('( select min(value) as value, jid as pjid from presences group by jid) as presences '), 'presences.pjid', '=', 'rosters.jid') ->where('value', '<', 5) ->whereNotIn('rosters.jid', array_keys($chats)) ->with('presence.capability') ->take(15) ->get(); $view->assign('top', $top); return $view->draw('_chat_empty'); } public function ajaxHttpGetExplore($page = 0) { $this->rpc('MovimTpl.fill', '#chat_explore', $this->prepareExplore($page)); } public function prepareExplore($page = 0) { $view = $this->tpl(); $pagination = 8; $users = Contact::public() ->notInRoster($this->user->session->id) ->orderByPresence() ->where('id', '!=', $this->user->id) ->skip($page * $pagination) ->take($pagination + 1) ->get(); $view->assign('presencestxt', getPresencesTxt()); $view->assign('users', $users); $view->assign('pagination', $pagination); $view->assign('page', $page); return $view->draw('_chat_explore'); } private function prepareComposeList(array $list) { $view = $this->tpl(); $view->assign('list', implode(', ', $list)); return $view->draw('_chat_compose_list'); } public function getSmileyPath($id) { return getSmileyPath($id); } }