summary refs log tree commit diff
path: root/src/timeline/TimelineView.cc
diff options
context:
space:
mode:
authorKonstantinos Sideris <sideris.konstantin@gmail.com>2018-06-17 19:18:12 +0300
committerKonstantinos Sideris <sideris.konstantin@gmail.com>2018-06-17 19:18:12 +0300
commit8704265978572e60f8b04d89cec2f404f5ea4113 (patch)
treef1e272705c26ce2b0121d6fffccd60f98246b84d /src/timeline/TimelineView.cc
parentAdd Visual Studio 2017 support (#336) (diff)
parentUpdate build instructions (diff)
downloadnheko-8704265978572e60f8b04d89cec2f404f5ea4113.tar.xz
Merge branch 'e2ee'
- Support for e2ee rooms
- Implement categories & file logging
- Let the user know when the app can't reach the server (#93)

fixes #13
fixes #326
Diffstat (limited to 'src/timeline/TimelineView.cc')
-rw-r--r--src/timeline/TimelineView.cc669
1 files changed, 593 insertions, 76 deletions
diff --git a/src/timeline/TimelineView.cc b/src/timeline/TimelineView.cc

index 71058d74..e437439e 100644 --- a/src/timeline/TimelineView.cc +++ b/src/timeline/TimelineView.cc
@@ -23,6 +23,8 @@ #include "ChatPage.h" #include "Config.h" #include "FloatingButton.h" +#include "Logging.hpp" +#include "Olm.hpp" #include "UserSettingsPage.h" #include "Utils.h" @@ -100,7 +102,7 @@ TimelineView::TimelineView(const QString &room_id, QWidget *parent) , room_id_{room_id} { init(); - http::client()->messages(room_id_, ""); + getMessages(); } void @@ -140,7 +142,7 @@ TimelineView::fetchHistory() return; isPaginationInProgress_ = true; - http::client()->messages(room_id_, prev_batch_token_); + getMessages(); paginationTimer_->start(5000); return; @@ -189,18 +191,13 @@ TimelineView::sliderMoved(int position) isPaginationInProgress_ = true; - // FIXME: Maybe move this to TimelineViewManager to remove the - // extra calls? - http::client()->messages(room_id_, prev_batch_token_); + getMessages(); } } void -TimelineView::addBackwardsEvents(const QString &room_id, const mtx::responses::Messages &msgs) +TimelineView::addBackwardsEvents(const mtx::responses::Messages &msgs) { - if (room_id_ != room_id) - return; - // We've reached the start of the timline and there're no more messages. if ((msgs.end == msgs.start) && msgs.chunk.size() == 0) { isTimelineFinished = true; @@ -239,19 +236,19 @@ TimelineItem * TimelineView::parseMessageEvent(const mtx::events::collections::TimelineEvents &event, TimelineDirection direction) { - namespace msg = mtx::events::msg; - using AudioEvent = mtx::events::RoomEvent<msg::Audio>; - using EmoteEvent = mtx::events::RoomEvent<msg::Emote>; - using FileEvent = mtx::events::RoomEvent<msg::File>; - using ImageEvent = mtx::events::RoomEvent<msg::Image>; - using NoticeEvent = mtx::events::RoomEvent<msg::Notice>; - using TextEvent = mtx::events::RoomEvent<msg::Text>; - using VideoEvent = mtx::events::RoomEvent<msg::Video>; + using namespace mtx::events; + + using AudioEvent = RoomEvent<msg::Audio>; + using EmoteEvent = RoomEvent<msg::Emote>; + using FileEvent = RoomEvent<msg::File>; + using ImageEvent = RoomEvent<msg::Image>; + using NoticeEvent = RoomEvent<msg::Notice>; + using TextEvent = RoomEvent<msg::Text>; + using VideoEvent = RoomEvent<msg::Video>; - if (mpark::holds_alternative<mtx::events::RedactionEvent<msg::Redaction>>(event)) { - auto redaction_event = - mpark::get<mtx::events::RedactionEvent<msg::Redaction>>(event); - const auto event_id = QString::fromStdString(redaction_event.redacts); + if (mpark::holds_alternative<RedactionEvent<msg::Redaction>>(event)) { + auto redaction_event = mpark::get<RedactionEvent<msg::Redaction>>(event); + const auto event_id = QString::fromStdString(redaction_event.redacts); QTimer::singleShot(0, this, [event_id, this]() { if (eventIds_.contains(event_id)) @@ -259,35 +256,96 @@ TimelineView::parseMessageEvent(const mtx::events::collections::TimelineEvents & }); return nullptr; - } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Audio>>(event)) { - auto audio = mpark::get<mtx::events::RoomEvent<msg::Audio>>(event); + } else if (mpark::holds_alternative<RoomEvent<msg::Audio>>(event)) { + auto audio = mpark::get<RoomEvent<msg::Audio>>(event); return processMessageEvent<AudioEvent, AudioItem>(audio, direction); - } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Emote>>(event)) { - auto emote = mpark::get<mtx::events::RoomEvent<msg::Emote>>(event); + } else if (mpark::holds_alternative<RoomEvent<msg::Emote>>(event)) { + auto emote = mpark::get<RoomEvent<msg::Emote>>(event); return processMessageEvent<EmoteEvent>(emote, direction); - } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::File>>(event)) { - auto file = mpark::get<mtx::events::RoomEvent<msg::File>>(event); + } else if (mpark::holds_alternative<RoomEvent<msg::File>>(event)) { + auto file = mpark::get<RoomEvent<msg::File>>(event); return processMessageEvent<FileEvent, FileItem>(file, direction); - } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Image>>(event)) { - auto image = mpark::get<mtx::events::RoomEvent<msg::Image>>(event); + } else if (mpark::holds_alternative<RoomEvent<msg::Image>>(event)) { + auto image = mpark::get<RoomEvent<msg::Image>>(event); return processMessageEvent<ImageEvent, ImageItem>(image, direction); - } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Notice>>(event)) { - auto notice = mpark::get<mtx::events::RoomEvent<msg::Notice>>(event); + } else if (mpark::holds_alternative<RoomEvent<msg::Notice>>(event)) { + auto notice = mpark::get<RoomEvent<msg::Notice>>(event); return processMessageEvent<NoticeEvent>(notice, direction); - } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Text>>(event)) { - auto text = mpark::get<mtx::events::RoomEvent<msg::Text>>(event); + } else if (mpark::holds_alternative<RoomEvent<msg::Text>>(event)) { + auto text = mpark::get<RoomEvent<msg::Text>>(event); return processMessageEvent<TextEvent>(text, direction); - } else if (mpark::holds_alternative<mtx::events::RoomEvent<msg::Video>>(event)) { - auto video = mpark::get<mtx::events::RoomEvent<msg::Video>>(event); + } else if (mpark::holds_alternative<RoomEvent<msg::Video>>(event)) { + auto video = mpark::get<RoomEvent<msg::Video>>(event); return processMessageEvent<VideoEvent, VideoItem>(video, direction); - } else if (mpark::holds_alternative<mtx::events::Sticker>(event)) { - return processMessageEvent<mtx::events::Sticker, StickerItem>( - mpark::get<mtx::events::Sticker>(event), direction); + } else if (mpark::holds_alternative<Sticker>(event)) { + return processMessageEvent<Sticker, StickerItem>(mpark::get<Sticker>(event), + direction); + } else if (mpark::holds_alternative<EncryptedEvent<msg::Encrypted>>(event)) { + auto decrypted = + parseEncryptedEvent(mpark::get<EncryptedEvent<msg::Encrypted>>(event)); + return parseMessageEvent(decrypted, direction); + } else if (mpark::holds_alternative<StateEvent<state::Encryption>>(event)) { + try { + cache::client()->setEncryptedRoom(room_id_.toStdString()); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to save room {} as encrypted", + room_id_.toStdString()); + } } return nullptr; } +TimelineEvent +TimelineView::parseEncryptedEvent(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e) +{ + MegolmSessionIndex index; + index.room_id = room_id_.toStdString(); + index.session_id = e.content.session_id; + index.sender_key = e.content.sender_key; + + mtx::events::RoomEvent<mtx::events::msg::Text> dummy; + dummy.origin_server_ts = e.origin_server_ts; + dummy.event_id = e.event_id; + dummy.sender = e.sender; + dummy.content.body = "-- Encrypted Event (No keys found for decryption) --"; + + if (!cache::client()->inboundMegolmSessionExists(index)) { + nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})", + index.room_id, + index.session_id, + e.sender); + // TODO: request megolm session_id & session_key from the sender. + return dummy; + } + + auto session = cache::client()->getInboundMegolmSession(index); + auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext); + + const auto msg_str = std::string((char *)res.data.data(), res.data.size()); + + // Add missing fields for the event. + json body = json::parse(msg_str); + body["event_id"] = e.event_id; + body["sender"] = e.sender; + body["origin_server_ts"] = e.origin_server_ts; + body["unsigned"] = e.unsigned_data; + + nhlog::crypto()->info("decrypted data: \n {}", body.dump(2)); + + json event_array = json::array(); + event_array.push_back(body); + + std::vector<TimelineEvent> events; + mtx::responses::utils::parse_timeline_events(event_array, events); + + if (events.size() == 1) + return events.at(0); + + dummy.content.body = "-- Encrypted Event (Unknown event type) --"; + return dummy; +} + void TimelineView::renderBottomEvents(const std::vector<TimelineEvent> &events) { @@ -427,10 +485,10 @@ TimelineView::init() paginationTimer_ = new QTimer(this); connect(paginationTimer_, &QTimer::timeout, this, &TimelineView::fetchHistory); - connect(http::client(), - &MatrixClient::messagesRetrieved, - this, - &TimelineView::addBackwardsEvents); + connect(this, &TimelineView::messagesRetrieved, this, &TimelineView::addBackwardsEvents); + + connect(this, &TimelineView::messageFailed, this, &TimelineView::handleFailedMessage); + connect(this, &TimelineView::messageSent, this, &TimelineView::updatePendingMessage); connect(scroll_area_->verticalScrollBar(), SIGNAL(valueChanged(int)), @@ -443,6 +501,27 @@ TimelineView::init() } void +TimelineView::getMessages() +{ + mtx::http::MessagesOpts opts; + opts.room_id = room_id_.toStdString(); + opts.from = prev_batch_token_.toStdString(); + + http::v2::client()->messages( + opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error("failed to call /messages ({}): {} - {}", + opts.room_id, + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); + return; + } + + emit messagesRetrieved(std::move(res)); + }); +} + +void TimelineView::updateLastSender(const QString &user_id, TimelineDirection direction) { if (direction == TimelineDirection::Bottom) @@ -513,8 +592,9 @@ TimelineView::addTimelineItem(TimelineItem *item, TimelineDirection direction) } void -TimelineView::updatePendingMessage(int txn_id, QString event_id) +TimelineView::updatePendingMessage(const std::string &txn_id, const QString &event_id) { + nhlog::ui()->info("[{}] message was received by the server", txn_id); if (!pending_msgs_.isEmpty() && pending_msgs_.head().txn_id == txn_id) { // We haven't received it yet auto msg = pending_msgs_.dequeue(); @@ -522,11 +602,18 @@ TimelineView::updatePendingMessage(int txn_id, QString event_id) if (msg.widget) { msg.widget->setEventId(event_id); - msg.widget->markReceived(); eventIds_[event_id] = msg.widget; - } - pending_sent_msgs_.append(msg); + // If the response comes after we have received the event from sync + // we've already marked the widget as received. + if (!msg.widget->isReceived()) { + msg.widget->markReceived(); + pending_sent_msgs_.append(msg); + } + } else { + nhlog::ui()->warn("[{}] received message response for invalid widget", + txn_id); + } } sendNextPendingMessage(); @@ -540,16 +627,28 @@ TimelineView::addUserMessage(mtx::events::MessageType ty, const QString &body) TimelineItem *view_item = new TimelineItem(ty, local_user_, body, with_sender, room_id_, scroll_widget_); + PendingMessage message; + message.ty = ty; + message.txn_id = http::v2::client()->generate_txn_id(); + message.body = body; + message.widget = view_item; + + try { + message.is_encrypted = cache::client()->isRoomEncrypted(room_id_.toStdString()); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to check encryption status of room {}", e.what()); + view_item->deleteLater(); + + // TODO: Send a notification to the user. + + return; + } + addTimelineItem(view_item); lastMessageDirection_ = TimelineDirection::Bottom; - QApplication::processEvents(); - saveLastMessageInfo(local_user_, QDateTime::currentDateTime()); - - int txn_id = http::client()->incrementTransactionId(); - PendingMessage message(ty, txn_id, body, "", "", -1, "", view_item); handleNewUserMessage(message); } @@ -567,19 +666,98 @@ TimelineView::sendNextPendingMessage() if (pending_msgs_.size() == 0) return; + using namespace mtx::events; + PendingMessage &m = pending_msgs_.head(); + + nhlog::ui()->info("[{}] sending next queued message", m.txn_id); + + if (m.is_encrypted) { + prepareEncryptedMessage(std::move(m)); + nhlog::ui()->info("[{}] sending encrypted event", m.txn_id); + return; + } + switch (m.ty) { - case mtx::events::MessageType::Audio: - case mtx::events::MessageType::Image: - case mtx::events::MessageType::Video: - case mtx::events::MessageType::File: - // FIXME: Improve the API - http::client()->sendRoomMessage( - m.ty, m.txn_id, room_id_, m.filename, m.mime, m.media_size, m.body); + case mtx::events::MessageType::Audio: { + http::v2::client()->send_room_message<msg::Audio, EventType::RoomMessage>( + room_id_.toStdString(), + m.txn_id, + toRoomMessage<msg::Audio>(m), + std::bind(&TimelineView::sendRoomMessageHandler, + this, + m.txn_id, + std::placeholders::_1, + std::placeholders::_2)); + + break; + } + case mtx::events::MessageType::Image: { + http::v2::client()->send_room_message<msg::Image, EventType::RoomMessage>( + room_id_.toStdString(), + m.txn_id, + toRoomMessage<msg::Image>(m), + std::bind(&TimelineView::sendRoomMessageHandler, + this, + m.txn_id, + std::placeholders::_1, + std::placeholders::_2)); + + break; + } + case mtx::events::MessageType::Video: { + http::v2::client()->send_room_message<msg::Video, EventType::RoomMessage>( + room_id_.toStdString(), + m.txn_id, + toRoomMessage<msg::Video>(m), + std::bind(&TimelineView::sendRoomMessageHandler, + this, + m.txn_id, + std::placeholders::_1, + std::placeholders::_2)); + + break; + } + case mtx::events::MessageType::File: { + http::v2::client()->send_room_message<msg::File, EventType::RoomMessage>( + room_id_.toStdString(), + m.txn_id, + toRoomMessage<msg::File>(m), + std::bind(&TimelineView::sendRoomMessageHandler, + this, + m.txn_id, + std::placeholders::_1, + std::placeholders::_2)); + + break; + } + case mtx::events::MessageType::Text: { + http::v2::client()->send_room_message<msg::Text, EventType::RoomMessage>( + room_id_.toStdString(), + m.txn_id, + toRoomMessage<msg::Text>(m), + std::bind(&TimelineView::sendRoomMessageHandler, + this, + m.txn_id, + std::placeholders::_1, + std::placeholders::_2)); + + break; + } + case mtx::events::MessageType::Emote: { + http::v2::client()->send_room_message<msg::Emote, EventType::RoomMessage>( + room_id_.toStdString(), + m.txn_id, + toRoomMessage<msg::Emote>(m), + std::bind(&TimelineView::sendRoomMessageHandler, + this, + m.txn_id, + std::placeholders::_1, + std::placeholders::_2)); break; + } default: - http::client()->sendRoomMessage( - m.ty, m.txn_id, room_id_, m.body, m.mime, m.media_size); + nhlog::ui()->warn("cannot send unknown message type: {}", m.body.toStdString()); break; } } @@ -593,7 +771,7 @@ TimelineView::notifyForLastEvent() if (lastTimelineItem) emit updateLastTimelineMessage(room_id_, lastTimelineItem->descriptionMessage()); else - qWarning() << "Cast to TimelineView failed" << room_id_; + nhlog::ui()->warn("cast to TimelineView failed: {}", room_id_.toStdString()); } void @@ -606,51 +784,51 @@ TimelineView::notifyForLastEvent(const TimelineEvent &event) } bool -TimelineView::isPendingMessage(const QString &txnid, +TimelineView::isPendingMessage(const std::string &txn_id, const QString &sender, const QString &local_userid) { if (sender != local_userid) return false; - auto match_txnid = [txnid](const auto &msg) -> bool { - return QString::number(msg.txn_id) == txnid; - }; + auto match_txnid = [txn_id](const auto &msg) -> bool { return msg.txn_id == txn_id; }; return std::any_of(pending_msgs_.cbegin(), pending_msgs_.cend(), match_txnid) || std::any_of(pending_sent_msgs_.cbegin(), pending_sent_msgs_.cend(), match_txnid); } void -TimelineView::removePendingMessage(const QString &txnid) +TimelineView::removePendingMessage(const std::string &txn_id) { - if (txnid.isEmpty()) + if (txn_id.empty()) return; for (auto it = pending_sent_msgs_.begin(); it != pending_sent_msgs_.end(); ++it) { - if (QString::number(it->txn_id) == txnid) { + if (it->txn_id == txn_id) { int index = std::distance(pending_sent_msgs_.begin(), it); pending_sent_msgs_.removeAt(index); if (pending_sent_msgs_.isEmpty()) sendNextPendingMessage(); - return; + nhlog::ui()->info("[{}] removed message with sync", txn_id); } } for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); ++it) { - if (QString::number(it->txn_id) == txnid) { - int index = std::distance(pending_msgs_.begin(), it); - pending_msgs_.removeAt(index); + if (it->txn_id == txn_id) { + if (it->widget) + it->widget->markReceived(); + + nhlog::ui()->info("[{}] received sync before message response", txn_id); return; } } } void -TimelineView::handleFailedMessage(int txnid) +TimelineView::handleFailedMessage(const std::string &txn_id) { - Q_UNUSED(txnid); + Q_UNUSED(txn_id); // Note: We do this even if the message has already been echoed. QTimer::singleShot(2000, this, SLOT(sendNextPendingMessage())); } @@ -673,7 +851,16 @@ TimelineView::readLastEvent() const const auto eventId = getLastEventId(); if (!eventId.isEmpty()) - http::client()->readEvent(room_id_, eventId); + http::v2::client()->read_event(room_id_.toStdString(), + eventId.toStdString(), + [this, eventId](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn( + "failed to read event ({}, {})", + room_id_.toStdString(), + eventId.toStdString()); + } + }); } QString @@ -743,7 +930,8 @@ void TimelineView::removeEvent(const QString &event_id) { if (!eventIds_.contains(event_id)) { - qWarning() << "unknown event_id couldn't be removed:" << event_id; + nhlog::ui()->warn("cannot remove widget with unknown event_id: {}", + event_id.toStdString()); return; } @@ -860,3 +1048,332 @@ TimelineView::isDateDifference(const QDateTime &first, const QDateTime &second) return diffInSeconds > fifteenMins; } + +void +TimelineView::sendRoomMessageHandler(const std::string &txn_id, + const mtx::responses::EventId &res, + mtx::http::RequestErr err) +{ + if (err) { + const int status_code = static_cast<int>(err->status_code); + nhlog::net()->warn("[{}] failed to send message: {} {}", + txn_id, + err->matrix_error.error, + status_code); + emit messageFailed(txn_id); + return; + } + + emit messageSent(txn_id, QString::fromStdString(res.event_id.to_string())); +} + +template<> +mtx::events::msg::Audio +toRoomMessage<mtx::events::msg::Audio>(const PendingMessage &m) +{ + mtx::events::msg::Audio audio; + audio.info.mimetype = m.mime.toStdString(); + audio.info.size = m.media_size; + audio.body = m.filename.toStdString(); + audio.url = m.body.toStdString(); + return audio; +} + +template<> +mtx::events::msg::Image +toRoomMessage<mtx::events::msg::Image>(const PendingMessage &m) +{ + mtx::events::msg::Image image; + image.info.mimetype = m.mime.toStdString(); + image.info.size = m.media_size; + image.body = m.filename.toStdString(); + image.url = m.body.toStdString(); + return image; +} + +template<> +mtx::events::msg::Video +toRoomMessage<mtx::events::msg::Video>(const PendingMessage &m) +{ + mtx::events::msg::Video video; + video.info.mimetype = m.mime.toStdString(); + video.info.size = m.media_size; + video.body = m.filename.toStdString(); + video.url = m.body.toStdString(); + return video; +} + +template<> +mtx::events::msg::Emote +toRoomMessage<mtx::events::msg::Emote>(const PendingMessage &m) +{ + mtx::events::msg::Emote emote; + emote.body = m.body.toStdString(); + return emote; +} + +template<> +mtx::events::msg::File +toRoomMessage<mtx::events::msg::File>(const PendingMessage &m) +{ + mtx::events::msg::File file; + file.info.mimetype = m.mime.toStdString(); + file.info.size = m.media_size; + file.body = m.filename.toStdString(); + file.url = m.body.toStdString(); + return file; +} + +template<> +mtx::events::msg::Text +toRoomMessage<mtx::events::msg::Text>(const PendingMessage &m) +{ + mtx::events::msg::Text text; + text.body = m.body.toStdString(); + return text; +} + +void +TimelineView::prepareEncryptedMessage(const PendingMessage &msg) +{ + const auto room_id = room_id_.toStdString(); + + using namespace mtx::events; + using namespace mtx::identifiers; + + json content; + + // Serialize the message to the plaintext that will be encrypted. + switch (msg.ty) { + case MessageType::Audio: { + content = json(toRoomMessage<msg::Audio>(msg)); + break; + } + case MessageType::Emote: { + content = json(toRoomMessage<msg::Emote>(msg)); + break; + } + case MessageType::File: { + content = json(toRoomMessage<msg::File>(msg)); + break; + } + case MessageType::Image: { + content = json(toRoomMessage<msg::Image>(msg)); + break; + } + case MessageType::Text: { + content = json(toRoomMessage<msg::Text>(msg)); + break; + } + case MessageType::Video: { + content = json(toRoomMessage<msg::Video>(msg)); + break; + } + default: + break; + } + + json doc{{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}}; + + try { + // Check if we have already an outbound megolm session then we can use. + if (cache::client()->outboundMegolmSessionExists(room_id)) { + auto data = olm::encrypt_group_message( + room_id, http::v2::client()->device_id(), doc.dump()); + + http::v2::client() + ->send_room_message<msg::Encrypted, EventType::RoomEncrypted>( + room_id, + msg.txn_id, + data, + std::bind(&TimelineView::sendRoomMessageHandler, + this, + msg.txn_id, + std::placeholders::_1, + std::placeholders::_2)); + return; + } + + nhlog::ui()->info("creating new outbound megolm session"); + + // Create a new outbound megolm session. + auto outbound_session = olm::client()->init_outbound_group_session(); + const auto session_id = mtx::crypto::session_id(outbound_session.get()); + const auto session_key = mtx::crypto::session_key(outbound_session.get()); + + // TODO: needs to be moved in the lib. + auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"}, + {"room_id", room_id}, + {"session_id", session_id}, + {"session_key", session_key}}; + + // Saving the new megolm session. + // TODO: Maybe it's too early to save. + OutboundGroupSessionData session_data; + session_data.session_id = session_id; + session_data.session_key = session_key; + session_data.message_index = 0; // TODO Update me + cache::client()->saveOutboundMegolmSession( + room_id, session_data, std::move(outbound_session)); + + const auto members = cache::client()->roomMembers(room_id); + nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id); + + auto keeper = std::make_shared<StateKeeper>( + [megolm_payload, room_id, doc, txn_id = msg.txn_id, this]() { + try { + auto data = olm::encrypt_group_message( + room_id, http::v2::client()->device_id(), doc.dump()); + + http::v2::client() + ->send_room_message<msg::Encrypted, EventType::RoomEncrypted>( + room_id, + txn_id, + data, + std::bind(&TimelineView::sendRoomMessageHandler, + this, + txn_id, + std::placeholders::_1, + std::placeholders::_2)); + + } catch (const lmdb::error &e) { + nhlog::db()->critical( + "failed to save megolm outbound session: {}", e.what()); + } + }); + + mtx::requests::QueryKeys req; + for (const auto &member : members) + req.device_keys[member] = {}; + + http::v2::client()->query_keys( + req, + [keeper = std::move(keeper), megolm_payload, this]( + const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to query device keys: {} {}", + err->matrix_error.error, + static_cast<int>(err->status_code)); + // TODO: Mark the event as failed. Communicate with the UI. + return; + } + + for (const auto &entry : res.device_keys) { + for (const auto &dev : entry.second) { + nhlog::net()->info("received device {}", dev.first); + + const auto device_keys = dev.second.keys; + const auto curveKey = "curve25519:" + dev.first; + const auto edKey = "ed25519:" + dev.first; + + if ((device_keys.find(curveKey) == device_keys.end()) || + (device_keys.find(edKey) == device_keys.end())) { + nhlog::net()->info( + "ignoring malformed keys for device {}", + dev.first); + continue; + } + + DevicePublicKeys pks; + pks.ed25519 = device_keys.at(edKey); + pks.curve25519 = device_keys.at(curveKey); + + // Validate signatures + for (const auto &algo : dev.second.keys) { + nhlog::net()->info( + "dev keys {} {}", algo.first, algo.second); + } + + auto room_key = + olm::client() + ->create_room_key_event(UserId(dev.second.user_id), + pks.ed25519, + megolm_payload) + .dump(); + + http::v2::client()->claim_keys( + dev.second.user_id, + {dev.second.device_id}, + std::bind(&TimelineView::handleClaimedKeys, + this, + keeper, + room_key, + pks, + dev.second.user_id, + dev.second.device_id, + std::placeholders::_1, + std::placeholders::_2)); + } + } + }); + + } catch (const lmdb::error &e) { + nhlog::db()->critical( + "failed to open outbound megolm session ({}): {}", room_id, e.what()); + return; + } +} + +void +TimelineView::handleClaimedKeys(std::shared_ptr<StateKeeper> keeper, + const std::string &room_key, + const DevicePublicKeys &pks, + const std::string &user_id, + const std::string &device_id, + const mtx::responses::ClaimKeys &res, + mtx::http::RequestErr err) +{ + if (err) { + nhlog::net()->warn("claim keys error: {}", err->matrix_error.error); + return; + } + + nhlog::net()->info("claimed keys for {} - {}", user_id, device_id); + + if (res.one_time_keys.size() == 0) { + nhlog::net()->info("no one-time keys found for device_id: {}", device_id); + return; + } + + if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) { + nhlog::net()->info( + "no one-time keys found in device_id {} for the user {}", device_id, user_id); + return; + } + + auto retrieved_devices = res.one_time_keys.at(user_id); + + for (const auto &rd : retrieved_devices) { + nhlog::net()->info("{} : \n {}", rd.first, rd.second.dump(2)); + + // TODO: Verify signatures + auto otk = rd.second.begin()->at("key"); + auto id_key = pks.curve25519; + + auto s = olm::client()->create_outbound_session(id_key, otk); + + auto device_msg = + olm::client()->create_olm_encrypted_content(s.get(), room_key, pks.curve25519); + + try { + cache::client()->saveOlmSession(id_key, std::move(s)); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to save outbound olm session: {}", e.what()); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to pickle outbound olm session: {}", + e.what()); + } + + json body{{"messages", {{user_id, {{device_id, device_msg}}}}}}; + + http::v2::client()->send_to_device( + "m.room.encrypted", body, [keeper](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to send " + "send_to_device " + "message: {}", + err->matrix_error.error); + } + }); + } +}