Skip to content
Snippets Groups Projects
TimelineModel.cpp 59.9 KiB
Newer Older
  • Learn to ignore specific revisions
  • #include "TimelineModel.h"
    
    
    #include <algorithm>
    
    #include <thread>
    
    #include <type_traits>
    
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    #include <QCache>
    
    #include <QFileDialog>
    #include <QMimeDatabase>
    
    #include <QRegularExpression>
    
    #include <QStandardPaths>
    
    #include "ChatPage.h"
    
    #include "EventAccessors.h"
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    #include "Logging.h"
    
    #include "MxcImageProvider.h"
    
    #include "Olm.h"
    
    #include "TimelineViewManager.h"
    
    #include "Utils.h"
    
    #include "dialogs/RawMessage.h"
    
    Q_DECLARE_METATYPE(QModelIndex)
    
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    namespace std {
    inline uint
    qHash(const std::string &key, uint seed = 0)
    {
            return qHash(QByteArray::fromRawData(key.data(), key.length()), seed);
    }
    }
    
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    namespace {
    
    struct RoomEventType
    
            template<class T>
            qml_mtx_events::EventType operator()(const mtx::events::Event<T> &e)
            {
                    using mtx::events::EventType;
                    switch (e.type) {
                    case EventType::RoomKeyRequest:
                            return qml_mtx_events::EventType::KeyRequest;
    
                    case EventType::Reaction:
                            return qml_mtx_events::EventType::Reaction;
    
                    case EventType::RoomAliases:
                            return qml_mtx_events::EventType::Aliases;
                    case EventType::RoomAvatar:
                            return qml_mtx_events::EventType::Avatar;
                    case EventType::RoomCanonicalAlias:
                            return qml_mtx_events::EventType::CanonicalAlias;
                    case EventType::RoomCreate:
    
                            return qml_mtx_events::EventType::RoomCreate;
    
                    case EventType::RoomEncrypted:
                            return qml_mtx_events::EventType::Encrypted;
                    case EventType::RoomEncryption:
                            return qml_mtx_events::EventType::Encryption;
                    case EventType::RoomGuestAccess:
    
                            return qml_mtx_events::EventType::RoomGuestAccess;
    
                    case EventType::RoomHistoryVisibility:
    
                            return qml_mtx_events::EventType::RoomHistoryVisibility;
    
                    case EventType::RoomJoinRules:
    
                            return qml_mtx_events::EventType::RoomJoinRules;
    
                    case EventType::RoomMember:
                            return qml_mtx_events::EventType::Member;
                    case EventType::RoomMessage:
                            return qml_mtx_events::EventType::UnknownMessage;
                    case EventType::RoomName:
                            return qml_mtx_events::EventType::Name;
                    case EventType::RoomPowerLevels:
                            return qml_mtx_events::EventType::PowerLevels;
                    case EventType::RoomTopic:
                            return qml_mtx_events::EventType::Topic;
                    case EventType::RoomTombstone:
                            return qml_mtx_events::EventType::Tombstone;
                    case EventType::RoomRedaction:
                            return qml_mtx_events::EventType::Redaction;
                    case EventType::RoomPinnedEvents:
                            return qml_mtx_events::EventType::PinnedEvents;
                    case EventType::Sticker:
                            return qml_mtx_events::EventType::Sticker;
                    case EventType::Tag:
                            return qml_mtx_events::EventType::Tag;
                    case EventType::Unsupported:
                            return qml_mtx_events::EventType::Unsupported;
    
                    default:
                            return qml_mtx_events::EventType::UnknownMessage;
    
                    }
            }
            qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Audio> &)
            {
                    return qml_mtx_events::EventType::AudioMessage;
            }
            qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Emote> &)
            {
                    return qml_mtx_events::EventType::EmoteMessage;
            }
            qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::File> &)
            {
                    return qml_mtx_events::EventType::FileMessage;
            }
            qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Image> &)
            {
                    return qml_mtx_events::EventType::ImageMessage;
            }
            qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Notice> &)
            {
                    return qml_mtx_events::EventType::NoticeMessage;
            }
            qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Text> &)
            {
                    return qml_mtx_events::EventType::TextMessage;
            }
            qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Video> &)
            {
                    return qml_mtx_events::EventType::VideoMessage;
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            }
    
            qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Redacted> &)
            {
                    return qml_mtx_events::EventType::Redacted;
    
            // ::EventType::Type operator()(const Event<mtx::events::msg::Location> &e) { return
            // ::EventType::LocationMessage; }
    };
    
    
    qml_mtx_events::EventType
    
    toRoomEventType(const mtx::events::collections::TimelineEvents &event)
    
            return std::visit(RoomEventType{}, event);
    
    QString
    toRoomEventTypeString(const mtx::events::collections::TimelineEvents &event)
    {
            return std::visit([](const auto &e) { return QString::fromStdString(to_string(e.type)); },
                              event);
    }
    
    
    TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent)
    
      : QAbstractListModel(parent)
    
      , events(room_id.toStdString(), this)
    
      , manager_(manager)
    
              this,
              &TimelineModel::redactionFailed,
              this,
              [](const QString &msg) { emit ChatPage::instance()->showNotification(msg); },
              Qt::QueuedConnection);
    
            connect(this,
                    &TimelineModel::newMessageToSend,
                    this,
                    &TimelineModel::addPendingMessage,
                    Qt::QueuedConnection);
    
            connect(this, &TimelineModel::addPendingMessageToStore, &events, &EventStore::addPending);
    
              &events,
              &EventStore::dataChanged,
    
              [this](int from, int to) {
    
                      nhlog::ui()->debug(
                        "data changed {} to {}", events.size() - to - 1, events.size() - from - 1);
                      emit dataChanged(index(events.size() - to - 1, 0),
                                       index(events.size() - from - 1, 0));
    
    
            connect(&events, &EventStore::beginInsertRows, this, [this](int from, int to) {
    
                    int first = events.size() - to;
                    int last  = events.size() - from;
                    if (from >= events.size()) {
                            int batch_size = to - from;
                            first += batch_size;
                            last += batch_size;
                    } else {
                            first -= 1;
                            last -= 1;
                    }
                    nhlog::ui()->debug("begin insert from {} to {}", first, last);
                    beginInsertRows(QModelIndex(), first, last);
    
            });
            connect(&events, &EventStore::endInsertRows, this, [this]() { endInsertRows(); });
    
            connect(&events, &EventStore::beginResetModel, this, [this]() { beginResetModel(); });
            connect(&events, &EventStore::endResetModel, this, [this]() { endResetModel(); });
    
            connect(&events, &EventStore::newEncryptedImage, this, &TimelineModel::newEncryptedImage);
    
            connect(
              &events, &EventStore::fetchedMore, this, [this]() { setPaginationInProgress(false); });
    
    QHash<int, QByteArray>
    TimelineModel::roleNames() const
    {
            return {
    
              {Type, "type"},
    
              {TypeString, "typeString"},
    
              {Body, "body"},
              {FormattedBody, "formattedBody"},
              {UserId, "userId"},
              {UserName, "userName"},
              {Timestamp, "timestamp"},
    
              {ThumbnailUrl, "thumbnailUrl"},
    
              {Blurhash, "blurhash"},
    
    Nicolas Werner's avatar
    Nicolas Werner committed
              {Filename, "filename"},
    
    Nicolas Werner's avatar
    Nicolas Werner committed
              {Filesize, "filesize"},
    
    Nicolas Werner's avatar
    Nicolas Werner committed
              {MimeType, "mimetype"},
    
              {Height, "height"},
              {Width, "width"},
              {ProportionalHeight, "proportionalHeight"},
    
              {Id, "id"},
    
              {State, "state"},
    
              {IsEncrypted, "isEncrypted"},
    
              {IsRoomEncrypted, "isRoomEncrypted"},
    
              {ReplyTo, "replyTo"},
    
              {Reactions, "reactions"},
    
              {RoomId, "roomId"},
    
              {RoomName, "roomName"},
              {RoomTopic, "roomTopic"},
    
    Nicolas Werner's avatar
    Nicolas Werner committed
              {Dump, "dump"},
    
            };
    }
    int
    TimelineModel::rowCount(const QModelIndex &parent) const
    {
            Q_UNUSED(parent);
    
            return this->events.size();
    
    QVariantMap
    
    TimelineModel::getDump(QString eventId, QString relatedTo) const
    
            if (auto event = events.event(eventId.toStdString(), relatedTo.toStdString()))
                    return data(*event, Dump).toMap();
    
    QVariant
    
    TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int role) const
    
            using namespace mtx::accessors;
    
            namespace acc = mtx::accessors;
    
            switch (role) {
            case UserId:
    
                    return QVariant(QString::fromStdString(acc::sender(event)));
    
                    return QVariant(displayName(QString::fromStdString(acc::sender(event))));
    
                    return QVariant(origin_server_ts(event));
    
            case Type:
    
                    return QVariant(toRoomEventType(event));
    
            case TypeString:
                    return QVariant(toRoomEventTypeString(event));
    
            case IsOnlyEmoji: {
                    QString qBody = QString::fromStdString(body(event));
    
                    QVector<uint> utf32_string = qBody.toUcs4();
                    int emojiCount             = 0;
    
                    for (auto &code : utf32_string) {
                            if (utils::codepointIsEmoji(code)) {
                                    emojiCount++;
                            } else {
                                    return QVariant(0);
                            }
                    }
    
                    return QVariant(emojiCount);
            }
    
            case Body:
    
                    return QVariant(utils::replaceEmoji(QString::fromStdString(body(event))));
    
            case FormattedBody: {
                    const static QRegularExpression replyFallback(
                      "<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption);
    
    
                    bool isReply = !in_reply_to_event(event).empty();
    
                    auto formattedBody_ = QString::fromStdString(formatted_body(event));
                    if (formattedBody_.isEmpty()) {
                            auto body_ = QString::fromStdString(body(event));
    
                            if (isReply) {
                                    while (body_.startsWith("> "))
                                            body_ = body_.right(body_.size() - body_.indexOf('\n') - 1);
                                    if (body_.startsWith('\n'))
                                            body_ = body_.right(body_.size() - 1);
                            }
    
                            formattedBody_ = body_.toHtmlEscaped().replace('\n', "<br>");
    
                    } else {
                            if (isReply)
                                    formattedBody_ = formattedBody_.remove(replyFallback);
                    }
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    
                    formattedBody_.replace("<img src=\"mxc:&#47;&#47;", "<img src=\"image://mxcImage/");
                    formattedBody_.replace("<img src=\"mxc://", "<img src=\"image://mxcImage/");
    
    
                    return QVariant(utils::replaceEmoji(
                      utils::linkifyMessage(utils::escapeBlacklistedHtml(formattedBody_))));
    
                    return QVariant(QString::fromStdString(url(event)));
    
            case ThumbnailUrl:
    
                    return QVariant(QString::fromStdString(thumbnail_url(event)));
    
            case Blurhash:
                    return QVariant(QString::fromStdString(blurhash(event)));
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            case Filename:
    
                    return QVariant(QString::fromStdString(filename(event)));
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            case Filesize:
    
                    return QVariant(utils::humanReadableFileSize(filesize(event)));
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            case MimeType:
    
                    return QVariant(QString::fromStdString(mimetype(event)));
    
                    return QVariant(qulonglong{media_height(event)});
    
                    return QVariant(qulonglong{media_width(event)});
            case ProportionalHeight: {
                    auto w = media_width(event);
                    if (w == 0)
                            w = 1;
    
                    double prop = media_height(event) / (double)w;
    
                    return QVariant(prop > 0 ? prop : 1.);
            }
    
            case Id:
    
                    return QVariant(QString::fromStdString(event_id(event)));
    
                    auto id             = QString::fromStdString(event_id(event));
    
                    auto containsOthers = [](const auto &vec) {
                            for (const auto &e : vec)
                                    if (e.second != http::client()->user_id().to_string())
                                            return true;
                            return false;
                    };
    
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    // only show read receipts for messages not from us
    
                    if (acc::sender(event) != http::client()->user_id().to_string())
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                            return qml_mtx_events::Empty;
    
                    else if (!id.isEmpty() && id[0] == "m")
    
                            return qml_mtx_events::Sent;
    
                    else if (read.contains(id) || containsOthers(cache::readReceipts(id, room_id_)))
    
                    else
                            return qml_mtx_events::Received;
    
            case IsEncrypted: {
    
                    auto id              = event_id(event);
                    auto encrypted_event = events.event(id, id, false);
                    return encrypted_event &&
                           std::holds_alternative<
                             mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
                             *encrypted_event);
    
            case IsRoomEncrypted: {
                    return cache::isRoomEncrypted(room_id_.toStdString());
            }
    
            case ReplyTo:
                    return QVariant(QString::fromStdString(in_reply_to_event(event)));
    
            case Reactions: {
    
                    return {};
    
                    return QVariant(room_id_);
    
            case RoomName:
    
                    return QVariant(QString::fromStdString(room_name(event)));
    
            case RoomTopic:
    
                    return QVariant(QString::fromStdString(room_topic(event)));
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            case Dump: {
                    QVariantMap m;
                    auto names = roleNames();
    
    
                    // m.insert(names[Section], data(id, static_cast<int>(Section)));
    
                    m.insert(names[Type], data(event, static_cast<int>(Type)));
                    m.insert(names[TypeString], data(event, static_cast<int>(TypeString)));
                    m.insert(names[IsOnlyEmoji], data(event, static_cast<int>(IsOnlyEmoji)));
                    m.insert(names[Body], data(event, static_cast<int>(Body)));
                    m.insert(names[FormattedBody], data(event, static_cast<int>(FormattedBody)));
                    m.insert(names[UserId], data(event, static_cast<int>(UserId)));
                    m.insert(names[UserName], data(event, static_cast<int>(UserName)));
                    m.insert(names[Timestamp], data(event, static_cast<int>(Timestamp)));
                    m.insert(names[Url], data(event, static_cast<int>(Url)));
                    m.insert(names[ThumbnailUrl], data(event, static_cast<int>(ThumbnailUrl)));
                    m.insert(names[Blurhash], data(event, static_cast<int>(Blurhash)));
                    m.insert(names[Filename], data(event, static_cast<int>(Filename)));
                    m.insert(names[Filesize], data(event, static_cast<int>(Filesize)));
                    m.insert(names[MimeType], data(event, static_cast<int>(MimeType)));
                    m.insert(names[Height], data(event, static_cast<int>(Height)));
                    m.insert(names[Width], data(event, static_cast<int>(Width)));
                    m.insert(names[ProportionalHeight],
                             data(event, static_cast<int>(ProportionalHeight)));
                    m.insert(names[Id], data(event, static_cast<int>(Id)));
                    m.insert(names[State], data(event, static_cast<int>(State)));
                    m.insert(names[IsEncrypted], data(event, static_cast<int>(IsEncrypted)));
                    m.insert(names[IsRoomEncrypted], data(event, static_cast<int>(IsRoomEncrypted)));
                    m.insert(names[ReplyTo], data(event, static_cast<int>(ReplyTo)));
                    m.insert(names[RoomName], data(event, static_cast<int>(RoomName)));
                    m.insert(names[RoomTopic], data(event, static_cast<int>(RoomTopic)));
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    
                    return QVariant(m);
            }
    
            default:
                    return QVariant();
            }
    }
    
    
    QVariant
    TimelineModel::data(const QModelIndex &index, int role) const
    {
            using namespace mtx::accessors;
            namespace acc = mtx::accessors;
    
            if (index.row() < 0 && index.row() >= rowCount())
    
                    return QVariant();
    
    
            auto event = events.event(rowCount() - index.row() - 1);
    
            if (!event)
                    return "";
    
    
            if (role == Section) {
    
                    QDateTime date = origin_server_ts(*event);
    
                    date.setTime(QTime());
    
    
                    std::string userId = acc::sender(*event);
    
                    for (int r = rowCount() - index.row(); r < events.size(); r++) {
                            auto tempEv = events.event(r);
                            if (!tempEv)
                                    break;
    
                            QDateTime prevDate = origin_server_ts(*tempEv);
    
                            prevDate.setTime(QTime());
                            if (prevDate != date)
                                    return QString("%2 %1")
                                      .arg(date.toMSecsSinceEpoch())
                                      .arg(QString::fromStdString(userId));
    
    
                            std::string prevUserId = acc::sender(*tempEv);
    
                            if (userId != prevUserId)
                                    break;
                    }
    
                    return QString("%1").arg(QString::fromStdString(userId));
            }
    
    
            return data(*event, role);
    
    bool
    TimelineModel::canFetchMore(const QModelIndex &) const
    {
    
            if (!events.size())
    
            if (auto first = events.event(0);
                first &&
                !std::holds_alternative<mtx::events::StateEvent<mtx::events::state::Create>>(*first))
    
    void
    TimelineModel::setPaginationInProgress(const bool paginationInProgress)
    {
            if (m_paginationInProgress == paginationInProgress) {
                    return;
            }
    
            m_paginationInProgress = paginationInProgress;
            emit paginationInProgressChanged(m_paginationInProgress);
    }
    
    
    void
    TimelineModel::fetchMore(const QModelIndex &)
    {
    
            if (m_paginationInProgress) {
    
                    nhlog::ui()->warn("Already loading older messages");
                    return;
            }
    
    
            setPaginationInProgress(true);
    
    
            events.fetchMore();
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    void
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    TimelineModel::addEvents(const mtx::responses::Timeline &timeline)
    
            if (timeline.events.empty())
                    return;
    
    
            events.handleSync(timeline);
    
            if (!timeline.events.empty())
                    updateLastMessage();
    
    template<typename T>
    auto
    isMessage(const mtx::events::RoomEvent<T> &e)
      -> std::enable_if_t<std::is_same<decltype(e.content.msgtype), std::string>::value, bool>
    {
            return true;
    }
    
    template<typename T>
    auto
    isMessage(const mtx::events::Event<T> &)
    {
            return false;
    }
    
    
    template<typename T>
    auto
    isMessage(const mtx::events::EncryptedEvent<T> &)
    {
            return true;
    }
    
    
    // Workaround. We also want to see a room at the top, if we just joined it
    auto
    isYourJoin(const mtx::events::StateEvent<mtx::events::state::Member> &e)
    {
            return e.content.membership == mtx::events::state::Membership::Join &&
                   e.state_key == http::client()->user_id().to_string();
    }
    template<typename T>
    auto
    isYourJoin(const mtx::events::Event<T> &)
    {
            return false;
    }
    
    
    void
    TimelineModel::updateLastMessage()
    
            for (auto it = events.size() - 1; it >= 0; --it) {
                    auto event = events.event(it, decryptDescription);
                    if (!event)
                            continue;
    
                    if (std::visit([](const auto &e) -> bool { return isYourJoin(e); }, *event)) {
                            auto time   = mtx::accessors::origin_server_ts(*event);
    
                            uint64_t ts = time.toMSecsSinceEpoch();
                            emit manager_->updateRoomsLastMessage(
                              room_id_,
    
                              DescInfo{QString::fromStdString(mtx::accessors::event_id(*event)),
    
                                       QString::fromStdString(http::client()->user_id().to_string()),
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                                       tr("You joined this room."),
    
                                       utils::descriptiveTime(time),
                                       ts,
                                       time});
                            return;
                    }
    
                    if (!std::visit([](const auto &e) -> bool { return isMessage(e); }, *event))
    
                            continue;
    
                    auto description = utils::getMessageDescription(
    
                      *event, QString::fromStdString(http::client()->user_id().to_string()), room_id_);
    
                    emit manager_->updateRoomsLastMessage(room_id_, description);
                    return;
            }
    
    void
    TimelineModel::setCurrentIndex(int index)
    {
            auto oldIndex = idToIndex(currentId);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            currentId     = indexToId(index);
    
            emit currentIndexChanged(index);
    
    
            if ((oldIndex > index || oldIndex == -1) && !currentId.startsWith("m") &&
    
                ChatPage::instance()->isActiveWindow()) {
    
                    readEvent(currentId.toStdString());
    
    void
    TimelineModel::readEvent(const std::string &id)
    {
            http::client()->read_event(room_id_.toStdString(), id, [this](mtx::http::RequestErr err) {
                    if (err) {
                            nhlog::net()->warn("failed to read_event ({}, {})",
                                               room_id_.toStdString(),
                                               currentId.toStdString());
                    }
            });
    }
    
    
    QString
    TimelineModel::displayName(QString id) const
    {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            return cache::displayName(room_id_, id).toHtmlEscaped();
    
    QString
    TimelineModel::avatarUrl(QString id) const
    {
    
            return cache::avatarUrl(room_id_, id);
    
    QString
    TimelineModel::formatDateSeparator(QDate date) const
    {
            auto now = QDateTime::currentDateTime();
    
            QString fmt = QLocale::system().dateFormat(QLocale::LongFormat);
    
            if (now.date().year() == date.year()) {
                    QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*");
                    fmt = fmt.remove(rx);
            }
    
            return date.toString(fmt);
    }
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    
    QString
    TimelineModel::escapeEmoji(QString str) const
    {
            return utils::replaceEmoji(str);
    }
    
    
    void
    TimelineModel::viewRawMessage(QString id) const
    {
    
            auto e = events.event(id.toStdString(), "", false);
            if (!e)
                    return;
            std::string ev = mtx::accessors::serialize_event(*e).dump(4);
    
            auto dialog    = new dialogs::RawMessage(QString::fromStdString(ev));
            Q_UNUSED(dialog);
    }
    
    TimelineModel::viewDecryptedRawMessage(QString id) const
    {
    
            auto e = events.event(id.toStdString(), "");
            if (!e)
                    return;
    
            std::string ev = mtx::accessors::serialize_event(*e).dump(4);
    
            auto dialog    = new dialogs::RawMessage(QString::fromStdString(ev));
            Q_UNUSED(dialog);
    }
    
    void
    
    TimelineModel::openUserProfile(QString userid) const
    {
            MainWindow::instance()->openUserProfile(userid, room_id_);
    }
    
    
    void
    TimelineModel::replyAction(QString id)
    {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            setReply(id);
            ChatPage::instance()->focusMessageInput();
    }
    
    RelatedInfo
    TimelineModel::relatedInfo(QString id)
    {
    
            auto event = events.event(id.toStdString(), "");
            if (!event)
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    return {};
    
    
            RelatedInfo related   = {};
    
            related.quoted_user   = QString::fromStdString(mtx::accessors::sender(*event));
            related.related_event = mtx::accessors::event_id(*event);
            related.type          = mtx::accessors::msg_type(*event);
    
    
            // get body, strip reply fallback, then transform the event to text, if it is a media event
            // etc
    
            related.quoted_body = QString::fromStdString(mtx::accessors::body(*event));
    
            QRegularExpression plainQuote("^>.*?$\n?", QRegularExpression::MultilineOption);
            while (related.quoted_body.startsWith(">"))
                    related.quoted_body.remove(plainQuote);
            if (related.quoted_body.startsWith("\n"))
                    related.quoted_body.remove(0, 1);
            related.quoted_body = utils::getQuoteBody(related);
    
            // get quoted body and strip reply fallback
    
            related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(*event);
    
            related.quoted_formatted_body.remove(QRegularExpression(
    
              "<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption));
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            related.room = room_id_;
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            return related;
    
    }
    
    void
    TimelineModel::readReceiptsAction(QString id) const
    {
            MainWindow::instance()->openReadReceiptsDialog(id);
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    void
    TimelineModel::redactEvent(QString id)
    {
            if (!id.isEmpty())
                    http::client()->redact_event(
                      room_id_.toStdString(),
                      id.toStdString(),
                      [this, id](const mtx::responses::EventId &, mtx::http::RequestErr err) {
                              if (err) {
                                      emit redactionFailed(
                                        tr("Message redaction failed: %1")
                                          .arg(QString::fromStdString(err->matrix_error.error)));
                                      return;
                              }
    
                              emit eventRedacted(id);
                      });
    }
    
    
    int
    TimelineModel::idToIndex(QString id) const
    {
            if (id.isEmpty())
                    return -1;
    
    
            auto idx = events.idToIndex(id.toStdString());
            if (idx)
    
    Nicolas Werner's avatar
    Nicolas Werner committed
                    return events.size() - *idx - 1;
    
            else
                    return -1;
    
    }
    
    QString
    TimelineModel::indexToId(int index) const
    {
    
    Nicolas Werner's avatar
    Nicolas Werner committed
            auto id = events.indexToId(events.size() - index - 1);
    
            return id ? QString::fromStdString(*id) : "";
    
    Nicolas Werner's avatar
    Nicolas Werner committed
    // Note: this will only be called for our messages
    
    void
    TimelineModel::markEventsAsRead(const std::vector<QString> &event_ids)
    {
            for (const auto &id : event_ids) {
                    read.insert(id);
                    int idx = idToIndex(id);
                    if (idx < 0) {
                            nhlog::ui()->warn("Read index out of range");
                            return;
                    }
                    emit dataChanged(index(idx, 0), index(idx, 0));
            }
    }
    
    TimelineModel::sendEncryptedMessage(const std::string txn_id, nlohmann::json content)
    
    {
            const auto room_id = room_id_.toStdString();
    
            using namespace mtx::events;
            using namespace mtx::identifiers;
    
    
            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::outboundMegolmSessionExists(room_id)) {
    
                            mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event;
                            event.content =
    
                              olm::encrypt_group_message(room_id, http::client()->device_id(), doc);
    
                            event.event_id         = txn_id;
                            event.room_id          = room_id;
                            event.sender           = http::client()->user_id().to_string();
                            event.type             = mtx::events::EventType::RoomEncrypted;
                            event.origin_server_ts = QDateTime::currentMSecsSinceEpoch();
    
                            emit this->addPendingMessageToStore(event);
    
                            return;
                    }
    
                    nhlog::ui()->debug("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::saveOutboundMegolmSession(
    
                      room_id, session_data, std::move(outbound_session));
    
    
                    const auto members = cache::roomMembers(room_id);
    
                    nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id);
    
    
                    auto keeper = std::make_shared<StateKeeper>([room_id, doc, txn_id, this]() {
                            try {
                                    mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> event;
                                    event.content = olm::encrypt_group_message(
                                      room_id, http::client()->device_id(), doc);
    
                                    event.event_id         = txn_id;
                                    event.room_id          = room_id;
                                    event.sender           = http::client()->user_id().to_string();
                                    event.type             = mtx::events::EventType::RoomEncrypted;
                                    event.origin_server_ts = QDateTime::currentMSecsSinceEpoch();
    
    
                                    emit this->addPendingMessageToStore(event);
                            } catch (const lmdb::error &e) {
                                    nhlog::db()->critical("failed to save megolm outbound session: {}",
                                                          e.what());
                                    emit ChatPage::instance()->showNotification(
                                      tr("Failed to encrypt event, sending aborted!"));
                            }
                    });
    
    
                    mtx::requests::QueryKeys req;
                    for (const auto &member : members)
                            req.device_keys[member] = {};
    
                    http::client()->query_keys(
                      req,
    
                      [keeper = std::move(keeper), megolm_payload, txn_id, 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));
    
                                      emit ChatPage::instance()->showNotification(
                                        tr("Failed to encrypt event, sending aborted!"));
    
                                      return;
                              }
    
                              for (const auto &user : res.device_keys) {
                                      // Mapping from a device_id with valid identity keys to the
                                      // generated room_key event used for sharing the megolm session.
                                      std::map<std::string, std::string> room_key_msgs;
                                      std::map<std::string, DevicePublicKeys> deviceKeys;
    
                                      room_key_msgs.clear();
                                      deviceKeys.clear();
    
                                      for (const auto &dev : user.second) {
                                              const auto user_id   = ::UserId(dev.second.user_id);
                                              const auto device_id = DeviceId(dev.second.device_id);
    
                                              const auto device_keys = dev.second.keys;
                                              const auto curveKey    = "curve25519:" + device_id.get();
                                              const auto edKey       = "ed25519:" + device_id.get();
    
                                              if ((device_keys.find(curveKey) == device_keys.end()) ||
                                                  (device_keys.find(edKey) == device_keys.end())) {
                                                      nhlog::net()->debug(
                                                        "ignoring malformed keys for device {}",
                                                        device_id.get());
                                                      continue;
                                              }
    
                                              DevicePublicKeys pks;
                                              pks.ed25519    = device_keys.at(edKey);
                                              pks.curve25519 = device_keys.at(curveKey);
    
                                              try {
                                                      if (!mtx::crypto::verify_identity_signature(
                                                            json(dev.second), device_id, user_id)) {
                                                              nhlog::crypto()->warn(
                                                                "failed to verify identity keys: {}",
                                                                json(dev.second).dump(2));
                                                              continue;
                                                      }
                                              } catch (const json::exception &e) {
                                                      nhlog::crypto()->warn(
                                                        "failed to parse device key json: {}",
                                                        e.what());
                                                      continue;
                                              } catch (const mtx::crypto::olm_exception &e) {
                                                      nhlog::crypto()->warn(
                                                        "failed to verify device key json: {}",
                                                        e.what());
                                                      continue;
                                              }
    
                                              auto room_key = olm::client()
                                                                ->create_room_key_event(
                                                                  user_id, pks.ed25519, megolm_payload)
                                                                .dump();
    
                                              room_key_msgs.emplace(device_id, room_key);
                                              deviceKeys.emplace(device_id, pks);
                                      }
    
                                      std::vector<std::string> valid_devices;
                                      valid_devices.reserve(room_key_msgs.size());
                                      for (auto const &d : room_key_msgs) {
                                              valid_devices.push_back(d.first);
    
                                              nhlog::net()->info("{}", d.first);
                                              nhlog::net()->info("  curve25519 {}",
                                                                 deviceKeys.at(d.first).curve25519);
                                              nhlog::net()->info("  ed25519 {}",
                                                                 deviceKeys.at(d.first).ed25519);
                                      }
    
                                      nhlog::net()->info(
                                        "sending claim request for user {} with {} devices",
                                        user.first,
                                        valid_devices.size());
    
                                      http::client()->claim_keys(
                                        user.first,
                                        valid_devices,
                                        std::bind(&TimelineModel::handleClaimedKeys,
                                                  this,
                                                  keeper,
                                                  room_key_msgs,
                                                  deviceKeys,
                                                  user.first,
                                                  std::placeholders::_1,
                                                  std::placeholders::_2));
    
                                      // TODO: Wait before sending the next batch of requests.
                                      std::this_thread::sleep_for(std::chrono::milliseconds(500));
                              }
                      });
    
                    // TODO: Let the user know about the errors.
            } catch (const lmdb::error &e) {
                    nhlog::db()->critical(
                      "failed to open outbound megolm session ({}): {}", room_id, e.what());
    
                    emit ChatPage::instance()->showNotification(
                      tr("Failed to encrypt event, sending aborted!"));
    
            } catch (const mtx::crypto::olm_exception &e) {
                    nhlog::crypto()->critical(
                      "failed to open outbound megolm session ({}): {}", room_id, e.what());
    
                    emit ChatPage::instance()->showNotification(
                      tr("Failed to encrypt event, sending aborted!"));
    
            }
    }
    
    void
    TimelineModel::handleClaimedKeys(std::shared_ptr<StateKeeper> keeper,
                                     const std::map<std::string, std::string> &room_keys,
                                     const std::map<std::string, DevicePublicKeys> &pks,
                                     const std::string &user_id,
                                     const mtx::responses::ClaimKeys &res,
                                     mtx::http::RequestErr err)
    {
            if (err) {
                    nhlog::net()->warn("claim keys error: {} {} {}",
                                       err->matrix_error.error,
                                       err->parse_error,
                                       static_cast<int>(err->status_code));
                    return;
            }
    
            nhlog::net()->debug("claimed keys for {}", user_id);
    
            if (res.one_time_keys.size() == 0) {
                    nhlog::net()->debug("no one-time keys found for user_id: {}", user_id);
                    return;
            }
    
            if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) {
                    nhlog::net()->debug("no one-time keys found for user_id: {}", user_id);
                    return;
            }
    
            auto retrieved_devices = res.one_time_keys.at(user_id);
    
            // Payload with all the to_device message to be sent.
            json body;
            body["messages"][user_id] = json::object();
    
            for (const auto &rd : retrieved_devices) {
                    const auto device_id = rd.first;
                    nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2));
    
                    // TODO: Verify signatures
                    auto otk = rd.second.begin()->at("key");
    
                    if (pks.find(device_id) == pks.end()) {
                            nhlog::net()->critical("couldn't find public key for device: {}",
                                                   device_id);
                            continue;
                    }
    
                    auto id_key = pks.at(device_id).curve25519;