matrixion/src/timeline/TimelineModel.cpp

1736 lines
72 KiB
C++
Raw Normal View History

2019-08-31 00:20:53 +03:00
#include "TimelineModel.h"
2019-09-09 22:42:33 +03:00
#include <algorithm>
2020-01-25 03:06:01 +03:00
#include <thread>
#include <type_traits>
2020-04-14 21:15:11 +03:00
#include <QCache>
2019-12-03 04:26:41 +03:00
#include <QFileDialog>
#include <QMimeDatabase>
#include <QRegularExpression>
#include <QSettings>
2019-12-03 04:26:41 +03:00
#include <QStandardPaths>
2019-09-11 01:54:40 +03:00
#include "ChatPage.h"
2019-12-27 23:49:55 +03:00
#include "EventAccessors.h"
2019-08-31 23:43:31 +03:00
#include "Logging.h"
#include "MainWindow.h"
#include "MatrixClient.h"
2019-12-03 04:26:41 +03:00
#include "MxcImageProvider.h"
2019-09-08 17:50:32 +03:00
#include "Olm.h"
2019-10-03 19:07:01 +03:00
#include "TimelineViewManager.h"
2019-08-31 00:20:53 +03:00
#include "Utils.h"
2019-09-08 16:26:46 +03:00
#include "dialogs/RawMessage.h"
2019-08-31 00:20:53 +03:00
Q_DECLARE_METATYPE(QModelIndex)
2020-04-14 21:15:11 +03:00
namespace std {
inline uint
qHash(const std::string &key, uint seed = 0)
{
return qHash(QByteArray::fromRawData(key.data(), key.length()), seed);
}
}
2019-08-31 23:43:31 +03:00
namespace {
2019-12-27 23:49:55 +03:00
struct RoomEventType
{
2019-12-27 23:49:55 +03:00
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::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;
2019-12-27 23:49:55 +03:00
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;
2019-12-27 23:49:55 +03:00
case EventType::RoomHistoryVisibility:
return qml_mtx_events::EventType::RoomHistoryVisibility;
2019-12-27 23:49:55 +03:00
case EventType::RoomJoinRules:
return qml_mtx_events::EventType::RoomJoinRules;
2019-12-27 23:49:55 +03:00
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;
2020-01-11 20:53:32 +03:00
default:
return qml_mtx_events::EventType::UnknownMessage;
2019-12-27 23:49:55 +03:00
}
}
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;
2019-10-04 02:10:46 +03:00
}
2019-12-27 23:49:55 +03:00
qml_mtx_events::EventType operator()(const mtx::events::Event<mtx::events::msg::Redacted> &)
{
return qml_mtx_events::EventType::Redacted;
}
2019-12-27 23:49:55 +03:00
// ::EventType::Type operator()(const Event<mtx::events::msg::Location> &e) { return
// ::EventType::LocationMessage; }
};
}
2019-09-09 22:42:33 +03:00
qml_mtx_events::EventType
2019-12-27 23:49:55 +03:00
toRoomEventType(const mtx::events::collections::TimelineEvents &event)
{
2019-12-27 23:49:55 +03:00
return std::visit(RoomEventType{}, event);
2019-08-31 23:43:31 +03:00
}
2020-01-23 22:59:17 +03:00
QString
toRoomEventTypeString(const mtx::events::collections::TimelineEvents &event)
{
return std::visit([](const auto &e) { return QString::fromStdString(to_string(e.type)); },
event);
}
2019-10-03 19:07:01 +03:00
TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent)
: QAbstractListModel(parent)
, room_id_(room_id)
2019-10-03 19:07:01 +03:00
, manager_(manager)
{
connect(
this, &TimelineModel::oldMessagesRetrieved, this, &TimelineModel::addBackwardsEvents);
2019-09-18 23:58:25 +03:00
connect(this, &TimelineModel::messageFailed, this, [this](QString txn_id) {
nhlog::ui()->error("Failed to send {}, retrying", txn_id.toStdString());
QTimer::singleShot(5000, this, [this]() { emit nextPendingMessage(); });
2019-09-18 23:58:25 +03:00
});
connect(this, &TimelineModel::messageSent, this, [this](QString txn_id, QString event_id) {
pending.removeOne(txn_id);
2019-09-18 23:58:25 +03:00
int idx = idToIndex(txn_id);
if (idx < 0) {
// transaction already received via sync
2019-09-18 23:58:25 +03:00
return;
}
eventOrder[idx] = event_id;
auto ev = events.value(txn_id);
2019-12-14 19:08:36 +03:00
ev = std::visit(
2019-09-18 23:58:25 +03:00
[event_id](const auto &e) -> mtx::events::collections::TimelineEvents {
auto eventCopy = e;
eventCopy.event_id = event_id.toStdString();
return eventCopy;
},
ev);
2019-12-13 03:31:14 +03:00
2019-09-18 23:58:25 +03:00
events.remove(txn_id);
events.insert(event_id, ev);
2019-09-19 22:47:16 +03:00
// mark our messages as read
readEvent(event_id.toStdString());
2019-09-19 22:47:16 +03:00
// ask to be notified for read receipts
2019-12-15 04:56:04 +03:00
cache::addPendingReceipt(room_id_, event_id);
2019-09-19 22:47:16 +03:00
2019-09-18 23:58:25 +03:00
emit dataChanged(index(idx, 0), index(idx, 0));
if (pending.size() > 0)
emit nextPendingMessage();
2019-09-18 23:58:25 +03:00
});
2019-09-29 13:29:17 +03:00
connect(this, &TimelineModel::redactionFailed, this, [](const QString &msg) {
emit ChatPage::instance()->showNotification(msg);
});
connect(
this, &TimelineModel::nextPendingMessage, this, &TimelineModel::processOnePendingMessage);
connect(this, &TimelineModel::newMessageToSend, this, &TimelineModel::addPendingMessage);
2020-01-11 20:53:32 +03:00
connect(this,
2020-01-23 22:34:04 +03:00
&TimelineModel::eventFetched,
2020-01-11 20:53:32 +03:00
this,
[this](QString requestingEvent, mtx::events::collections::TimelineEvents event) {
events.insert(QString::fromStdString(mtx::accessors::event_id(event)),
event);
auto idx = idToIndex(requestingEvent);
if (idx >= 0)
emit dataChanged(index(idx, 0), index(idx, 0));
});
}
2019-08-31 00:20:53 +03:00
QHash<int, QByteArray>
TimelineModel::roleNames() const
{
return {
{Section, "section"},
2019-08-31 00:20:53 +03:00
{Type, "type"},
2020-01-23 22:59:17 +03:00
{TypeString, "typeString"},
2019-08-31 00:20:53 +03:00
{Body, "body"},
{FormattedBody, "formattedBody"},
{UserId, "userId"},
{UserName, "userName"},
{Timestamp, "timestamp"},
{Url, "url"},
2019-10-09 01:36:03 +03:00
{ThumbnailUrl, "thumbnailUrl"},
{Blurhash, "blurhash"},
2019-09-29 11:45:35 +03:00
{Filename, "filename"},
2019-10-04 02:10:46 +03:00
{Filesize, "filesize"},
2019-09-29 11:45:35 +03:00
{MimeType, "mimetype"},
{Height, "height"},
{Width, "width"},
{ProportionalHeight, "proportionalHeight"},
2019-09-08 16:26:46 +03:00
{Id, "id"},
2019-09-18 23:58:25 +03:00
{State, "state"},
2019-09-20 00:02:56 +03:00
{IsEncrypted, "isEncrypted"},
{ReplyTo, "replyTo"},
{RoomId, "roomId"},
{RoomName, "roomName"},
{RoomTopic, "roomTopic"},
2020-01-04 01:21:33 +03:00
{Dump, "dump"},
2019-08-31 00:20:53 +03:00
};
}
int
TimelineModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return (int)this->eventOrder.size();
}
QVariantMap
TimelineModel::getDump(QString eventId) const
{
if (events.contains(eventId))
2020-01-11 20:53:32 +03:00
return data(eventId, Dump).toMap();
return {};
}
2019-08-31 00:20:53 +03:00
QVariant
2020-01-11 20:53:32 +03:00
TimelineModel::data(const QString &id, int role) const
2019-08-31 00:20:53 +03:00
{
2019-12-27 23:49:55 +03:00
using namespace mtx::accessors;
2020-01-11 20:53:32 +03:00
namespace acc = mtx::accessors;
2019-09-08 17:50:32 +03:00
mtx::events::collections::TimelineEvents event = events.value(id);
2019-12-14 19:08:36 +03:00
if (auto e =
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) {
2019-09-08 17:50:32 +03:00
event = decryptEvent(*e).event;
}
2019-08-31 00:20:53 +03:00
switch (role) {
case UserId:
2019-12-27 23:49:55 +03:00
return QVariant(QString::fromStdString(acc::sender(event)));
case UserName:
2019-12-27 23:49:55 +03:00
return QVariant(displayName(QString::fromStdString(acc::sender(event))));
case Timestamp:
2019-12-27 23:49:55 +03:00
return QVariant(origin_server_ts(event));
case Type:
2019-12-27 23:49:55 +03:00
return QVariant(toRoomEventType(event));
2020-01-23 22:59:17 +03:00
case TypeString:
return QVariant(toRoomEventTypeString(event));
2019-09-11 01:54:40 +03:00
case Body:
2019-12-27 23:49:55 +03:00
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);
}
return QVariant(utils::replaceEmoji(
utils::linkifyMessage(utils::escapeBlacklistedHtml(formattedBody_))));
}
case Url:
2019-12-27 23:49:55 +03:00
return QVariant(QString::fromStdString(url(event)));
2019-10-09 01:36:03 +03:00
case ThumbnailUrl:
2019-12-27 23:49:55 +03:00
return QVariant(QString::fromStdString(thumbnail_url(event)));
case Blurhash:
return QVariant(QString::fromStdString(blurhash(event)));
2019-09-29 11:45:35 +03:00
case Filename:
2019-12-27 23:49:55 +03:00
return QVariant(QString::fromStdString(filename(event)));
2019-10-04 02:10:46 +03:00
case Filesize:
2019-12-27 23:49:55 +03:00
return QVariant(utils::humanReadableFileSize(filesize(event)));
2019-09-29 11:45:35 +03:00
case MimeType:
2019-12-27 23:49:55 +03:00
return QVariant(QString::fromStdString(mimetype(event)));
case Height:
2019-12-27 23:49:55 +03:00
return QVariant(qulonglong{media_height(event)});
case Width:
2019-12-27 23:49:55 +03:00
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.);
}
2019-09-08 16:26:46 +03:00
case Id:
return id;
2019-09-18 23:58:25 +03:00
case State:
2019-09-19 22:47:16 +03:00
// only show read receipts for messages not from us
2019-12-27 23:49:55 +03:00
if (acc::sender(event) != http::client()->user_id().to_string())
2019-09-19 22:47:16 +03:00
return qml_mtx_events::Empty;
2019-09-18 23:58:25 +03:00
else if (pending.contains(id))
return qml_mtx_events::Sent;
2019-12-15 04:56:04 +03:00
else if (read.contains(id) || cache::readReceipts(id, room_id_).size() > 1)
return qml_mtx_events::Read;
2019-09-18 23:58:25 +03:00
else
return qml_mtx_events::Received;
2019-09-20 00:02:56 +03:00
case IsEncrypted: {
2019-12-14 19:08:36 +03:00
return std::holds_alternative<
2019-12-27 23:49:55 +03:00
mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(events[id]);
}
2019-12-27 23:49:55 +03:00
case ReplyTo:
return QVariant(QString::fromStdString(in_reply_to_event(event)));
case RoomId:
return QVariant(QString::fromStdString(room_id(event)));
case RoomName:
2019-12-27 23:49:55 +03:00
return QVariant(QString::fromStdString(room_name(event)));
case RoomTopic:
2019-12-27 23:49:55 +03:00
return QVariant(QString::fromStdString(room_topic(event)));
2020-01-04 01:21:33 +03:00
case Dump: {
QVariantMap m;
auto names = roleNames();
2020-01-11 20:53:32 +03:00
// m.insert(names[Section], data(id, static_cast<int>(Section)));
m.insert(names[Type], data(id, static_cast<int>(Type)));
2020-01-23 22:59:17 +03:00
m.insert(names[TypeString], data(id, static_cast<int>(TypeString)));
2020-01-11 20:53:32 +03:00
m.insert(names[Body], data(id, static_cast<int>(Body)));
m.insert(names[FormattedBody], data(id, static_cast<int>(FormattedBody)));
m.insert(names[UserId], data(id, static_cast<int>(UserId)));
m.insert(names[UserName], data(id, static_cast<int>(UserName)));
m.insert(names[Timestamp], data(id, static_cast<int>(Timestamp)));
m.insert(names[Url], data(id, static_cast<int>(Url)));
m.insert(names[ThumbnailUrl], data(id, static_cast<int>(ThumbnailUrl)));
m.insert(names[Blurhash], data(id, static_cast<int>(Blurhash)));
2020-01-11 20:53:32 +03:00
m.insert(names[Filename], data(id, static_cast<int>(Filename)));
m.insert(names[Filesize], data(id, static_cast<int>(Filesize)));
m.insert(names[MimeType], data(id, static_cast<int>(MimeType)));
m.insert(names[Height], data(id, static_cast<int>(Height)));
m.insert(names[Width], data(id, static_cast<int>(Width)));
m.insert(names[ProportionalHeight], data(id, static_cast<int>(ProportionalHeight)));
m.insert(names[Id], data(id, static_cast<int>(Id)));
m.insert(names[State], data(id, static_cast<int>(State)));
m.insert(names[IsEncrypted], data(id, static_cast<int>(IsEncrypted)));
m.insert(names[ReplyTo], data(id, static_cast<int>(ReplyTo)));
m.insert(names[RoomName], data(id, static_cast<int>(RoomName)));
m.insert(names[RoomTopic], data(id, static_cast<int>(RoomTopic)));
2020-01-04 01:21:33 +03:00
return QVariant(m);
}
2019-08-31 00:20:53 +03:00
default:
return QVariant();
}
}
2020-01-11 20:53:32 +03:00
QVariant
TimelineModel::data(const QModelIndex &index, int role) const
{
using namespace mtx::accessors;
namespace acc = mtx::accessors;
if (index.row() < 0 && index.row() >= (int)eventOrder.size())
return QVariant();
QString id = eventOrder[index.row()];
mtx::events::collections::TimelineEvents event = events.value(id);
if (role == Section) {
QDateTime date = origin_server_ts(event);
date.setTime(QTime());
std::string userId = acc::sender(event);
for (size_t r = index.row() + 1; r < eventOrder.size(); r++) {
auto tempEv = events.value(eventOrder[r]);
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(id, role);
}
bool
TimelineModel::canFetchMore(const QModelIndex &) const
{
if (eventOrder.empty())
return true;
if (!std::holds_alternative<mtx::events::StateEvent<mtx::events::state::Create>>(
events[eventOrder.back()]))
return true;
else
return false;
}
void
TimelineModel::fetchMore(const QModelIndex &)
{
if (paginationInProgress) {
nhlog::ui()->warn("Already loading older messages");
return;
}
paginationInProgress = true;
mtx::http::MessagesOpts opts;
opts.room_id = room_id_.toStdString();
opts.from = prev_batch_token_.toStdString();
nhlog::ui()->debug("Paginating room {}", opts.room_id);
http::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,
err->parse_error);
paginationInProgress = false;
return;
}
emit oldMessagesRetrieved(std::move(res));
paginationInProgress = false;
});
}
2019-08-31 23:43:31 +03:00
void
2019-09-03 03:14:49 +03:00
TimelineModel::addEvents(const mtx::responses::Timeline &timeline)
2019-08-31 23:43:31 +03:00
{
if (isInitialSync) {
2019-09-03 03:14:49 +03:00
prev_batch_token_ = QString::fromStdString(timeline.prev_batch);
isInitialSync = false;
}
if (timeline.events.empty())
return;
2019-09-09 22:42:33 +03:00
std::vector<QString> ids = internalAddEvents(timeline.events);
if (ids.empty())
return;
2020-01-04 01:21:33 +03:00
beginInsertRows(QModelIndex(), 0, static_cast<int>(ids.size() - 1));
this->eventOrder.insert(this->eventOrder.begin(), ids.rbegin(), ids.rend());
2019-09-09 22:42:33 +03:00
endInsertRows();
2019-10-03 19:07:01 +03:00
updateLastMessage();
}
2019-10-03 19:07:01 +03:00
2019-11-09 05:30:17 +03:00
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;
}
void
TimelineModel::updateLastMessage()
{
// Get the user setting to show decrypted messages in side bar
bool decrypt = QSettings().value("user/decrypt_sidebar", true).toBool();
2020-01-04 01:21:33 +03:00
for (auto it = eventOrder.begin(); it != eventOrder.end(); ++it) {
2019-11-09 05:30:17 +03:00
auto event = events.value(*it);
2019-12-14 19:08:36 +03:00
if (auto e = std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(
2019-11-09 05:30:17 +03:00
&event)) {
if (decrypt) {
event = decryptEvent(*e).event;
}
2019-11-09 05:30:17 +03:00
}
2019-12-14 19:08:36 +03:00
if (!std::visit([](const auto &e) -> bool { return isMessage(e); }, event))
2019-11-09 05:30:17 +03:00
continue;
auto description = utils::getMessageDescription(
event, QString::fromStdString(http::client()->user_id().to_string()), room_id_);
emit manager_->updateRoomsLastMessage(room_id_, description);
return;
}
2019-09-09 22:42:33 +03:00
}
std::vector<QString>
TimelineModel::internalAddEvents(
const std::vector<mtx::events::collections::TimelineEvents> &timeline)
{
2019-08-31 23:43:31 +03:00
std::vector<QString> ids;
2019-12-13 03:31:14 +03:00
for (auto e : timeline) {
2019-12-27 23:49:55 +03:00
QString id = QString::fromStdString(mtx::accessors::event_id(e));
2019-08-31 23:43:31 +03:00
2019-09-18 23:58:25 +03:00
if (this->events.contains(id)) {
this->events.insert(id, e);
int idx = idToIndex(id);
emit dataChanged(index(idx, 0), index(idx, 0));
continue;
2019-09-18 23:58:25 +03:00
}
QString txid = QString::fromStdString(mtx::accessors::transaction_id(e));
if (this->pending.removeOne(txid)) {
this->events.insert(id, e);
this->events.remove(txid);
int idx = idToIndex(txid);
if (idx < 0) {
nhlog::ui()->warn("Received index out of range");
continue;
}
eventOrder[idx] = id;
emit dataChanged(index(idx, 0), index(idx, 0));
continue;
}
2019-09-09 22:42:33 +03:00
if (auto redaction =
2019-12-14 19:08:36 +03:00
std::get_if<mtx::events::RedactionEvent<mtx::events::msg::Redaction>>(&e)) {
2019-09-09 22:42:33 +03:00
QString redacts = QString::fromStdString(redaction->redacts);
auto redacted = std::find(eventOrder.begin(), eventOrder.end(), redacts);
if (redacted != eventOrder.end()) {
2019-12-14 19:08:36 +03:00
auto redactedEvent = std::visit(
2019-09-09 22:42:33 +03:00
[](const auto &ev)
-> mtx::events::RoomEvent<mtx::events::msg::Redacted> {
mtx::events::RoomEvent<mtx::events::msg::Redacted>
replacement = {};
replacement.event_id = ev.event_id;
replacement.room_id = ev.room_id;
replacement.sender = ev.sender;
replacement.origin_server_ts = ev.origin_server_ts;
replacement.type = ev.type;
return replacement;
},
e);
events.insert(redacts, redactedEvent);
int row = (int)std::distance(eventOrder.begin(), redacted);
emit dataChanged(index(row, 0), index(row, 0));
}
continue; // don't insert redaction into timeline
}
if (auto event =
2019-12-14 19:08:36 +03:00
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&e)) {
2020-04-14 20:29:22 +03:00
auto e_ = decryptEvent(*event).event;
auto encInfo = mtx::accessors::file(e_);
2019-12-13 03:31:14 +03:00
2020-04-14 20:29:22 +03:00
if (encInfo)
emit newEncryptedImage(encInfo.value());
}
2019-08-31 23:43:31 +03:00
this->events.insert(id, e);
ids.push_back(id);
2020-01-11 20:53:32 +03:00
auto replyTo = mtx::accessors::in_reply_to_event(e);
auto qReplyTo = QString::fromStdString(replyTo);
if (!replyTo.empty() && !events.contains(qReplyTo)) {
http::client()->get_event(
this->room_id_.toStdString(),
replyTo,
[this, id, replyTo](
const mtx::events::collections::TimelineEvents &timeline,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->error(
"Failed to retrieve event with id {}, which was "
"requested to show the replyTo for event {}",
replyTo,
id.toStdString());
return;
}
2020-01-23 22:34:04 +03:00
emit eventFetched(id, timeline);
2020-01-11 20:53:32 +03:00
});
}
2019-08-31 23:43:31 +03:00
}
2019-09-09 22:42:33 +03:00
return ids;
2019-08-31 23:43:31 +03:00
}
2019-10-03 23:39:56 +03:00
void
TimelineModel::setCurrentIndex(int index)
{
auto oldIndex = idToIndex(currentId);
2019-10-06 15:00:49 +03:00
currentId = indexToId(index);
2019-10-03 23:39:56 +03:00
emit currentIndexChanged(index);
if ((oldIndex > index || oldIndex == -1) && !pending.contains(currentId) &&
ChatPage::instance()->isActiveWindow()) {
readEvent(currentId.toStdString());
2019-10-03 23:39:56 +03:00
}
}
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());
}
});
}
void
TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs)
{
2019-09-09 22:42:33 +03:00
std::vector<QString> ids = internalAddEvents(msgs.chunk);
if (!ids.empty()) {
2020-01-04 01:21:33 +03:00
beginInsertRows(QModelIndex(),
static_cast<int>(this->eventOrder.size()),
static_cast<int>(this->eventOrder.size() + ids.size() - 1));
this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end());
endInsertRows();
}
prev_batch_token_ = QString::fromStdString(msgs.end);
}
QString
TimelineModel::displayName(QString id) const
{
2020-02-02 17:15:00 +03:00
return cache::displayName(room_id_, id).toHtmlEscaped();
}
2019-09-07 23:22:07 +03:00
QString
TimelineModel::avatarUrl(QString id) const
{
2019-12-15 04:56:04 +03:00
return cache::avatarUrl(room_id_, id);
2019-09-07 23:22:07 +03:00
}
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);
}
2019-09-07 03:01:44 +03:00
QString
TimelineModel::escapeEmoji(QString str) const
{
return utils::replaceEmoji(str);
}
2019-09-08 16:26:46 +03:00
void
TimelineModel::viewRawMessage(QString id) const
{
std::string ev = utils::serialize_event(events.value(id)).dump(4);
auto dialog = new dialogs::RawMessage(QString::fromStdString(ev));
Q_UNUSED(dialog);
}
2019-09-08 17:50:32 +03:00
void
TimelineModel::viewDecryptedRawMessage(QString id) const
{
auto event = events.value(id);
if (auto e =
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) {
event = decryptEvent(*e).event;
}
std::string ev = utils::serialize_event(event).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_);
}
2019-09-08 17:50:32 +03:00
DecryptionResult
TimelineModel::decryptEvent(const mtx::events::EncryptedEvent<mtx::events::msg::Encrypted> &e) const
{
2020-04-14 21:15:11 +03:00
static QCache<std::string, DecryptionResult> decryptedEvents{300};
if (auto cachedEvent = decryptedEvents.object(e.event_id))
return *cachedEvent;
2019-09-08 17:50:32 +03:00
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::Notice> dummy;
dummy.origin_server_ts = e.origin_server_ts;
dummy.event_id = e.event_id;
dummy.sender = e.sender;
dummy.content.body =
tr("-- Encrypted Event (No keys found for decryption) --",
"Placeholder, when the message was not decrypted yet or can't be decrypted.")
2019-09-08 17:50:32 +03:00
.toStdString();
try {
2019-12-15 04:56:04 +03:00
if (!cache::inboundMegolmSessionExists(index)) {
2019-09-08 17:50:32 +03:00
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.
2020-04-14 21:15:11 +03:00
decryptedEvents.insert(
dummy.event_id, new DecryptionResult{dummy, false}, 1);
2019-09-08 17:50:32 +03:00
return {dummy, false};
}
} catch (const lmdb::error &e) {
nhlog::db()->critical("failed to check megolm session's existence: {}", e.what());
dummy.content.body = tr("-- Decryption Error (failed to communicate with DB) --",
"Placeholder, when the message can't be decrypted, because "
"the DB access failed when trying to lookup the session.")
.toStdString();
2020-04-14 21:15:11 +03:00
decryptedEvents.insert(dummy.event_id, new DecryptionResult{dummy, false}, 1);
2019-09-08 17:50:32 +03:00
return {dummy, false};
}
std::string msg_str;
try {
2019-12-15 04:56:04 +03:00
auto session = cache::getInboundMegolmSession(index);
2019-09-08 17:50:32 +03:00
auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext);
msg_str = std::string((char *)res.data.data(), res.data.size());
} catch (const lmdb::error &e) {
nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})",
index.room_id,
index.session_id,
index.sender_key,
e.what());
dummy.content.body =
tr("-- Decryption Error (failed to retrieve megolm keys from db) --",
"Placeholder, when the message can't be decrypted, because the DB access "
"failed.")
.toStdString();
2020-04-14 21:15:11 +03:00
decryptedEvents.insert(dummy.event_id, new DecryptionResult{dummy, false}, 1);
2019-09-08 17:50:32 +03:00
return {dummy, false};
} catch (const mtx::crypto::olm_exception &e) {
nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}",
index.room_id,
index.session_id,
index.sender_key,
e.what());
dummy.content.body =
tr("-- Decryption Error (%1) --",
"Placeholder, when the message can't be decrypted. In this case, the Olm "
"decrytion returned an error, which is passed ad %1.")
2019-09-08 17:50:32 +03:00
.arg(e.what())
.toStdString();
2020-04-14 21:15:11 +03:00
decryptedEvents.insert(dummy.event_id, new DecryptionResult{dummy, false}, 1);
2019-09-08 17:50:32 +03:00
return {dummy, false};
}
// 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;
// relations are unencrypted in content...
if (json old_ev = e; old_ev["content"].count("m.relates_to") != 0)
body["content"]["m.relates_to"] = old_ev["content"]["m.relates_to"];
2019-09-08 17:50:32 +03:00
json event_array = json::array();
event_array.push_back(body);
2019-09-08 18:28:40 +03:00
std::vector<mtx::events::collections::TimelineEvents> temp_events;
mtx::responses::utils::parse_timeline_events(event_array, temp_events);
2019-09-08 17:50:32 +03:00
2020-04-14 21:15:11 +03:00
if (temp_events.size() == 1) {
decryptedEvents.insert(e.event_id, new DecryptionResult{temp_events[0], true}, 1);
return {temp_events[0], true};
}
2019-09-08 17:50:32 +03:00
dummy.content.body =
tr("-- Encrypted Event (Unknown event type) --",
"Placeholder, when the message was decrypted, but we couldn't parse it, because "
"Nheko/mtxclient don't support that event type yet.")
2019-09-08 17:50:32 +03:00
.toStdString();
2020-04-14 21:15:11 +03:00
decryptedEvents.insert(dummy.event_id, new DecryptionResult{dummy, false}, 1);
2019-09-08 17:50:32 +03:00
return {dummy, false};
}
2019-09-11 01:54:40 +03:00
void
TimelineModel::replyAction(QString id)
{
2020-04-13 17:22:30 +03:00
setReply(id);
ChatPage::instance()->focusMessageInput();
}
RelatedInfo
TimelineModel::relatedInfo(QString id)
{
if (!events.contains(id))
return {};
2019-11-11 21:58:20 +03:00
auto event = events.value(id);
2019-12-14 19:08:36 +03:00
if (auto e =
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) {
2019-11-11 21:58:20 +03:00
event = decryptEvent(*e).event;
}
2020-01-21 05:36:26 +03:00
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);
related.quoted_body = QString::fromStdString(mtx::accessors::body(event));
related.quoted_body = utils::getQuoteBody(related);
related.quoted_formatted_body = mtx::accessors::formattedBodyWithFallback(event);
related.quoted_formatted_body.remove(QRegularExpression(
"<mx-reply>.*</mx-reply>", QRegularExpression::DotMatchesEverythingOption));
2019-11-03 05:28:16 +03:00
related.room = room_id_;
2019-09-11 01:54:40 +03:00
2020-04-13 17:22:30 +03:00
return related;
}
void
TimelineModel::readReceiptsAction(QString id) const
{
MainWindow::instance()->openReadReceiptsDialog(id);
2019-09-11 01:54:40 +03:00
}
2019-09-18 21:34:30 +03:00
2019-09-29 13:29:17 +03:00
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);
});
}
2019-09-18 21:34:30 +03:00
int
TimelineModel::idToIndex(QString id) const
{
if (id.isEmpty())
return -1;
for (int i = 0; i < (int)eventOrder.size(); i++)
if (id == eventOrder[i])
return i;
return -1;
}
QString
TimelineModel::indexToId(int index) const
{
if (index < 0 || index >= (int)eventOrder.size())
return "";
return eventOrder[index];
}
2019-09-19 22:47:16 +03:00
// 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));
}
}
2019-09-19 23:44:25 +03:00
void
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}};
2019-09-19 23:44:25 +03:00
try {
// Check if we have already an outbound megolm session then we can use.
2019-12-15 04:56:04 +03:00
if (cache::outboundMegolmSessionExists(room_id)) {
auto data =
olm::encrypt_group_message(room_id, http::client()->device_id(), doc);
2019-09-19 23:44:25 +03:00
http::client()->send_room_message<msg::Encrypted, EventType::RoomEncrypted>(
room_id,
txn_id,
data,
[this, 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(QString::fromStdString(txn_id));
}
emit messageSent(
QString::fromStdString(txn_id),
QString::fromStdString(res.event_id.to_string()));
});
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
2019-12-15 04:56:04 +03:00
cache::saveOutboundMegolmSession(
2019-09-19 23:44:25 +03:00
room_id, session_data, std::move(outbound_session));
2019-12-15 04:56:04 +03:00
const auto members = cache::roomMembers(room_id);
2019-09-19 23:44:25 +03:00
nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id);
auto keeper =
std::make_shared<StateKeeper>([megolm_payload, room_id, doc, txn_id, this]() {
try {
auto data = olm::encrypt_group_message(
room_id, http::client()->device_id(), doc);
2019-09-19 23:44:25 +03:00
http::client()
->send_room_message<msg::Encrypted, EventType::RoomEncrypted>(
room_id,
txn_id,
data,
[this, 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(
QString::fromStdString(txn_id));
}
emit messageSent(
QString::fromStdString(txn_id),
QString::fromStdString(res.event_id.to_string()));
});
} catch (const lmdb::error &e) {
nhlog::db()->critical(
"failed to save megolm outbound session: {}", e.what());
emit messageFailed(QString::fromStdString(txn_id));
2019-09-19 23:44:25 +03:00
}
});
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](
2019-09-19 23:44:25 +03:00
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.
emit messageFailed(QString::fromStdString(txn_id));
2019-09-19 23:44:25 +03:00
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 messageFailed(QString::fromStdString(txn_id));
2019-09-19 23:44:25 +03:00
} catch (const mtx::crypto::olm_exception &e) {
nhlog::crypto()->critical(
"failed to open outbound megolm session ({}): {}", room_id, e.what());
emit messageFailed(QString::fromStdString(txn_id));
2019-09-19 23:44:25 +03:00
}
}
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;
auto s = olm::client()->create_outbound_session(id_key, otk);
if (room_keys.find(device_id) == room_keys.end()) {
nhlog::net()->critical("couldn't find m.room_key for device: {}",
device_id);
continue;
}
auto device_msg = olm::client()->create_olm_encrypted_content(
s.get(), room_keys.at(device_id), pks.at(device_id).curve25519);
try {
2019-12-15 04:56:04 +03:00
cache::saveOlmSession(id_key, std::move(s));
2019-09-19 23:44:25 +03:00
} 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());
}
body["messages"][user_id][device_id] = device_msg;
}
nhlog::net()->info("send_to_device: {}", user_id);
http::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);
}
(void)keeper;
});
}
struct SendMessageVisitor
{
SendMessageVisitor(const QString &txn_id, TimelineModel *model)
: txn_id_qstr_(txn_id)
, model_(model)
{}
template<typename T>
void operator()(const mtx::events::Event<T> &)
{}
template<typename T,
std::enable_if_t<std::is_same<decltype(T::msgtype), std::string>::value, int> = 0>
void operator()(const mtx::events::RoomEvent<T> &msg)
{
2019-12-15 04:56:04 +03:00
if (cache::isRoomEncrypted(model_->room_id_.toStdString())) {
auto encInfo = mtx::accessors::file(msg);
if (encInfo)
emit model_->newEncryptedImage(encInfo.value());
model_->sendEncryptedMessage(txn_id_qstr_.toStdString(),
nlohmann::json(msg.content));
} else {
QString txn_id_qstr = txn_id_qstr_;
TimelineModel *model = model_;
http::client()->send_room_message<T, mtx::events::EventType::RoomMessage>(
model->room_id_.toStdString(),
txn_id_qstr.toStdString(),
msg.content,
[txn_id_qstr, model](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_qstr.toStdString(),
err->matrix_error.error,
status_code);
emit model->messageFailed(txn_id_qstr);
}
emit model->messageSent(
txn_id_qstr, QString::fromStdString(res.event_id.to_string()));
});
}
}
QString txn_id_qstr_;
TimelineModel *model_;
};
void
TimelineModel::processOnePendingMessage()
{
if (pending.isEmpty())
return;
QString txn_id_qstr = pending.first();
auto event = events.value(txn_id_qstr);
2019-12-14 19:08:36 +03:00
std::visit(SendMessageVisitor{txn_id_qstr, this}, event);
}
void
TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event)
{
std::visit(
[](auto &msg) {
msg.type = mtx::events::EventType::RoomMessage;
msg.event_id = http::client()->generate_txn_id();
msg.sender = http::client()->user_id().to_string();
msg.origin_server_ts = QDateTime::currentMSecsSinceEpoch();
},
event);
internalAddEvents({event});
2019-12-27 23:49:55 +03:00
QString txn_id_qstr = QString::fromStdString(mtx::accessors::event_id(event));
2020-01-04 01:21:33 +03:00
beginInsertRows(QModelIndex(), 0, 0);
pending.push_back(txn_id_qstr);
2020-01-04 01:21:33 +03:00
this->eventOrder.insert(this->eventOrder.begin(), txn_id_qstr);
endInsertRows();
updateLastMessage();
emit nextPendingMessage();
}
2019-12-03 04:26:41 +03:00
bool
2019-12-03 04:26:41 +03:00
TimelineModel::saveMedia(QString eventId) const
{
mtx::events::collections::TimelineEvents event = events.value(eventId);
2019-12-14 19:08:36 +03:00
if (auto e =
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) {
2019-12-03 04:26:41 +03:00
event = decryptEvent(*e).event;
}
2019-12-27 23:49:55 +03:00
QString mxcUrl = QString::fromStdString(mtx::accessors::url(event));
QString originalFilename = QString::fromStdString(mtx::accessors::filename(event));
QString mimeType = QString::fromStdString(mtx::accessors::mimetype(event));
2019-12-03 04:26:41 +03:00
2019-12-27 23:49:55 +03:00
auto encryptionInfo = mtx::accessors::file(event);
2019-12-03 04:26:41 +03:00
2019-12-27 23:49:55 +03:00
qml_mtx_events::EventType eventType = toRoomEventType(event);
2019-12-03 04:26:41 +03:00
QString dialogTitle;
if (eventType == qml_mtx_events::EventType::ImageMessage) {
dialogTitle = tr("Save image");
} else if (eventType == qml_mtx_events::EventType::VideoMessage) {
dialogTitle = tr("Save video");
} else if (eventType == qml_mtx_events::EventType::AudioMessage) {
dialogTitle = tr("Save audio");
} else {
dialogTitle = tr("Save file");
}
const QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString();
const QString downloadsFolder =
QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
const QString openLocation = downloadsFolder + "/" + originalFilename;
2019-12-03 04:26:41 +03:00
const QString filename = QFileDialog::getSaveFileName(
manager_->getWidget(), dialogTitle, openLocation, filterString);
2019-12-03 04:26:41 +03:00
if (filename.isEmpty())
return false;
2019-12-03 04:26:41 +03:00
const auto url = mxcUrl.toStdString();
http::client()->download(
url,
[filename, url, encryptionInfo](const std::string &data,
const std::string &,
const std::string &,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to retrieve image {}: {} {}",
url,
err->matrix_error.error,
static_cast<int>(err->status_code));
return;
}
try {
auto temp = data;
if (encryptionInfo)
temp = mtx::crypto::to_string(
mtx::crypto::decrypt_file(temp, encryptionInfo.value()));
QFile file(filename);
if (!file.open(QIODevice::WriteOnly))
return;
file.write(QByteArray(temp.data(), (int)temp.size()));
file.close();
return;
2019-12-03 04:26:41 +03:00
} catch (const std::exception &e) {
nhlog::ui()->warn("Error while saving file to: {}", e.what());
}
});
return true;
2019-12-03 04:26:41 +03:00
}
void
TimelineModel::cacheMedia(QString eventId)
{
mtx::events::collections::TimelineEvents event = events.value(eventId);
2019-12-14 19:08:36 +03:00
if (auto e =
std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&event)) {
2019-12-03 04:26:41 +03:00
event = decryptEvent(*e).event;
}
2019-12-27 23:49:55 +03:00
QString mxcUrl = QString::fromStdString(mtx::accessors::url(event));
QString originalFilename = QString::fromStdString(mtx::accessors::filename(event));
QString mimeType = QString::fromStdString(mtx::accessors::mimetype(event));
2019-12-03 04:26:41 +03:00
2019-12-27 23:49:55 +03:00
auto encryptionInfo = mtx::accessors::file(event);
2019-12-03 04:26:41 +03:00
// If the message is a link to a non mxcUrl, don't download it
if (!mxcUrl.startsWith("mxc://")) {
emit mediaCached(mxcUrl, mxcUrl);
return;
}
QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix();
const auto url = mxcUrl.toStdString();
QFileInfo filename(QString("%1/media_cache/%2.%3")
.arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation))
.arg(QString(mxcUrl).remove("mxc://"))
.arg(suffix));
if (QDir::cleanPath(filename.path()) != filename.path()) {
nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url);
return;
}
QDir().mkpath(filename.path());
if (filename.isReadable()) {
emit mediaCached(mxcUrl, filename.filePath());
return;
}
http::client()->download(
url,
[this, mxcUrl, filename, url, encryptionInfo](const std::string &data,
const std::string &,
const std::string &,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn("failed to retrieve image {}: {} {}",
url,
err->matrix_error.error,
static_cast<int>(err->status_code));
return;
}
try {
auto temp = data;
if (encryptionInfo)
temp = mtx::crypto::to_string(
mtx::crypto::decrypt_file(temp, encryptionInfo.value()));
QFile file(filename.filePath());
if (!file.open(QIODevice::WriteOnly))
return;
file.write(QByteArray(temp.data(), temp.size()));
file.close();
} catch (const std::exception &e) {
nhlog::ui()->warn("Error while saving file to: {}", e.what());
}
emit mediaCached(mxcUrl, filename.filePath());
});
}
2020-01-17 03:25:14 +03:00
QString
TimelineModel::formatTypingUsers(const std::vector<QString> &users, QColor bg)
{
QString temp =
tr("%1 and %2 are typing.",
2020-01-17 03:25:14 +03:00
"Multiple users are typing. First argument is a comma separated list of potentially "
"multiple users. Second argument is the last user of that list. (If only one user is "
"typing, %1 is empty. You should still use it in your string though to silence Qt "
"warnings.)",
users.size());
if (users.empty()) {
return "";
}
QStringList uidWithoutLast;
auto formatUser = [this, bg](const QString &user_id) -> QString {
auto uncoloredUsername = escapeEmoji(displayName(user_id));
QString prefix =
QString("<font color=\"%1\">").arg(manager_->userColor(user_id, bg).name());
// color only parts that don't have a font already specified
QString coloredUsername;
int index = 0;
do {
auto startIndex = uncoloredUsername.indexOf("<font", index);
if (startIndex - index != 0)
coloredUsername +=
prefix +
uncoloredUsername.midRef(
index, startIndex > 0 ? startIndex - index : -1) +
"</font>";
auto endIndex = uncoloredUsername.indexOf("</font>", startIndex);
if (endIndex > 0)
endIndex += sizeof("</font>") - 1;
if (endIndex - startIndex != 0)
coloredUsername +=
uncoloredUsername.midRef(startIndex, endIndex - startIndex);
index = endIndex;
} while (index > 0 && index < uncoloredUsername.size());
return coloredUsername;
2020-01-17 03:25:14 +03:00
};
for (size_t i = 0; i + 1 < users.size(); i++) {
uidWithoutLast.append(formatUser(users[i]));
}
return temp.arg(uidWithoutLast.join(", ")).arg(formatUser(users.back()));
}
2020-01-23 22:34:04 +03:00
QString
TimelineModel::formatJoinRuleEvent(QString id)
{
if (!events.contains(id))
return "";
auto event =
std::get_if<mtx::events::StateEvent<mtx::events::state::JoinRules>>(&events[id]);
if (!event)
return "";
QString user = QString::fromStdString(event->sender);
QString name = escapeEmoji(displayName(user));
switch (event->content.join_rule) {
case mtx::events::state::JoinRule::Public:
return tr("%1 opened the room to the public.").arg(name);
case mtx::events::state::JoinRule::Invite:
return tr("%1 made this room require and invitation to join.").arg(name);
default:
// Currently, knock and private are reserved keywords and not implemented in Matrix.
return "";
}
}
QString
TimelineModel::formatGuestAccessEvent(QString id)
{
if (!events.contains(id))
return "";
auto event =
std::get_if<mtx::events::StateEvent<mtx::events::state::GuestAccess>>(&events[id]);
if (!event)
return "";
QString user = QString::fromStdString(event->sender);
QString name = escapeEmoji(displayName(user));
switch (event->content.guest_access) {
case mtx::events::state::AccessState::CanJoin:
return tr("%1 made the room open to guests.").arg(name);
case mtx::events::state::AccessState::Forbidden:
return tr("%1 has closed the room to guest access.").arg(name);
default:
return "";
}
}
QString
TimelineModel::formatHistoryVisibilityEvent(QString id)
{
if (!events.contains(id))
return "";
auto event =
std::get_if<mtx::events::StateEvent<mtx::events::state::HistoryVisibility>>(&events[id]);
if (!event)
return "";
QString user = QString::fromStdString(event->sender);
QString name = escapeEmoji(displayName(user));
switch (event->content.history_visibility) {
case mtx::events::state::Visibility::WorldReadable:
return tr("%1 made the room history world readable. Events may be now read by "
"non-joined people.")
.arg(name);
case mtx::events::state::Visibility::Shared:
return tr("%1 set the room history visible to members from this point on.")
.arg(name);
case mtx::events::state::Visibility::Invited:
return tr("%1 set the room history visible to members since they were invited.")
.arg(name);
case mtx::events::state::Visibility::Joined:
return tr("%1 set the room history visible to members since they joined the room.")
.arg(name);
default:
return "";
}
}
QString
TimelineModel::formatPowerLevelEvent(QString id)
{
if (!events.contains(id))
return "";
auto event =
std::get_if<mtx::events::StateEvent<mtx::events::state::PowerLevels>>(&events[id]);
if (!event)
return "";
QString user = QString::fromStdString(event->sender);
QString name = escapeEmoji(displayName(user));
// TODO: power levels rendering is actually a bit complex. work on this later.
return tr("%1 has changed the room's permissions.").arg(name);
}
2020-01-23 22:34:04 +03:00
QString
TimelineModel::formatMemberEvent(QString id)
{
if (!events.contains(id))
return "";
auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(&events[id]);
if (!event)
return "";
mtx::events::StateEvent<mtx::events::state::Member> *prevEvent = nullptr;
QString prevEventId = QString::fromStdString(event->unsigned_data.replaces_state);
if (!prevEventId.isEmpty()) {
if (!events.contains(prevEventId)) {
http::client()->get_event(
this->room_id_.toStdString(),
event->unsigned_data.replaces_state,
[this, id, prevEventId](
const mtx::events::collections::TimelineEvents &timeline,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->error(
"Failed to retrieve event with id {}, which was "
"requested to show the membership for event {}",
prevEventId.toStdString(),
id.toStdString());
return;
}
emit eventFetched(id, timeline);
});
} else {
prevEvent =
std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(
&events[prevEventId]);
}
}
QString user = QString::fromStdString(event->state_key);
QString name = escapeEmoji(displayName(user));
QString rendered;
2020-01-23 22:34:04 +03:00
// see table https://matrix.org/docs/spec/client_server/latest#m-room-member
using namespace mtx::events::state;
switch (event->content.membership) {
case Membership::Invite:
rendered = tr("%1 was invited.").arg(name);
break;
2020-01-23 22:34:04 +03:00
case Membership::Join:
if (prevEvent && prevEvent->content.membership == Membership::Join) {
bool displayNameChanged =
prevEvent->content.display_name != event->content.display_name;
bool avatarChanged =
prevEvent->content.avatar_url != event->content.avatar_url;
if (displayNameChanged && avatarChanged)
rendered =
tr("%1 changed their display name and avatar.").arg(name);
2020-01-23 22:34:04 +03:00
else if (displayNameChanged)
rendered = tr("%1 changed their display name.").arg(name);
2020-01-23 22:34:04 +03:00
else if (avatarChanged)
rendered = tr("%1 changed their avatar.").arg(name);
2020-01-23 22:34:04 +03:00
// the case of nothing changed but join follows join shouldn't happen, so
// just show it as join
} else {
rendered = tr("%1 joined.").arg(name);
2020-01-23 22:34:04 +03:00
}
break;
2020-01-23 22:34:04 +03:00
case Membership::Leave:
if (!prevEvent) // Should only ever happen temporarily
return "";
if (prevEvent->content.membership == Membership::Invite) {
if (event->state_key == event->sender)
rendered = tr("%1 rejected their invite.").arg(name);
2020-01-23 22:34:04 +03:00
else
rendered = tr("Revoked the invite to %1.").arg(name);
2020-01-23 22:34:04 +03:00
} else if (prevEvent->content.membership == Membership::Join) {
if (event->state_key == event->sender)
rendered = tr("%1 left the room.").arg(name);
2020-01-23 22:34:04 +03:00
else
rendered = tr("Kicked %1.").arg(name);
2020-01-23 22:34:04 +03:00
} else if (prevEvent->content.membership == Membership::Ban) {
rendered = tr("Unbanned %1.").arg(name);
2020-01-23 22:34:04 +03:00
} else if (prevEvent->content.membership == Membership::Knock) {
if (event->state_key == event->sender)
rendered = tr("%1 redacted their knock.").arg(name);
2020-01-23 22:34:04 +03:00
else
rendered = tr("Rejected the knock from %1.").arg(name);
2020-01-23 22:34:04 +03:00
} else
return tr("%1 left after having already left!",
"This is a leave event after the user already left and shouldn't "
2020-01-23 22:34:04 +03:00
"happen apart from state resets")
.arg(name);
break;
2020-01-23 22:34:04 +03:00
case Membership::Ban:
rendered = tr("%1 was banned.").arg(name);
break;
2020-01-23 22:34:04 +03:00
case Membership::Knock:
rendered = tr("%1 knocked.").arg(name);
break;
}
if (event->content.reason != "") {
rendered += tr(" Reason: %1").arg(QString::fromStdString(event->content.reason));
2020-01-23 22:34:04 +03:00
}
return rendered;
2020-01-23 22:34:04 +03:00
}