-
Sébastien Blin authored
Change-Id: Iee6512a8379a8414a84f843713732a35e0c375fc
Sébastien Blin authoredChange-Id: Iee6512a8379a8414a84f843713732a35e0c375fc
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
conversationmodel.cpp 155.52 KiB
/****************************************************************************
* Copyright (C) 2017-2021 Savoir-faire Linux Inc. *
* Author: Nicolas Jäger <nicolas.jager@savoirfairelinux.com> *
* Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> *
* Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com> *
* Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com> *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Lesser General Public *
* License as published by the Free Software Foundation; either *
* version 2.1 of the License, or (at your option) any later version. *
* *
* This library is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
***************************************************************************/
#include "api/conversationmodel.h"
// LRC
#include "api/lrc.h"
#include "api/behaviorcontroller.h"
#include "api/contactmodel.h"
#include "api/newcallmodel.h"
#include "api/newaccountmodel.h"
#include "api/account.h"
#include "api/call.h"
#include "api/datatransfer.h"
#include "api/datatransfermodel.h"
#include "callbackshandler.h"
#include "containerview.h"
#include "authority/storagehelper.h"
#include "uri.h"
// Dbus
#include "dbus/configurationmanager.h"
#include "dbus/callmanager.h"
// daemon
#include <account_const.h>
#include <datatransfer_interface.h>
// Qt
#include <QtCore/QTimer>
#include <QFileInfo>
// std
#include <algorithm>
#include <mutex>
#include <regex>
#include <fstream>
#include <sstream>
namespace lrc {
using namespace authority;
using namespace api;
class ConversationModelPimpl : public QObject
{
Q_OBJECT
public:
ConversationModelPimpl(const ConversationModel& linked,
Lrc& lrc,
Database& db,
const CallbacksHandler& callbacksHandler,
const BehaviorController& behaviorController);
~ConversationModelPimpl();
using FilterPredicate = std::function<bool(const conversation::Info& convInfo)>;
/**
* return a conversation index from conversations or -1 if no index is found.
* @param uid of the contact to search.
* @return an int.
*/
int indexOf(const QString& uid) const;
/**
* return a reference to a conversation with given filter
* @param pred a unary comparison predicate with which to find the conversation
* @param searchResultIncluded if need to search in contacts and userSearch
* @return a reference to a conversation with given uid
*/
std::reference_wrapper<conversation::Info> getConversation(
const FilterPredicate& pred, const bool searchResultIncluded = false) const;
/**
* return a reference to a conversation with given uid.
* @param conversation uid.
* @param searchResultIncluded if need to search in contacts and userSearch.
* @return a reference to a conversation with the given uid.
*/
std::reference_wrapper<conversation::Info> getConversationForUid(
const QString& uid, const bool searchResultIncluded = false) const;
/**
* return a reference to a conversation with participant.
* @param participant uri.
* @param searchResultIncluded if need to search in contacts and userSearch.
* @return a reference to a conversation with the given peer uri.
* @warning we could have multiple swarm conversations for the same peer. This function will
* return an active one-to-one conversation.
*/
std::reference_wrapper<conversation::Info> getConversationForPeerUri(
const QString& uri, const bool searchResultIncluded = false) const;
/**
* return a conversation index from conversations or -1 if no index is found.
* @param uri of the contact to search.
* @return an int.
*/
int indexOfContact(const QString& uri) const;
/**
* Initialize conversations_ and filteredConversations_
*/
void initConversations();
/**
* Filter all conversations
*/
bool filter(const conversation::Info& conv);
/**
* Sort conversation by last action
*/
bool sort(const conversation::Info& convA, const conversation::Info& convB);
/**
* Call contactModel.addContact if necessary
* @param contactUri
*/
void sendContactRequest(const QString& contactUri);
/**
* Add a conversation with contactUri
* @param convId
* @param contactUri
*/
void addConversationWith(const QString& convId, const QString& contactUri);
/**
* Add a swarm conversation to conversation list
* @param convId
*/
void addSwarmConversation(const QString& convId);
/**
* Add call interaction for conversation with callId
* @param callId
* @param duration
*/
void addOrUpdateCallMessage(const QString& callId,
const QString& from,
bool incoming,
const std::time_t& duration = -1);
/**
* Add a new message from a peer in the database
* @param peerId the author id
* @param body the content of the message
* @param timestamp the timestamp of the message
* @param daemonId the daemon id
* @return msgId generated (in db)
*/
QString addIncomingMessage(const QString& peerId,
const QString& body,
const uint64_t& timestamp = 0,
const QString& daemonId = "");
/**
* Change the status of an interaction. Listen from callbacksHandler
* @param accountId, account linked
* @param messageId, interaction to update
* @param conversationId, conversation
* @param peerId, peer id
* @param status, new status for this interaction
*/
void slotUpdateInteractionStatus(const QString& accountId,
const QString& conversationId,
const QString& peerId,
const QString& messageId,
int status);
/**
* place a call
* @param uid, conversation id
* @param isAudioOnly, allow to specify if the call is only audio. Set to false by default.
*/
void placeCall(const QString& uid, bool isAudioOnly = false);
/**
* get number of unread messages
*/
int getNumberOfUnreadMessagesFor(const QString& uid);
/**
* Handle data transfer progression
*/
void updateTransfer(QTimer* timer,
const QString& conversation,
int conversationIdx,
const QString& interactionId);
bool usefulDataFromDataTransfer(const QString& fileId,
const datatransfer::Info& info,
QString& interactionId,
QString& conversationId);
void awaitingHost(const QString& fileId, datatransfer::Info info);
bool hasOneOneSwarmWith(const QString& participant);
/**
* accept a file transfer
* @param convUid
* @param interactionId
* @param final name of the file
*/
void acceptTransfer(const QString& convUid, const QString& interactionId, const QString& path);
void handleIncomingFile(const QString& convId,
const QString& interactionId,
const QString& displayName,
int totalSize);
void addConversationRequest(const MapStringString& convRequest);
void addContactRequest(const QString& contactUri);
// filter out ourself from conversation participants.
const VectorString peersForConversation(const conversation::Info& conversation) const;
// insert swarm interactions. Return false if interaction already exists.
bool insertSwarmInteraction(const QString& interactionId,
const interaction::Info& interaction,
conversation::Info& conversation,
bool insertAtBegin);
void invalidateModel();
void emplaceBackConversation(conversation::Info&& conversation);
void eraseConversation(const QString& convId);
void eraseConversation(int index);
const ConversationModel& linked;
Lrc& lrc;
Database& db;
const CallbacksHandler& callbacksHandler;
const BehaviorController& behaviorController;
ConversationModel::ConversationQueue conversations; ///< non-filtered conversations
ConversationModel::ConversationQueue searchResults;
ConversationModel::ConversationQueueProxy filteredConversations;
ConversationModel::ConversationQueueProxy customFilteredConversations;
QString currentFilter;
FilterType typeFilter;
FilterType customTypeFilter;
std::map<QString, std::mutex> interactionsLocks; ///< {convId, mutex}
MapStringString transfIdToDbIntId;
public Q_SLOTS:
/**
* Listen from contactModel when updated (like new alias, avatar, etc.)
*/
void slotContactModelUpdated(const QString& uri);
/**
* Listen from contactModel when a new contact is added
* @param uri
*/
void slotContactAdded(const QString& contactUri);
/**
* Listen from contactModel when receive a new contact request
* @param uri
*/
void slotIncomingContactRequest(const QString& contactUri);
/**
* Listen from contactModel when a pending contact is accepted
* @param uri
*/
void slotPendingContactAccepted(const QString& uri);
/**
* Listen from contactModel when aa new contact is removed
* @param uri
*/
void slotContactRemoved(const QString& uri);
/**
* Listen from callmodel for new calls.
* @param fromId caller uri
* @param callId
*/
void slotIncomingCall(const QString& fromId, const QString& callId);
/**
* Listen from callmodel for calls status changed.
* @param callId
*/
void slotCallStatusChanged(const QString& callId, int code);
/**
* Listen from callmodel for writing "Call started"
* @param callId
*/
void slotCallStarted(const QString& callId);
/**
* Listen from callmodel for writing "Call ended"
* @param callId
*/
void slotCallEnded(const QString& callId);
/**
* Listen from CallbacksHandler for new incoming interactions;
* @param accountId
* @param msgId
* @param peerId
* @param payloads body
*/
void slotNewAccountMessage(const QString& accountId,
const QString& peerId,
const QString& msgId,
const MapStringString& payloads);
/**
* Listen from CallbacksHandler for new messages in a SIP call
* @param callId call linked to the interaction
* @param from author uri
* @param body of the message
*/
void slotIncomingCallMessage(const QString& callId, const QString& from, const QString& body);
/**
* Listen from CallModel when a call is added to a conference
* @param callId
* @param confId
*/
void slotCallAddedToConference(const QString& callId, const QString& confId);
/**
* Listen from CallbacksHandler when a conference is deleted.
* @param confId
*/
void slotConferenceRemoved(const QString& confId);
/**
* Listen for when a contact is composing
* @param accountId
* @param contactUri
* @param isComposing
*/
void slotComposingStatusChanged(const QString& accountId,
const QString& convId,
const QString& contactUri,
bool isComposing);
void slotTransferStatusCreated(const QString& fileId, api::datatransfer::Info info);
void slotTransferStatusCanceled(const QString& fileId, api::datatransfer::Info info);
void slotTransferStatusAwaitingPeer(const QString& fileId, api::datatransfer::Info info);
void slotTransferStatusAwaitingHost(const QString& fileId, api::datatransfer::Info info);
void slotTransferStatusOngoing(const QString& fileId, api::datatransfer::Info info);
void slotTransferStatusFinished(const QString& fileId, api::datatransfer::Info info);
void slotTransferStatusError(const QString& fileId, api::datatransfer::Info info);
void slotTransferStatusTimeoutExpired(const QString& fileId, api::datatransfer::Info info);
void slotTransferStatusUnjoinable(const QString& fileId, api::datatransfer::Info info);
bool updateTransferStatus(const QString& fileId,
datatransfer::Info info,
interaction::Status newStatus,
bool& updated);
void slotConversationLoaded(uint32_t requestId,
const QString& accountId,
const QString& conversationId,
const VectorMapStringString& messages);
void slotMessageReceived(const QString& accountId,
const QString& conversationId,
const MapStringString& message);
void slotConversationRequestReceived(const QString& accountId,
const QString& conversationId,
const MapStringString& metadatas);
void slotConversationRequestDeclined(const QString& accountId, const QString& conversationId);
void slotConversationMemberEvent(const QString& accountId,
const QString& conversationId,
const QString& memberUri,
int event);
void slotConversationReady(const QString& accountId, const QString& conversationId);
void slotConversationRemoved(const QString& accountId, const QString& conversationId);
};
ConversationModel::ConversationModel(const account::Info& owner,
Lrc& lrc,
Database& db,
const CallbacksHandler& callbacksHandler,
const BehaviorController& behaviorController)
: QObject(nullptr)
, pimpl_(std::make_unique<ConversationModelPimpl>(*this,
lrc,
db,
callbacksHandler,
behaviorController))
, owner(owner)
{}
ConversationModel::~ConversationModel() {}
const ConversationModel::ConversationQueue&
ConversationModel::getConversations() const
{
return pimpl_->conversations;
}
const ConversationModel::ConversationQueueProxy&
ConversationModel::allFilteredConversations() const
{
if (!pimpl_->filteredConversations.isDirty())
return pimpl_->filteredConversations;
return pimpl_->filteredConversations.filter().sort().validate();
}
QMap<ConferenceableItem, ConferenceableValue>
ConversationModel::getConferenceableConversations(const QString& convId, const QString& filter) const
{
auto conversationIdx = pimpl_->indexOf(convId);
if (conversationIdx == -1 || !owner.enabled) {
return {};
}
QMap<ConferenceableItem, ConferenceableValue> result;
ConferenceableValue callsVector, contactsVector;
auto currentConfId = pimpl_->conversations.at(conversationIdx).confId;
auto currentCallId = pimpl_->conversations.at(conversationIdx).callId;
auto calls = pimpl_->lrc.getCalls();
auto conferences = pimpl_->lrc.getConferences();
auto& conversations = pimpl_->conversations;
auto currentAccountID = pimpl_->linked.owner.id;
// add contacts for current account
for (const auto& conv : conversations) {
// conversations with calls will be added in call section
// we want to add only contacts non-swarm or one-to-one conversation
auto& peers = pimpl_->peersForConversation(conv);
if (!conv.callId.isEmpty() || !conv.confId.isEmpty() || !conv.isCoreDialog()
|| peers.empty()) {
continue;
}
try {
auto contact = owner.contactModel->getContact(peers.front());
if (contact.isBanned || contact.profileInfo.type == profile::Type::PENDING) {
continue;
}
QVector<AccountConversation> cv;
AccountConversation accConv = {conv.uid, currentAccountID};
cv.push_back(accConv);
if (filter.isEmpty()) {
contactsVector.push_back(cv);
continue;
}
bool result = contact.profileInfo.alias.contains(filter, Qt::CaseInsensitive)
|| contact.profileInfo.uri.contains(filter, Qt::CaseInsensitive)
|| contact.registeredName.contains(filter, Qt::CaseInsensitive);
if (result) {
contactsVector.push_back(cv);
}
} catch (const std::out_of_range& e) {
qDebug() << e.what();
continue;
}
}
if (calls.empty() && conferences.empty()) {
result.insert(ConferenceableItem::CONTACT, contactsVector);
return result;
}
// filter out calls from conference
for (const auto& c : conferences) {
for (const auto& subcal : pimpl_->lrc.getConferenceSubcalls(c)) {
auto position = std::find(calls.begin(), calls.end(), subcal);
if (position != calls.end()) {
calls.erase(position);
}
}
}
// found conversations and account for calls and conferences
QMap<QString, QVector<AccountConversation>> tempConferences;
for (const auto& account_id : pimpl_->lrc.getAccountModel().getAccountList()) {
try {
auto& accountInfo = pimpl_->lrc.getAccountModel().getAccountInfo(account_id);
auto type = accountInfo.profileInfo.type == profile::Type::SIP ? FilterType::SIP
: FilterType::JAMI;
auto accountConv = accountInfo.conversationModel->getFilteredConversations(type);
accountConv.for_each([this,
filter,
&accountInfo,
account_id,
currentCallId,
currentConfId,
&conferences,
&calls,
&tempConferences,
&callsVector](const conversation::Info& conv) {
bool confFilterPredicate = !conv.confId.isEmpty() && conv.confId != currentConfId
&& std::find(conferences.begin(),
conferences.end(),
conv.confId)
!= conferences.end();
bool callFilterPredicate = !conv.callId.isEmpty() && conv.callId != currentCallId
&& std::find(calls.begin(), calls.end(), conv.callId)
!= calls.end();
auto& peers = pimpl_->peersForConversation(conv);
if ((!confFilterPredicate && !callFilterPredicate) || !conv.isCoreDialog()) {
return;
}
// vector of conversationID accountID pair
// for call has only one entry, for conference multyple
QVector<AccountConversation> cv;
AccountConversation accConv = {conv.uid, account_id};
cv.push_back(accConv);
bool isConference = !conv.confId.isEmpty();
// call could be added if it is not conference and in active state
bool shouldAddCall = false;
if (!isConference && accountInfo.callModel->hasCall(conv.callId)) {
const auto& call = accountInfo.callModel->getCall(conv.callId);
shouldAddCall = call.status == lrc::api::call::Status::PAUSED
|| call.status == lrc::api::call::Status::IN_PROGRESS;
}
auto contact = accountInfo.contactModel->getContact(peers.front());
// check if contact satisfy filter
bool result = (filter.isEmpty() || isConference)
? true
: (contact.profileInfo.alias.contains(filter)
|| contact.profileInfo.uri.contains(filter)
|| contact.registeredName.contains(filter));
if (!result) {
return;
}
if (isConference && tempConferences.count(conv.confId)) {
tempConferences.find(conv.confId).value().push_back(accConv);
} else if (isConference) {
tempConferences.insert(conv.confId, cv);
} else if (shouldAddCall) {
callsVector.push_back(cv);
}
});
} catch (...) {
}
}
for (auto it : tempConferences.toStdMap()) {
if (filter.isEmpty()) {
callsVector.push_back(it.second);
continue;
}
for (AccountConversation accConv : it.second) {
try {
auto& account = pimpl_->lrc.getAccountModel().getAccountInfo(accConv.accountId);
auto& conv = account.conversationModel->getConversationForUid(accConv.convId)->get();
auto& peers = pimpl_->peersForConversation(conv);
if (!conv.isCoreDialog()) {
continue;
}
auto cont = account.contactModel->getContact(peers.front());
if (cont.profileInfo.alias.contains(filter) || cont.profileInfo.uri.contains(filter)
|| cont.registeredName.contains(filter)) {
callsVector.push_back(it.second);
continue;
}
} catch (...) {
}
}
}
result.insert(ConferenceableItem::CALL, callsVector);
result.insert(ConferenceableItem::CONTACT, contactsVector);
return result;
}
const ConversationModel::ConversationQueue&
ConversationModel::getAllSearchResults() const
{
return pimpl_->searchResults;
}
const ConversationModel::ConversationQueueProxy&
ConversationModel::getFilteredConversations(const FilterType& filter,
bool forceUpdate,
const bool includeBanned) const
{
if (pimpl_->customTypeFilter == filter && !pimpl_->customFilteredConversations.isDirty()
&& !forceUpdate)
return pimpl_->customFilteredConversations;
pimpl_->customTypeFilter = filter;
return pimpl_->customFilteredConversations.reset(pimpl_->conversations)
.filter([this, &includeBanned](const conversation::Info& entry) {
try {
if (entry.isLegacy()) {
auto& peers = pimpl_->peersForConversation(entry);
if (peers.isEmpty()) {
return false;
}
auto contactInfo = owner.contactModel->getContact(peers.front());
// do not check blocked contacts for conversation with many participants
if (!includeBanned && (contactInfo.isBanned && peers.size() == 1))
return false;
}
switch (pimpl_->customTypeFilter) {
case FilterType::JAMI:
// we have conversation with many participants only for JAMI
return (owner.profileInfo.type == profile::Type::JAMI && !entry.isRequest);
case FilterType::SIP:
return (owner.profileInfo.type == profile::Type::SIP && !entry.isRequest);
case FilterType::REQUEST:
return entry.isRequest;
case FilterType::INVALID:
default:
break;
}
} catch (...) {
}
return false;
})
.validate();
}
const ConversationModel::ConversationQueueProxy&
ConversationModel::getFilteredConversations(const profile::Type& profileType,
bool forceUpdate,
const bool includeBanned) const
{
FilterType filterType = FilterType::INVALID;
switch (profileType) {
case lrc::api::profile::Type::JAMI:
filterType = lrc::api::FilterType::JAMI;
break;
case lrc::api::profile::Type::SIP:
filterType = lrc::api::FilterType::SIP;
break;
}
return getFilteredConversations(filterType, forceUpdate, includeBanned);
}
OptRef<conversation::Info>
ConversationModel::getConversationForUid(const QString& uid) const
{
try {
return std::make_optional(pimpl_->getConversationForUid(uid, true));
} catch (const std::out_of_range&) {
return std::nullopt;
}
}
OptRef<conversation::Info>
ConversationModel::getConversationForPeerUri(const QString& uri) const
{
try {
return std::make_optional(pimpl_->getConversation(
[this, uri](const conversation::Info& conv) -> bool {
if (!conv.isCoreDialog()) {
return false;
}
if (conv.mode == conversation::Mode::ONE_TO_ONE) {
return pimpl_->peersForConversation(conv).indexOf(uri) != -1;
}
return uri == pimpl_->peersForConversation(conv).front();
},
true));
} catch (const std::out_of_range&) {
return std::nullopt;
}
}
OptRef<conversation::Info>
ConversationModel::getConversationForCallId(const QString& callId) const
{
try {
return std::make_optional(pimpl_->getConversation(
[callId](const conversation::Info& conv) -> bool {
return (callId == conv.callId || callId == conv.confId);
},
true));
} catch (const std::out_of_range&) {
return std::nullopt;
}
}
OptRef<conversation::Info>
ConversationModel::filteredConversation(unsigned row) const
{
auto conversations = allFilteredConversations();
if (row >= conversations.get().size())
return std::nullopt;
return std::make_optional(conversations.get().at(row));
}
OptRef<conversation::Info>
ConversationModel::searchResultForRow(unsigned row) const
{
auto& results = pimpl_->searchResults;
if (row >= results.size())
return std::nullopt;
return std::make_optional(std::ref(results.at(row)));
}
void
ConversationModel::makePermanent(const QString& uid)
{
try {
auto& conversation = pimpl_->getConversationForUid(uid, true).get();
if (conversation.participants.empty()) {
// Should not
qDebug() << "ConversationModel::addConversation can't add a conversation with no "
"participant";
return;
}
// Send contact request if non used
auto& peers = pimpl_->peersForConversation(conversation);
if (peers.size() != 1) {
return;
}
pimpl_->sendContactRequest(peers.front());
} catch (const std::out_of_range& e) {
qDebug() << "make permanent failed. conversation not found";
}
}
void
ConversationModel::selectConversation(const QString& uid) const
{
try {
auto& conversation = pimpl_->getConversationForUid(uid, true).get();
bool callEnded = true;
if (!conversation.callId.isEmpty()) {
try {
auto call = owner.callModel->getCall(conversation.callId);
callEnded = call.status == call::Status::ENDED;
} catch (...) {
}
}
if (!conversation.confId.isEmpty() && owner.confProperties.isRendezVous) {
// If we are on a rendez vous account and we select the conversation,
// attach to the call.
CallManager::instance().unholdConference(conversation.confId);
}
if (not callEnded and not conversation.confId.isEmpty()) {
emit pimpl_->behaviorController.showCallView(owner.id, conversation.uid);
} else if (callEnded) {
emit pimpl_->behaviorController.showChatView(owner.id, conversation.uid);
} else {
try {
auto call = owner.callModel->getCall(conversation.callId);
switch (call.status) {
case call::Status::INCOMING_RINGING:
case call::Status::OUTGOING_RINGING:
case call::Status::CONNECTING:
case call::Status::SEARCHING:
// We are currently in a call
emit pimpl_->behaviorController.showIncomingCallView(owner.id, conversation.uid);
break;
case call::Status::PAUSED:
case call::Status::CONNECTED:
case call::Status::IN_PROGRESS:
// We are currently receiving a call
emit pimpl_->behaviorController.showCallView(owner.id, conversation.uid);
break;
case call::Status::PEER_BUSY:
emit pimpl_->behaviorController.showLeaveMessageView(owner.id, conversation.uid);
break;
case call::Status::TIMEOUT:
case call::Status::TERMINATING:
case call::Status::INVALID:
case call::Status::INACTIVE:
// call just ended
emit pimpl_->behaviorController.showChatView(owner.id, conversation.uid);
break;
case call::Status::ENDED:
default: // ENDED
// nothing to do
break;
}
} catch (const std::out_of_range&) {
// Should not happen
emit pimpl_->behaviorController.showChatView(owner.id, conversation.uid);
}
}
} catch (const std::out_of_range& e) {
qDebug() << "select conversation failed. conversation not exists";
}
}
void
ConversationModel::removeConversation(const QString& uid, bool banned)
{
// Get conversation
auto conversationIdx = pimpl_->indexOf(uid);
if (conversationIdx == -1)
return;
auto& conversation = pimpl_->conversations.at(conversationIdx);
if (conversation.participants.empty()) {
// Should not
qDebug() << "ConversationModel::removeConversation can't remove a conversation without "
"participant";
return;
}
if (!conversation.isCoreDialog()) {
ConfigurationManager::instance().removeConversation(owner.id, uid);
pimpl_->eraseConversation(conversationIdx);
pimpl_->invalidateModel();
emit conversationRemoved(uid);
return;
}
// Remove contact from daemon
// NOTE: this will also remove the conversation into the database for non-swarm and remove
// conversation repository for one-to-one.
auto& peers = pimpl_->peersForConversation(conversation);
if (peers.size() != 1) {
return;
}
owner.contactModel->removeContact(peers.front(), banned);
}
void
ConversationModel::deleteObsoleteHistory(int days)
{
if (days < 1)
return; // unlimited history
auto currentTime = static_cast<long int>(std::time(nullptr)); // since epoch, in seconds...
auto date = currentTime - (days * 86400);
storage::deleteObsoleteHistory(pimpl_->db, date);
}
void
ConversationModelPimpl::placeCall(const QString& uid, bool isAudioOnly)
{
try {
auto& conversation = getConversationForUid(uid, true).get();
if (conversation.participants.empty()) {
// Should not
qDebug()
<< "ConversationModel::placeCall can't call a conversation without participant";
return;
}
auto& peers = peersForConversation(conversation);
// there is no calls in group with more than 2 participants
if (peers.size() != 1) {
return;
}
// Disallow multiple call
if (!conversation.callId.isEmpty()) {
try {
auto call = linked.owner.callModel->getCall(conversation.callId);
switch (call.status) {
case call::Status::INCOMING_RINGING:
case call::Status::OUTGOING_RINGING:
case call::Status::CONNECTING:
case call::Status::SEARCHING:
case call::Status::PAUSED:
case call::Status::IN_PROGRESS:
case call::Status::CONNECTED:
return;
case call::Status::INVALID:
case call::Status::INACTIVE:
case call::Status::ENDED:
case call::Status::PEER_BUSY:
case call::Status::TIMEOUT:
case call::Status::TERMINATING:
default:
break;
}
} catch (const std::out_of_range&) {
}
}
auto convId = uid;
auto participant = peers.front();
bool isTemporary = participant == convId;
auto contactInfo = linked.owner.contactModel->getContact(participant);
auto uri = contactInfo.profileInfo.uri;
if (uri.isEmpty())
return; // Incorrect item
// Don't call banned contact
if (contactInfo.isBanned) {
qDebug() << "ContactModel::placeCall: denied, contact is banned";
return;
}
if (linked.owner.profileInfo.type != profile::Type::SIP) {
uri = "ring:" + uri; // Add the ring: before or it will fail.
}
auto cb = ([this, isTemporary, uri, isAudioOnly, &conversation](QString conversationId) {
if (indexOf(conversationId) < 0) {
qDebug() << "Can't place call: conversation not exists";
return;
}
auto& newConv = isTemporary ? getConversationForUid(conversationId).get()
: conversation;
newConv.callId = linked.owner.callModel->createCall(uri, isAudioOnly);
if (newConv.callId.isEmpty()) {
qDebug() << "Can't place call (daemon side failure ?)";
return;
}
invalidateModel();
emit behaviorController.showIncomingCallView(linked.owner.id, newConv.uid);
});
if (isTemporary) {
QMetaObject::Connection* const connection = new QMetaObject::Connection;
*connection = connect(&this->linked,
&ConversationModel::conversationReady,
[cb, connection, convId](QString conversationId,
QString participantId) {
if (participantId != convId) {
return;
}
cb(conversationId);
QObject::disconnect(*connection);
if (connection) {
delete connection;
}
});
}
sendContactRequest(participant);
if (!isTemporary) {
cb(convId);
}
} catch (const std::out_of_range& e) {
qDebug() << "could not place call to not existing conversation";
}
}
void
ConversationModel::placeAudioOnlyCall(const QString& uid)
{
pimpl_->placeCall(uid, true);
}
void
ConversationModel::placeCall(const QString& uid)
{
pimpl_->placeCall(uid);
}
MapStringString
ConversationModel::getConversationInfos(const QString& conversationId)
{
MapStringString ret = ConfigurationManager::instance().conversationInfos(owner.id,
conversationId);
return ret;
}
void
ConversationModel::createConversation(const VectorString& participants, const QString& title)
{
auto convUid = ConfigurationManager::instance().startConversation(owner.id);
for (const auto& participant : participants) {
ConfigurationManager::instance().addConversationMember(owner.id, convUid, participant);
}
if (!title.isEmpty()) {
MapStringString info = getConversationInfos(convUid);
info["title"] = title;
updateConversationInfo(convUid, info);
}
pimpl_->addSwarmConversation(convUid);
emit newConversation(convUid);
pimpl_->invalidateModel();
emit modelChanged();
}
void
ConversationModel::updateConversationInfo(const QString& conversationId, const MapStringString info)
{
ConfigurationManager::instance().updateConversationInfos(owner.id, conversationId, info);
}
bool
ConversationModel::hasPendingRequests() const
{
return pendingRequestCount() > 0;
}
int
ConversationModel::pendingRequestCount() const
{
int pendingRequestCount = 0;
std::for_each(pimpl_->conversations.begin(),
pimpl_->conversations.end(),
[&pendingRequestCount](const auto& c) { pendingRequestCount += c.isRequest; });
return pendingRequestCount;
}
int
ConversationModel::notificationsCount() const
{
int notificationsCount = 0;
std::for_each(pimpl_->conversations.begin(),
pimpl_->conversations.end(),
[¬ificationsCount](const auto& c) {
if (c.isRequest)
notificationsCount += 1;
else {
notificationsCount += c.unreadMessages;
}
});
return notificationsCount;
}
QString
ConversationModel::title(const QString& conversationId) const
{
auto conversationOpt = getConversationForUid(conversationId);
if (!conversationOpt.has_value()) {
return {};
}
auto& conversation = conversationOpt->get();
if (conversation.isCoreDialog()) {
auto peer = pimpl_->peersForConversation(conversation);
if (peer.isEmpty())
return {};
// In this case, we can just display contact name
return owner.contactModel->bestNameForContact(peer.at(0));
}
// In this case, it depends if we have infos from daemon (TODO conferencesInfo() support)
// NOTE: Do not call any daemon method there as title() is called a lot for drawing
QString title;
auto idx = 0;
for (const auto& member : conversation.participants) {
if (member == owner.profileInfo.uri) {
title += owner.accountModel->bestNameForAccount(owner.id);
} else {
title += owner.contactModel->bestNameForContact(member);
}
idx += 1;
if (idx != conversation.participants.size()) {
title += ", ";
}
}
return title;
}
void
ConversationModel::sendMessage(const QString& uid, const QString& body, const QString& parentId)
{
try {
auto& conversation = pimpl_->getConversationForUid(uid, true).get();
if (!conversation.isLegacy()) {
ConfigurationManager::instance().sendMessage(owner.id, uid, body, parentId);
return;
}
auto& peers = pimpl_->peersForConversation(conversation);
if (peers.isEmpty()) {
// Should not
qDebug() << "ConversationModel::sendMessage can't send a interaction to a conversation "
"with no participant";
return;
}
auto convId = uid;
auto& peerId = peers.front();
bool isTemporary = peerId == convId;
auto cb = ([this, isTemporary, body, &conversation, parentId, convId](
QString conversationId) {
if (pimpl_->indexOf(conversationId) < 0) {
return;
}
auto& newConv = isTemporary ? pimpl_->getConversationForUid(conversationId).get()
: conversation;
if (newConv.isSwarm()) {
ConfigurationManager::instance().sendMessage(owner.id,
conversationId,
body,
parentId);
return;
}
auto& peers = pimpl_->peersForConversation(newConv);
if (peers.isEmpty()) {
return;
}
uint64_t daemonMsgId = 0;
auto status = interaction::Status::SENDING;
auto convId = newConv.uid;
QStringList callLists = CallManager::instance().getCallList(); // no auto
// workaround: sometimes, it may happen that the daemon delete a call, but lrc
// don't. We check if the call is
// still valid every time the user want to send a message.
if (not newConv.callId.isEmpty() and not callLists.contains(newConv.callId))
newConv.callId.clear();
if (not newConv.callId.isEmpty()
and call::canSendSIPMessage(owner.callModel->getCall(newConv.callId))) {
status = interaction::Status::UNKNOWN;
owner.callModel->sendSipMessage(newConv.callId, body);
} else {
daemonMsgId = owner.contactModel->sendDhtMessage(peers.front(), body);
}
// Add interaction to database
interaction::Info
msg {{}, body, std::time(nullptr), 0, interaction::Type::TEXT, status, true};
auto msgId = storage::addMessageToConversation(pimpl_->db, convId, msg);
// Update conversation
if (status == interaction::Status::SENDING) {
// Because the daemon already give an id for the message, we need to store it.
storage::addDaemonMsgId(pimpl_->db, msgId, toQString(daemonMsgId));
}
bool ret = false;
{
std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[convId]);
ret = newConv.interactions.insert(std::pair<QString, interaction::Info>(msgId, msg))
.second;
}
if (!ret) {
qDebug()
<< "ConversationModel::sendMessage failed to send message because an existing "
"key was already present in the database key ="
<< msgId;
return;
}
newConv.lastMessageUid = msgId;
// Emit this signal for chatview in the client
emit newInteraction(convId, msgId, msg);
// This conversation is now at the top of the list
// The order has changed, informs the client to redraw the list
pimpl_->invalidateModel();
emit modelChanged();
Q_EMIT dataChanged(pimpl_->indexOf(convId));
});
if (isTemporary) {
QMetaObject::Connection* const connection = new QMetaObject::Connection;
*connection = connect(this,
&ConversationModel::conversationReady,
[cb, connection, convId](QString conversationId,
QString participantId) {
if (participantId != convId) {
return;
}
cb(conversationId);
QObject::disconnect(*connection);
if (connection) {
delete connection;
}
});
}
pimpl_->sendContactRequest(peerId);
if (!isTemporary) {
cb(convId);
}
} catch (const std::out_of_range& e) {
qDebug() << "could not send message to not existing conversation";
}
}
void
ConversationModel::refreshFilter()
{
pimpl_->invalidateModel();
emit filterChanged();
}
void
ConversationModel::updateSearchStatus(const QString& status) const
{
emit searchStatusChanged(status);
}
void
ConversationModel::setFilter(const QString& filter)
{
pimpl_->currentFilter = filter;
pimpl_->invalidateModel();
pimpl_->searchResults.clear();
emit searchResultUpdated();
owner.contactModel->searchContact(filter);
emit filterChanged();
}
void
ConversationModel::setFilter(const FilterType& filter)
{
// Switch between PENDING, RING and SIP contacts.
pimpl_->typeFilter = filter;
pimpl_->invalidateModel();
emit filterChanged();
}
void
ConversationModel::joinConversations(const QString& uidA, const QString& uidB)
{
auto conversationAIdx = pimpl_->indexOf(uidA);
auto conversationBIdx = pimpl_->indexOf(uidB);
if (conversationAIdx == -1 || conversationBIdx == -1 || !owner.enabled)
return;
auto& conversationA = pimpl_->conversations[conversationAIdx];
auto& conversationB = pimpl_->conversations[conversationBIdx];
if (conversationA.callId.isEmpty() || conversationB.callId.isEmpty())
return;
if (conversationA.confId.isEmpty()) {
if (conversationB.confId.isEmpty()) {
owner.callModel->joinCalls(conversationA.callId, conversationB.callId);
} else {
owner.callModel->joinCalls(conversationA.callId, conversationB.confId);
conversationA.confId = conversationB.confId;
}
} else {
if (conversationB.confId.isEmpty()) {
owner.callModel->joinCalls(conversationA.confId, conversationB.callId);
conversationB.confId = conversationA.confId;
} else {
owner.callModel->joinCalls(conversationA.confId, conversationB.confId);
conversationB.confId = conversationA.confId;
}
}
}
void
ConversationModel::clearHistory(const QString& uid)
{
auto conversationIdx = pimpl_->indexOf(uid);
if (conversationIdx == -1)
return;
auto& conversation = pimpl_->conversations.at(conversationIdx);
// Remove all TEXT interactions from database
storage::clearHistory(pimpl_->db, uid);
// Update conversation
{
std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[uid]);
conversation.interactions.clear();
}
storage::getHistory(pimpl_->db, conversation); // will contain "Conversation started"
emit modelChanged();
emit conversationCleared(uid);
Q_EMIT dataChanged(conversationIdx);
}
void
ConversationModel::clearInteractionFromConversation(const QString& convId,
const QString& interactionId)
{
auto conversationIdx = pimpl_->indexOf(convId);
if (conversationIdx == -1)
return;
auto erased_keys = 0;
bool lastInteractionUpdated = false;
bool updateDisplayedUid = false;
QString newDisplayedUid = 0;
QString participantURI = "";
{
std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[convId]);
try {
auto& conversation = pimpl_->conversations.at(conversationIdx);
if (conversation.isSwarm()) {
// WARNING: clearInteractionFromConversation not implemented for swarm
return;
}
storage::clearInteractionFromConversation(pimpl_->db, convId, interactionId);
erased_keys = conversation.interactions.erase(interactionId);
auto messageId = conversation.lastDisplayedMessageUid.find(
pimpl_->peersForConversation(conversation).front());
if (messageId != conversation.lastDisplayedMessageUid.end()
&& messageId->second == interactionId) {
// Update lastDisplayedMessageUid
for (auto iter = conversation.interactions.find(interactionId);
iter != conversation.interactions.end();
--iter) {
if (isOutgoing(iter->second) && iter->first != interactionId) {
newDisplayedUid = iter->first;
break;
}
}
updateDisplayedUid = true;
participantURI = pimpl_->peersForConversation(conversation).front();
conversation.lastDisplayedMessageUid.at(
pimpl_->peersForConversation(conversation).front())
= newDisplayedUid;
}
if (conversation.lastMessageUid == interactionId) {
// Update lastMessageUid
auto newLastId = QString::number(0);
if (!conversation.interactions.empty())
newLastId = conversation.interactions.rbegin()->first;
conversation.lastMessageUid = newLastId;
lastInteractionUpdated = true;
}
} catch (const std::out_of_range& e) {
qDebug() << "can't clear interaction from conversation: " << e.what();
}
}
if (updateDisplayedUid) {
emit displayedInteractionChanged(convId, participantURI, interactionId, newDisplayedUid);
}
if (erased_keys > 0) {
pimpl_->filteredConversations.invalidate();
emit interactionRemoved(convId, interactionId);
}
if (lastInteractionUpdated) {
// last interaction as changed, so the order can change.
emit modelChanged();
Q_EMIT dataChanged(conversationIdx);
}
}
void
ConversationModel::retryInteraction(const QString& convId, const QString& interactionId)
{
auto conversationIdx = pimpl_->indexOf(convId);
if (conversationIdx == -1)
return;
auto interactionType = interaction::Type::INVALID;
QString body = {};
{
std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[convId]);
try {
auto& conversation = pimpl_->conversations.at(conversationIdx);
if (conversation.isSwarm()) {
// WARNING: retry interaction is not implemented for swarm
return;
}
auto& interactions = conversation.interactions;
auto it = interactions.find(interactionId);
if (it == interactions.end())
return;
if (!interaction::isOutgoing(it->second))
return; // Do not retry non outgoing info
if (it->second.type == interaction::Type::TEXT
|| (it->second.type == interaction::Type::DATA_TRANSFER
&& interaction::isOutgoing(it->second))) {
body = it->second.body;
interactionType = it->second.type;
} else
return;
storage::clearInteractionFromConversation(pimpl_->db, convId, interactionId);
conversation.interactions.erase(interactionId);
} catch (const std::out_of_range& e) {
qDebug() << "can't find interaction from conversation: " << e.what();
return;
}
}
emit interactionRemoved(convId, interactionId);
// Send a new interaction like the previous one
if (interactionType == interaction::Type::TEXT) {
sendMessage(convId, body);
} else {
// send file
QFileInfo f(body);
sendFile(convId, body, f.fileName());
}
}
bool
ConversationModel::isLastDisplayed(const QString& convId,
const QString& interactionId,
const QString participant)
{
auto conversationIdx = pimpl_->indexOf(convId);
try {
auto& conversation = pimpl_->conversations.at(conversationIdx);
if (conversation.lastDisplayedMessageUid.find(participant)
!= conversation.lastDisplayedMessageUid.end()) {
return conversation.lastDisplayedMessageUid.find(participant)->second == interactionId;
}
} catch (const std::out_of_range& e) {
}
return false;
}
void
ConversationModel::clearAllHistory()
{
storage::clearAllHistory(pimpl_->db);
for (auto& conversation : pimpl_->conversations) {
{
if (conversation.isSwarm()) {
// WARNING: clear all history is not implemented for swarm
continue;
}
std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[conversation.uid]);
conversation.interactions.clear();
}
storage::getHistory(pimpl_->db, conversation);
Q_EMIT dataChanged(pimpl_->indexOf(conversation.uid));
}
emit modelChanged();
}
void
ConversationModel::setInteractionRead(const QString& convId, const QString& interactionId)
{
auto conversationIdx = pimpl_->indexOf(convId);
if (conversationIdx == -1) {
return;
}
bool emitUpdated = false;
interaction::Info itCopy;
{
std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[convId]);
auto& interactions = pimpl_->conversations[conversationIdx].interactions;
auto it = interactions.find(interactionId);
if (it != interactions.end()) {
emitUpdated = true;
if (it->second.isRead) {
return;
}
it->second.isRead = true;
if (pimpl_->conversations[conversationIdx].unreadMessages != 0)
pimpl_->conversations[conversationIdx].unreadMessages -= 1;
itCopy = it->second;
}
}
if (emitUpdated) {
pimpl_->invalidateModel();
if (pimpl_->conversations[conversationIdx].isSwarm()) {
ConfigurationManager::instance().setMessageDisplayed(owner.id,
"swarm:" + convId,
interactionId,
3);
} else {
auto daemonId = storage::getDaemonIdByInteractionId(pimpl_->db, interactionId);
if (owner.profileInfo.type != profile::Type::SIP) {
ConfigurationManager::instance()
.setMessageDisplayed(owner.id,
"jami:"
+ pimpl_
->peersForConversation(
pimpl_->conversations[conversationIdx])
.front(),
daemonId,
3);
}
storage::setInteractionRead(pimpl_->db, interactionId);
}
emit interactionStatusUpdated(convId, interactionId, itCopy);
emit pimpl_->behaviorController.newReadInteraction(owner.id, convId, interactionId);
}
}
void
ConversationModel::clearUnreadInteractions(const QString& convId)
{
auto conversationOpt = getConversationForUid(convId);
if (!conversationOpt.has_value()) {
return;
}
auto& conversation = conversationOpt->get();
bool emitUpdated = false;
QString lastDisplayed;
{
std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[convId]);
auto& interactions = conversation.interactions;
if (conversation.isSwarm()) {
emitUpdated = true;
if (!interactions.empty())
lastDisplayed = interactions.rbegin()->first;
} else {
std::for_each(interactions.begin(),
interactions.end(),
[&](decltype(*interactions.begin())& it) {
if (!it.second.isRead) {
emitUpdated = true;
it.second.isRead = true;
if (owner.profileInfo.type != profile::Type::SIP)
lastDisplayed = storage::getDaemonIdByInteractionId(pimpl_->db,
it.first);
storage::setInteractionRead(pimpl_->db, it.first);
}
});
}
}
if (!lastDisplayed.isEmpty()) {
auto to = conversation.isSwarm()
? "swarm:" + convId
: "jami:" + pimpl_->peersForConversation(conversation).front();
ConfigurationManager::instance().setMessageDisplayed(owner.id, to, lastDisplayed, 3);
}
if (emitUpdated) {
conversation.unreadMessages = 0;
pimpl_->invalidateModel();
emit conversationUpdated(convId);
Q_EMIT dataChanged(pimpl_->indexOf(convId));
}
}
int
ConversationModel::loadConversationMessages(const QString& conversationId, const int size)
{
auto conversationOpt = getConversationForUid(conversationId);
if (!conversationOpt.has_value()) {
return -1;
}
auto& conversation = conversationOpt->get();
if (conversation.allMessagesLoaded) {
return -1;
}
auto lastMsgId = conversation.interactions.empty() ? ""
: conversation.interactions.front().first;
return ConfigurationManager::instance().loadConversationMessages(owner.id,
conversationId,
lastMsgId,
size);
}
void
ConversationModel::acceptConversationRequest(const QString& conversationId)
{
auto conversationOpt = getConversationForUid(conversationId);
if (!conversationOpt.has_value()) {
return;
}
auto& conversation = conversationOpt->get();
auto& peers = pimpl_->peersForConversation(conversation);
if (peers.isEmpty()) {
return;
}
switch (conversation.mode) {
case conversation::Mode::NON_SWARM:
pimpl_->sendContactRequest(peers.front());
return;
case conversation::Mode::ONE_TO_ONE: {
// add contact if not added. Otherwise, accept the conversation request
try {
auto contact = owner.contactModel->getContact(peers.front());
auto notAdded = contact.profileInfo.type == profile::Type::TEMPORARY
|| contact.profileInfo.type == profile::Type::PENDING;
if (notAdded) {
owner.contactModel->addContact(contact);
return;
}
} catch (std::out_of_range& e) {
}
break;
}
case conversation::Mode::ADMIN_INVITES_ONLY:
case conversation::Mode::INVITES_ONLY:
case conversation::Mode::PUBLIC:
default:
break;
}
conversation.needsSyncing = true;
Q_EMIT conversationUpdated(conversation.uid);
pimpl_->invalidateModel();
emit modelChanged();
ConfigurationManager::instance().acceptConversationRequest(owner.id, conversationId);
}
void
ConversationModel::declineConversationRequest(const QString& conversationId, bool banned)
{
auto conversationOpt = getConversationForUid(conversationId);
if (!conversationOpt.has_value()) {
return;
}
auto& conversation = conversationOpt->get();
// for non-swarm and one-to-one conversation remove contact.
if (conversation.mode == conversation::Mode::ONE_TO_ONE
|| conversation.mode == conversation::Mode::NON_SWARM) {
removeConversation(conversationId, banned);
} else {
ConfigurationManager::instance().declineConversationRequest(owner.id, conversationId);
}
}
const VectorString
ConversationModel::peersForConversation(const QString& conversationId)
{
const auto conversationOpt = getConversationForUid(conversationId);
if (!conversationOpt.has_value()) {
return {};
}
const auto& conversation = conversationOpt->get();
return pimpl_->peersForConversation(conversation);
}
void
ConversationModel::addConversationMember(const QString& conversationId, const QString& memberId)
{
ConfigurationManager::instance().addConversationMember(owner.id, conversationId, memberId);
}
void
ConversationModel::removeConversationMember(const QString& conversationId, const QString& memberId)
{
ConfigurationManager::instance().removeConversationMember(owner.id, conversationId, memberId);
}
ConversationModelPimpl::ConversationModelPimpl(const ConversationModel& linked,
Lrc& lrc,
Database& db,
const CallbacksHandler& callbacksHandler,
const BehaviorController& behaviorController)
: linked(linked)
, lrc {lrc}
, db(db)
, callbacksHandler(callbacksHandler)
, typeFilter(FilterType::INVALID)
, customTypeFilter(FilterType::INVALID)
, behaviorController(behaviorController)
{
filteredConversations.bindSortCallback(this, &ConversationModelPimpl::sort);
filteredConversations.bindFilterCallback(this, &ConversationModelPimpl::filter);
initConversations();
// Contact related
connect(&*linked.owner.contactModel,
&ContactModel::modelUpdated,
this,
&ConversationModelPimpl::slotContactModelUpdated);
connect(&*linked.owner.contactModel,
&ContactModel::contactAdded,
this,
&ConversationModelPimpl::slotContactAdded);
connect(&*linked.owner.contactModel,
&ContactModel::incomingContactRequest,
this,
&ConversationModelPimpl::slotIncomingContactRequest);
connect(&*linked.owner.contactModel,
&ContactModel::pendingContactAccepted,
this,
&ConversationModelPimpl::slotPendingContactAccepted);
connect(&*linked.owner.contactModel,
&ContactModel::contactRemoved,
this,
&ConversationModelPimpl::slotContactRemoved);
// Messages related
connect(&*linked.owner.contactModel,
&lrc::ContactModel::newAccountMessage,
this,
&ConversationModelPimpl::slotNewAccountMessage);
connect(&callbacksHandler,
&CallbacksHandler::incomingCallMessage,
this,
&ConversationModelPimpl::slotIncomingCallMessage);
connect(&callbacksHandler,
&CallbacksHandler::accountMessageStatusChanged,
this,
&ConversationModelPimpl::slotUpdateInteractionStatus);
// Call related
connect(&*linked.owner.contactModel,
&ContactModel::incomingCall,
this,
&ConversationModelPimpl::slotIncomingCall);
connect(&*linked.owner.callModel,
&lrc::api::NewCallModel::callStatusChanged,
this,
&ConversationModelPimpl::slotCallStatusChanged);
connect(&*linked.owner.callModel,
&lrc::api::NewCallModel::callStarted,
this,
&ConversationModelPimpl::slotCallStarted);
connect(&*linked.owner.callModel,
&lrc::api::NewCallModel::callEnded,
this,
&ConversationModelPimpl::slotCallEnded);
connect(&*linked.owner.callModel,
&lrc::api::NewCallModel::callAddedToConference,
this,
&ConversationModelPimpl::slotCallAddedToConference);
connect(&callbacksHandler,
&CallbacksHandler::conferenceRemoved,
this,
&ConversationModelPimpl::slotConferenceRemoved);
connect(&ConfigurationManager::instance(),
&ConfigurationManagerInterface::composingStatusChanged,
this,
&ConversationModelPimpl::slotComposingStatusChanged);
// data transfer
connect(&*linked.owner.contactModel,
&ContactModel::newAccountTransfer,
this,
&ConversationModelPimpl::slotTransferStatusCreated);
connect(&callbacksHandler,
&CallbacksHandler::transferStatusCanceled,
this,
&ConversationModelPimpl::slotTransferStatusCanceled);
connect(&callbacksHandler,
&CallbacksHandler::transferStatusAwaitingPeer,
this,
&ConversationModelPimpl::slotTransferStatusAwaitingPeer);
connect(&callbacksHandler,
&CallbacksHandler::transferStatusAwaitingHost,
this,
&ConversationModelPimpl::slotTransferStatusAwaitingHost);
connect(&callbacksHandler,
&CallbacksHandler::transferStatusOngoing,
this,
&ConversationModelPimpl::slotTransferStatusOngoing);
connect(&callbacksHandler,
&CallbacksHandler::transferStatusFinished,
this,
&ConversationModelPimpl::slotTransferStatusFinished);
connect(&callbacksHandler,
&CallbacksHandler::transferStatusError,
this,
&ConversationModelPimpl::slotTransferStatusError);
connect(&callbacksHandler,
&CallbacksHandler::transferStatusTimeoutExpired,
this,
&ConversationModelPimpl::slotTransferStatusTimeoutExpired);
connect(&callbacksHandler,
&CallbacksHandler::transferStatusUnjoinable,
this,
&ConversationModelPimpl::slotTransferStatusUnjoinable);
// swarm conversations
connect(&callbacksHandler,
&CallbacksHandler::conversationLoaded,
this,
&ConversationModelPimpl::slotConversationLoaded);
connect(&callbacksHandler,
&CallbacksHandler::messageReceived,
this,
&ConversationModelPimpl::slotMessageReceived);
connect(&callbacksHandler,
&CallbacksHandler::conversationRequestReceived,
this,
&ConversationModelPimpl::slotConversationRequestReceived);
connect(&callbacksHandler,
&CallbacksHandler::conversationRequestDeclined,
this,
&ConversationModelPimpl::slotConversationRequestDeclined);
connect(&callbacksHandler,
&CallbacksHandler::conversationReady,
this,
&ConversationModelPimpl::slotConversationReady);
connect(&callbacksHandler,
&CallbacksHandler::conversationRemoved,
this,
&ConversationModelPimpl::slotConversationRemoved);
connect(&callbacksHandler,
&CallbacksHandler::conversationMemberEvent,
this,
&ConversationModelPimpl::slotConversationMemberEvent);
}
ConversationModelPimpl::~ConversationModelPimpl()
{
// Contact related
disconnect(&*linked.owner.contactModel,
&ContactModel::modelUpdated,
this,
&ConversationModelPimpl::slotContactModelUpdated);
disconnect(&*linked.owner.contactModel,
&ContactModel::contactAdded,
this,
&ConversationModelPimpl::slotContactAdded);
disconnect(&*linked.owner.contactModel,
&ContactModel::incomingContactRequest,
this,
&ConversationModelPimpl::slotIncomingContactRequest);
disconnect(&*linked.owner.contactModel,
&ContactModel::pendingContactAccepted,
this,
&ConversationModelPimpl::slotPendingContactAccepted);
disconnect(&*linked.owner.contactModel,
&ContactModel::contactRemoved,
this,
&ConversationModelPimpl::slotContactRemoved);
// Messages related
disconnect(&*linked.owner.contactModel,
&lrc::ContactModel::newAccountMessage,
this,
&ConversationModelPimpl::slotNewAccountMessage);
disconnect(&callbacksHandler,
&CallbacksHandler::incomingCallMessage,
this,
&ConversationModelPimpl::slotIncomingCallMessage);
disconnect(&callbacksHandler,
&CallbacksHandler::accountMessageStatusChanged,
this,
&ConversationModelPimpl::slotUpdateInteractionStatus);
// Call related
disconnect(&*linked.owner.contactModel,
&ContactModel::incomingCall,
this,
&ConversationModelPimpl::slotIncomingCall);
disconnect(&*linked.owner.callModel,
&lrc::api::NewCallModel::callStatusChanged,
this,
&ConversationModelPimpl::slotCallStatusChanged);
disconnect(&*linked.owner.callModel,
&lrc::api::NewCallModel::callStarted,
this,
&ConversationModelPimpl::slotCallStarted);
disconnect(&*linked.owner.callModel,
&lrc::api::NewCallModel::callEnded,
this,
&ConversationModelPimpl::slotCallEnded);
disconnect(&*linked.owner.callModel,
&lrc::api::NewCallModel::callAddedToConference,
this,
&ConversationModelPimpl::slotCallAddedToConference);
disconnect(&callbacksHandler,
&CallbacksHandler::conferenceRemoved,
this,
&ConversationModelPimpl::slotConferenceRemoved);
disconnect(&ConfigurationManager::instance(),
&ConfigurationManagerInterface::composingStatusChanged,
this,
&ConversationModelPimpl::slotComposingStatusChanged);
// data transfer
disconnect(&*linked.owner.contactModel,
&ContactModel::newAccountTransfer,
this,
&ConversationModelPimpl::slotTransferStatusCreated);
disconnect(&callbacksHandler,
&CallbacksHandler::transferStatusCanceled,
this,
&ConversationModelPimpl::slotTransferStatusCanceled);
disconnect(&callbacksHandler,
&CallbacksHandler::transferStatusAwaitingPeer,
this,
&ConversationModelPimpl::slotTransferStatusAwaitingPeer);
disconnect(&callbacksHandler,
&CallbacksHandler::transferStatusAwaitingHost,
this,
&ConversationModelPimpl::slotTransferStatusAwaitingHost);
disconnect(&callbacksHandler,
&CallbacksHandler::transferStatusOngoing,
this,
&ConversationModelPimpl::slotTransferStatusOngoing);
disconnect(&callbacksHandler,
&CallbacksHandler::transferStatusFinished,
this,
&ConversationModelPimpl::slotTransferStatusFinished);
disconnect(&callbacksHandler,
&CallbacksHandler::transferStatusError,
this,
&ConversationModelPimpl::slotTransferStatusError);
disconnect(&callbacksHandler,
&CallbacksHandler::transferStatusTimeoutExpired,
this,
&ConversationModelPimpl::slotTransferStatusTimeoutExpired);
disconnect(&callbacksHandler,
&CallbacksHandler::transferStatusUnjoinable,
this,
&ConversationModelPimpl::slotTransferStatusUnjoinable);
// swarm conversations
disconnect(&callbacksHandler,
&CallbacksHandler::conversationLoaded,
this,
&ConversationModelPimpl::slotConversationLoaded);
disconnect(&callbacksHandler,
&CallbacksHandler::messageReceived,
this,
&ConversationModelPimpl::slotMessageReceived);
disconnect(&callbacksHandler,
&CallbacksHandler::conversationRequestReceived,
this,
&ConversationModelPimpl::slotConversationRequestReceived);
disconnect(&callbacksHandler,
&CallbacksHandler::conversationRequestDeclined,
this,
&ConversationModelPimpl::slotConversationRequestDeclined);
disconnect(&callbacksHandler,
&CallbacksHandler::conversationReady,
this,
&ConversationModelPimpl::slotConversationReady);
disconnect(&callbacksHandler,
&CallbacksHandler::conversationRemoved,
this,
&ConversationModelPimpl::slotConversationRemoved);
disconnect(&callbacksHandler,
&CallbacksHandler::conversationMemberEvent,
this,
&ConversationModelPimpl::slotConversationMemberEvent);
}
void
ConversationModelPimpl::initConversations()
{
const MapStringString accountDetails = ConfigurationManager::instance().getAccountDetails(
linked.owner.id);
if (accountDetails.empty())
return;
// Fill swarm conversations
QStringList swarms = ConfigurationManager::instance().getConversations(linked.owner.id);
for (auto& swarmConv : swarms) {
addSwarmConversation(swarmConv);
}
VectorMapStringString conversationsRequests = ConfigurationManager::instance()
.getConversationRequests(linked.owner.id);
for (auto& request : conversationsRequests) {
addConversationRequest(request);
}
// Fill conversations
for (auto const& c : linked.owner.contactModel->getAllContacts().toStdMap()) {
auto conv = storage::getConversationsWithPeer(db, c.second.profileInfo.uri);
if (hasOneOneSwarmWith(c.second.profileInfo.uri))
continue;
if (conv.empty()) {
// Can't find a conversation with this contact
// add pending not swarm conversation
if (c.second.profileInfo.type == profile::Type::PENDING) {
addContactRequest(c.second.profileInfo.uri);
continue;
}
conv.push_back(storage::beginConversationWithPeer(db, c.second.profileInfo.uri));
}
addConversationWith(conv[0], c.first);
auto convIdx = indexOf(conv[0]);
// Check if file transfer interactions were left in an incorrect state
std::lock_guard<std::mutex> lk(interactionsLocks[conversations[convIdx].uid]);
for (auto& interaction : conversations[convIdx].interactions) {
if (interaction.second.status == interaction::Status::TRANSFER_CREATED
|| interaction.second.status == interaction::Status::TRANSFER_AWAITING_HOST
|| interaction.second.status == interaction::Status::TRANSFER_AWAITING_PEER
|| interaction.second.status == interaction::Status::TRANSFER_ONGOING
|| interaction.second.status == interaction::Status::TRANSFER_ACCEPTED) {
// If a datatransfer was left in a non-terminal status in DB, we switch this status
// to ERROR
// TODO : Improve for DBus clients as daemon and transfer may still be ongoing
storage::updateInteractionStatus(db,
interaction.first,
interaction::Status::TRANSFER_ERROR);
interaction.second.status = interaction::Status::TRANSFER_ERROR;
}
}
}
invalidateModel();
filteredConversations.reset(conversations).sort();
// Load all non treated messages for this account
QVector<Message> messages = ConfigurationManager::instance()
.getLastMessages(linked.owner.id, storage::getLastTimestamp(db));
for (const auto& message : messages) {
uint64_t timestamp = 0;
try {
timestamp = static_cast<uint64_t>(message.received);
} catch (...) {
}
addIncomingMessage(message.from, message.payloads["text/plain"], timestamp);
}
}
const VectorString
ConversationModelPimpl::peersForConversation(const conversation::Info& conversation) const
{
VectorString result {};
switch (conversation.mode) {
case conversation::Mode::NON_SWARM:
return conversation.participants;
default:
break;
}
// Note: for one to one, we must return self
if (conversation.participants.size() == 1)
return conversation.participants;
for (const auto& participant : conversation.participants)
if (participant != linked.owner.profileInfo.uri)
result.push_back(participant);
return result;
}
bool
ConversationModelPimpl::filter(const conversation::Info& entry)
{
try {
// TODO: filter for group?
// for now group conversation filtered by first peer
auto& peers = peersForConversation(entry);
if (peers.size() < 1) {
return false;
}
auto uriPeer = peers.front();
contact::Info contactInfo;
try {
contactInfo = linked.owner.contactModel->getContact(uriPeer);
} catch (...) {
// Note: as we search for contacts, when importing a new account,
// the conversation's request can be there without contact, causing
// the function to fail.
contactInfo.profileInfo.uri = uriPeer;
}
auto uri = URI(currentFilter);
bool stripScheme = (uri.schemeType() < URI::SchemeType::COUNT__);
FlagPack<URI::Section> flags = URI::Section::USER_INFO | URI::Section::HOSTNAME
| URI::Section::PORT;
if (!stripScheme) {
flags |= URI::Section::SCHEME;
}
currentFilter = uri.format(flags);
// Check contact
// If contact is banned, only match if filter is a perfect match
// do not check banned contact for conversation with multiple participants
if (contactInfo.isBanned && peers.size() == 1) {
if (currentFilter == "")
return false;
return contactInfo.profileInfo.uri == currentFilter
|| contactInfo.profileInfo.alias == currentFilter
|| contactInfo.registeredName == currentFilter;
}
std::regex regexFilter;
auto isValidReFilter = true;
try {
regexFilter = std::regex(currentFilter.toStdString(), std::regex_constants::icase);
} catch (std::regex_error&) {
isValidReFilter = false;
}
auto filterUriAndReg = [regexFilter, isValidReFilter](auto contact, auto filter) {
auto result = contact.profileInfo.uri.contains(filter)
|| contact.registeredName.contains(filter);
if (!result) {
auto regexFound = isValidReFilter
? (!contact.profileInfo.uri.isEmpty()
&& std::regex_search(contact.profileInfo.uri.toStdString(),
regexFilter))
|| std::regex_search(contact.registeredName.toStdString(),
regexFilter)
: false;
result |= regexFound;
}
return result;
};
// Check type
switch (typeFilter) {
case FilterType::JAMI:
case FilterType::SIP:
if (entry.isRequest)
return false;
if (contactInfo.profileInfo.type == profile::Type::TEMPORARY)
return filterUriAndReg(contactInfo, currentFilter);
break;
case FilterType::REQUEST:
if (!entry.isRequest)
return false;
break;
default:
break;
}
// Otherwise perform usual regex search
bool result = contactInfo.profileInfo.alias.contains(currentFilter);
if (!result && isValidReFilter)
result |= std::regex_search(contactInfo.profileInfo.alias.toStdString(), regexFilter);
if (!result)
result |= filterUriAndReg(contactInfo, currentFilter);
return result;
} catch (std::out_of_range&) {
// getContact() failed
return false;
}
}
bool
ConversationModelPimpl::sort(const conversation::Info& convA, const conversation::Info& convB)
{
// A or B is a temporary contact
if (convA.participants.isEmpty())
return true;
if (convB.participants.isEmpty())
return false;
if (convA.uid == convB.uid)
return false;
auto& mtxA = interactionsLocks[convA.uid];
auto& mtxB = interactionsLocks[convB.uid];
std::lock(mtxA, mtxB);
std::lock_guard<std::mutex> lockConvA(mtxA, std::adopt_lock);
std::lock_guard<std::mutex> lockConvB(mtxB, std::adopt_lock);
auto& historyA = convA.interactions;
auto& historyB = convB.interactions;
// A or B is a new conversation (without CONTACT interaction)
if (convA.uid.isEmpty() || convB.uid.isEmpty())
return convA.uid.isEmpty();
if (historyA.empty() && historyB.empty()) {
// If no information to compare, sort by Ring ID. For group conversation sort by first peer
auto& peersForA = peersForConversation(convA);
auto& peersForB = peersForConversation(convB);
if (peersForA.isEmpty()) {
return false;
}
if (peersForB.isEmpty()) {
return true;
}
return peersForA.front() > peersForB.front();
}
if (historyA.empty())
return false;
if (historyB.empty())
return true;
// Sort by last Interaction
try {
auto lastMessageA = historyA.at(convA.lastMessageUid);
auto lastMessageB = historyB.at(convB.lastMessageUid);
return lastMessageA.timestamp > lastMessageB.timestamp;
} catch (const std::exception& e) {
qDebug() << "ConversationModel::sortConversations(), can't get lastMessage";
return false;
}
}
void
ConversationModelPimpl::sendContactRequest(const QString& contactUri)
{
auto contact = linked.owner.contactModel->getContact(contactUri);
auto isNotUsed = contact.profileInfo.type == profile::Type::TEMPORARY
|| contact.profileInfo.type == profile::Type::PENDING;
if (isNotUsed)
linked.owner.contactModel->addContact(contact);
}
void
ConversationModelPimpl::slotConversationLoaded(uint32_t,
const QString& accountId,
const QString& conversationId,
const VectorMapStringString& messages)
{
if (accountId != linked.owner.id) {
return;
}
try {
auto& conversation = getConversationForUid(conversationId).get();
auto size = messages.size();
for (int i = size - 1; i >= 0; --i) {
auto message = messages[i];
if (message["type"].isEmpty()) {
continue;
}
if (message["type"] == "initial") {
conversation.allMessagesLoaded = true;
if (message.find("invited") == message.end()) {
continue;
}
}
auto msgId = message["id"];
auto msg = interaction::Info(message, linked.owner.profileInfo.uri);
auto downloadFile = false;
if (msg.type == interaction::Type::DATA_TRANSFER) {
auto fileId = message["fileId"];
QString path;
qlonglong bytesProgress, totalSize;
linked.owner.dataTransferModel->fileTransferInfo(accountId,
conversationId,
fileId,
path,
totalSize,
bytesProgress);
QFileInfo fi(path);
if (fi.isSymLink()) {
msg.body = fi.symLinkTarget();
} else {
msg.body = path;
}
msg.status = bytesProgress == 0 ? interaction::Status::TRANSFER_AWAITING_HOST
: bytesProgress == totalSize ? interaction::Status::TRANSFER_FINISHED
: interaction::Status::TRANSFER_ONGOING;
linked.owner.dataTransferModel->registerTransferId(fileId, msgId);
downloadFile = (bytesProgress == 0);
} else if (msg.type == interaction::Type::CALL) {
msg.body = storage::getCallInteractionString(msg.authorUri, msg.duration);
}
insertSwarmInteraction(msgId, msg, conversation, true);
if (downloadFile) {
// Note, we must do this after insertSwarmInteraction to find the interaction
handleIncomingFile(conversationId,
msgId,
message["displayName"],
message["totalSize"].toInt());
}
}
for (int j = conversation.interactions.size() - 1; j >= 0; j--) {
if (conversation.interactions.atIndex(j).second.type != interaction::Type::MERGE) {
conversation.lastMessageUid = conversation.interactions.atIndex(j).first;
break;
}
}
invalidateModel();
emit linked.modelChanged();
emit linked.newMessagesAvailable(linked.owner.id, conversationId);
auto conversationIdx = indexOf(conversationId);
Q_EMIT linked.dataChanged(conversationIdx);
} catch (const std::exception& e) {
qDebug() << "messages loaded for not existing conversation";
}
}
void
ConversationModelPimpl::slotMessageReceived(const QString& accountId,
const QString& conversationId,
const MapStringString& message)
{
if (accountId != linked.owner.id) {
return;
}
try {
auto& conversation = getConversationForUid(conversationId).get();
if (message["type"].isEmpty()) {
return;
}
if (message["type"] == "initial") {
conversation.allMessagesLoaded = true;
if (message.find("invited") == message.end()) {
return;
}
}
auto msgId = message["id"];
auto msg = interaction::Info(message, linked.owner.profileInfo.uri);
api::datatransfer::Info info;
QString fileId;
if (msg.type == interaction::Type::DATA_TRANSFER) {
// save data transfer interaction to db and assosiate daemon id with interaction id,
// conversation id and db id
QString fileId = message["fileId"];
QString path;
qlonglong bytesProgress, totalSize;
linked.owner.dataTransferModel->fileTransferInfo(accountId,
conversationId,
fileId,
path,
totalSize,
bytesProgress);
QFileInfo fi(path);
if (fi.isSymLink()) {
msg.body = fi.symLinkTarget();
} else {
msg.body = path;
}
msg.status = bytesProgress == 0 ? interaction::Status::TRANSFER_AWAITING_HOST
: bytesProgress == totalSize ? interaction::Status::TRANSFER_FINISHED
: interaction::Status::TRANSFER_ONGOING;
linked.owner.dataTransferModel->registerTransferId(fileId, msgId);
} else if (msg.type == interaction::Type::CALL) {
msg.body = storage::getCallInteractionString(msg.authorUri, msg.duration);
} else if (msg.type == interaction::Type::TEXT
&& msg.authorUri != linked.owner.profileInfo.uri) {
conversation.unreadMessages++;
}
if (!insertSwarmInteraction(msgId, msg, conversation, false)) {
// message already exists
return;
}
if (msg.type == interaction::Type::MERGE) {
invalidateModel();
return;
}
conversation.lastMessageUid = msgId;
invalidateModel();
if (!interaction::isOutgoing(msg)) {
emit behaviorController.newUnreadInteraction(linked.owner.id,
conversationId,
msgId,
msg);
}
emit linked.newInteraction(conversationId, msgId, msg);
emit linked.modelChanged();
if (msg.status == interaction::Status::TRANSFER_AWAITING_HOST) {
handleIncomingFile(conversationId,
msgId,
message["displayName"],
message["totalSize"].toInt());
}
Q_EMIT linked.dataChanged(indexOf(conversationId));
} catch (const std::exception& e) {
qDebug() << "messages received for not existing conversation";
}
}
bool
ConversationModelPimpl::insertSwarmInteraction(const QString& interactionId,
const interaction::Info& interaction,
conversation::Info& conversation,
bool insertAtBegin)
{
std::lock_guard<std::mutex> lk(interactionsLocks[conversation.uid]);
int index = conversation.interactions.indexOfMessage(interaction.parentId);
if (index >= 0) {
auto result = conversation.interactions.insert(index + 1,
qMakePair(interactionId, interaction));
if (!result.second) {
return false;
}
} else {
auto result = conversation.interactions.insert(std::make_pair(interactionId, interaction),
insertAtBegin);
if (!result.second) {
return false;
}
conversation.parentsId[interactionId] = interaction.parentId;
}
if (!conversation.parentsId.values().contains(interactionId)) {
return true;
}
auto msgIds = conversation.parentsId.keys(interactionId);
conversation.interactions.moveMessages(msgIds, interactionId);
for (auto& msg : msgIds) {
conversation.parentsId.remove(msg);
}
return true;
}
void
ConversationModelPimpl::slotConversationRequestReceived(const QString& accountId,
const QString&,
const MapStringString& metadatas)
{
if (accountId != linked.owner.id) {
return;
}
addConversationRequest(metadatas);
}
void
ConversationModelPimpl::slotConversationRequestDeclined(const QString& accountId,
const QString& convId)
{
auto conversationIndex = indexOf(convId);
if (accountId != linked.owner.id || conversationIndex < 0)
return;
eraseConversation(conversationIndex);
Q_EMIT linked.conversationRemoved(convId);
Q_EMIT linked.modelChanged();
}
void
ConversationModelPimpl::slotConversationReady(const QString& accountId,
const QString& conversationId)
{
// we receive this signal after we accept or after we send a conversation request
if (accountId != linked.owner.id) {
return;
}
// remove non swarm conversation that was added from slotContactAdded
const VectorMapStringString& members = ConfigurationManager::instance()
.getConversationMembers(accountId, conversationId);
VectorString participants;
// it means conversation with one participant. In this case we could have non swarm conversation
bool shouldRemoveNonSwarmConversation = members.size() == 2;
for (const auto& member : members) {
participants.append(member["uri"]);
if (shouldRemoveNonSwarmConversation) {
try {
auto& conversation = getConversationForPeerUri(member["uri"]).get();
// remove non swarm conversation
if (conversation.isLegacy()) {
eraseConversation(conversation.uid);
storage::removeContactConversations(db, member["uri"]);
invalidateModel();
emit linked.conversationRemoved(conversation.uid);
emit linked.modelChanged();
}
} catch (...) {
}
}
}
int conversationIdx = indexOf(conversationId);
bool conversationExists = conversationIdx >= 0;
if (!conversationExists) {
addSwarmConversation(conversationId);
}
auto& conversation = getConversationForUid(conversationId).get();
if (conversationExists) {
// if swarm request already exists, update participnts
auto& conversation = getConversationForUid(conversationId).get();
conversation.participants = participants;
const MapStringString& details = ConfigurationManager::instance()
.conversationInfos(accountId, conversationId);
conversation.mode = conversation::to_mode(details["mode"].toInt());
conversation.isRequest = false;
conversation.needsSyncing = false;
Q_EMIT linked.conversationUpdated(conversationId);
Q_EMIT linked.dataChanged(conversationIdx);
auto id = ConfigurationManager::instance().loadConversationMessages(linked.owner.id,
conversationId,
"",
0);
auto& peers = peersForConversation(conversation);
if (peers.size() == 1)
emit linked.conversationReady(conversationId, peers.front());
return;
}
invalidateModel();
// we use conversationReady callback only for conversation with one participant. We could use
// participants.front()
auto& peers = peersForConversation(conversation);
if (peers.size() == 1)
emit linked.conversationReady(conversationId, peers.front());
emit linked.newConversation(conversationId);
emit linked.modelChanged();
}
void
ConversationModelPimpl::slotConversationRemoved(const QString& accountId,
const QString& conversationId)
{
auto conversationIndex = indexOf(conversationId);
if (accountId != linked.owner.id || conversationIndex < 0) {
return;
}
try {
auto removeConversation = [&]() {
// remove swarm conversation
eraseConversation(conversationIndex);
Q_EMIT linked.conversationRemoved(conversationId);
};
auto& conversation = getConversationForUid(conversationId).get();
auto& peers = peersForConversation(conversation);
// if swarm conversation removed but we still have contact we create non swarm conversation.
// we should create non swarm conversation only for removed one-to-one conversation.
if (conversation.mode == conversation::Mode::ONE_TO_ONE) {
if (peers.isEmpty()) {
removeConversation();
return;
}
auto contactId = peers.first();
removeConversation();
auto conv = storage::getConversationsWithPeer(db, contactId);
if (conv.empty()) {
conv.push_back(storage::beginConversationWithPeer(db, contactId));
}
addConversationWith(conv[0], contactId);
emit linked.newConversation(conv[0]);
} else {
removeConversation();
}
} catch (const std::exception& e) {
qWarning() << e.what();
}
}
void
ConversationModelPimpl::slotConversationMemberEvent(const QString& accountId,
const QString& conversationId,
const QString& memberUri,
int event)
{
if (accountId != linked.owner.id || indexOf(conversationId) < 0) {
return;
}
switch (event) {
case 0: // add
// clear search result
for (unsigned int i = 0; i < searchResults.size(); ++i) {
if (searchResults.at(i).uid == memberUri)
searchResults.erase(searchResults.begin() + i);
}
break;
case 1: // joins
break;
case 2: // leave
break;
case 3: // banned
break;
}
// update participants
auto& conversation = getConversationForUid(conversationId).get();
const VectorMapStringString& members
= ConfigurationManager::instance().getConversationMembers(linked.owner.id, conversationId);
VectorString uris;
VectorString membersRemaining;
for (auto& member : members) {
uris.append(member["uri"]);
if (member["role"] != "left")
membersRemaining.append(member["uri"]);
}
conversation.participants = uris;
conversation.readOnly = membersRemaining == VectorString(1, linked.owner.profileInfo.uri);
invalidateModel();
Q_EMIT linked.modelChanged();
Q_EMIT linked.conversationUpdated(conversationId);
Q_EMIT linked.dataChanged(indexOf(conversationId));
}
void
ConversationModelPimpl::slotIncomingContactRequest(const QString& contactUri)
{
// It is contact request. But for compatibility with swarm conversations we add it like non
// swarm conversation request.
addContactRequest(contactUri);
}
void
ConversationModelPimpl::slotContactAdded(const QString& contactUri)
{
auto conv = storage::getConversationsWithPeer(db, contactUri);
bool addConversation = false;
bool removeConversation = false;
try {
auto& conversation = getConversationForPeerUri(contactUri).get();
// swarm conversation we update when receive conversation ready signal.
if (conversation.isSwarm()) {
MapStringString details = ConfigurationManager::instance()
.conversationInfos(linked.owner.id, conversation.uid);
bool needsSyncing = details["syncing"] == "true";
if (conversation.needsSyncing != needsSyncing) {
conversation.isRequest = false;
conversation.needsSyncing = needsSyncing;
Q_EMIT linked.dataChanged(indexOf(conversation.uid));
Q_EMIT linked.conversationUpdated(conversation.uid);
invalidateModel();
emit linked.modelChanged();
}
return;
}
if (conv.empty()) {
conv.push_back(storage::beginConversationWithPeer(db, contactUri));
}
// remove temporary conversation that was added when receiving an incoming request
removeConversation = indexOf(contactUri) != -1;
// add a conversation if not exists
addConversation = indexOf(conv[0]) == -1;
} catch (std::out_of_range&) {
/*
if the conversation does not exists we save it to DB and add non-swarm
conversation to the conversion list. After receiving a conversation request or
conversation ready signal swarm conversation should be updated and removed from DB.
*/
addConversation = true;
if (conv.empty()) {
conv.push_back(storage::beginConversationWithPeer(db, contactUri));
}
}
if (addConversation) {
addConversationWith(conv[0], contactUri);
emit linked.conversationReady(conv[0], contactUri);
emit linked.newConversation(conv[0]);
}
if (removeConversation) {
eraseConversation(indexOf(contactUri));
invalidateModel();
Q_EMIT linked.conversationRemoved(contactUri);
emit linked.modelChanged();
}
}
void
ConversationModelPimpl::addContactRequest(const QString& contactUri)
{
try {
getConversationForPeerUri(contactUri).get();
// request from contact already exists, return
return;
} catch (std::out_of_range&) {
// no conversation exists. Add contact request
conversation::Info conversation;
conversation.uid = contactUri;
conversation.accountId = linked.owner.id;
conversation.participants = {contactUri};
conversation.mode = conversation::Mode::NON_SWARM;
conversation.isRequest = true;
emplaceBackConversation(std::move(conversation));
invalidateModel();
emit linked.newConversation(contactUri);
emit linked.modelChanged();
}
}
void
ConversationModelPimpl::addConversationRequest(const MapStringString& convRequest)
{
auto convId = convRequest["id"];
auto convIdx = indexOf(convId);
if (convIdx != -1)
return;
auto peerUri = convRequest["from"];
auto mode = conversation::to_mode(convRequest["mode"].toInt());
if (mode == conversation::Mode::ONE_TO_ONE) {
try {
// check if we have contact request for peer
auto& conv = getConversationForPeerUri(peerUri).get();
if (conv.mode == conversation::Mode::NON_SWARM) {
// update conversation and remove the invite conversation from db
conv.mode = mode;
conv.uid = convId;
storage::removeContactConversations(db, peerUri);
invalidateModel();
emit linked.modelChanged();
return;
}
} catch (std::out_of_range&) {
qWarning() << "Couldn't find contact request conversation for" << peerUri;
}
}
// add the author to the contact model's contact list as a PENDING
// if they aren't already a contact
linked.owner.contactModel->addToContacts(peerUri);
conversation::Info conversation;
conversation.uid = convId;
conversation.accountId = linked.owner.id;
conversation.participants = {linked.owner.profileInfo.uri, peerUri};
conversation.mode = mode;
conversation.isRequest = true;
emplaceBackConversation(std::move(conversation));
invalidateModel();
emit linked.newConversation(peerUri);
emit linked.modelChanged();
}
void
ConversationModelPimpl::slotPendingContactAccepted(const QString& uri)
{
auto type = linked.owner.profileInfo.type;
try {
type = linked.owner.contactModel->getContact(uri).profileInfo.type;
} catch (std::out_of_range& e) {
}
profile::Info profileInfo {uri, {}, {}, type};
storage::createOrUpdateProfile(linked.owner.id, profileInfo, true);
auto convs = storage::getConversationsWithPeer(db, uri);
if (!convs.empty()) {
try {
auto contact = linked.owner.contactModel->getContact(uri);
auto interaction = interaction::Info {uri,
{},
std::time(nullptr),
0,
interaction::Type::CONTACT,
interaction::Status::SUCCESS,
true};
auto msgId = storage::addMessageToConversation(db, convs[0], interaction);
interaction.body = storage::getContactInteractionString(uri,
interaction::Status::SUCCESS);
auto convIdx = indexOf(convs[0]);
if (convIdx >= 0) {
std::lock_guard<std::mutex> lk(interactionsLocks[conversations[convIdx].uid]);
conversations[convIdx].interactions.emplace(msgId, interaction);
}
filteredConversations.invalidate();
emit linked.newInteraction(convs[0], msgId, interaction);
Q_EMIT linked.dataChanged(convIdx);
} catch (std::out_of_range& e) {
qDebug() << "ConversationModelPimpl::slotContactAdded can't find contact";
}
}
}
void
ConversationModelPimpl::slotContactRemoved(const QString& uri)
{
auto conversationIdx = indexOfContact(uri);
if (conversationIdx == -1) {
qDebug() << "ConversationModelPimpl::slotContactRemoved, but conversation not found";
return; // Not a contact
}
auto& conversationId = conversations[conversationIdx].uid;
eraseConversation(conversationIdx);
invalidateModel();
emit linked.conversationRemoved(conversationId);
emit linked.modelChanged();
}
void
ConversationModelPimpl::slotContactModelUpdated(const QString& uri)
{
try {
auto& conversation = getConversationForPeerUri(uri, true).get();
invalidateModel();
emit linked.conversationUpdated(conversation.uid);
Q_EMIT linked.dataChanged(indexOf(conversation.uid));
} catch (std::out_of_range&) {
qDebug() << "contact update attempted for inexistent conversation";
}
if (currentFilter.isEmpty()) {
if (searchResults.empty())
return;
searchResults.clear();
emit linked.searchResultUpdated();
return;
}
searchResults.clear();
auto users = linked.owner.contactModel->getSearchResults();
for (auto& user : users) {
conversation::Info conversationInfo;
conversationInfo.uid = user.profileInfo.uri;
conversationInfo.participants.push_back(user.profileInfo.uri);
conversationInfo.accountId = linked.owner.id;
searchResults.emplace_front(std::move(conversationInfo));
}
emit linked.searchResultUpdated();
}
void
ConversationModelPimpl::addSwarmConversation(const QString& convId)
{
VectorString participants;
const VectorMapStringString& members = ConfigurationManager::instance()
.getConversationMembers(linked.owner.id, convId);
auto accountURI = linked.owner.profileInfo.uri;
QString otherMember;
const MapStringString& details = ConfigurationManager::instance()
.conversationInfos(linked.owner.id, convId);
auto mode = conversation::to_mode(details["mode"].toInt());
conversation::Info conversation;
conversation.uid = convId;
conversation.accountId = linked.owner.id;
QString lastRead;
VectorString membersRemaining;
for (auto& member : members) {
// this check should be removed once all usage of participants replaced by
// peersForConversation. We should have ourself in participants list
// Note: if members.size() == 1, it's a conv with self so we're also the peer
participants.append(member["uri"]);
if (mode == conversation::Mode::ONE_TO_ONE && member["uri"] != accountURI) {
otherMember = member["uri"];
} else if (member["uri"] == accountURI) {
lastRead = member["lastDisplayed"];
}
conversation.lastDisplayedMessageUid.emplace(member["uri"], member["lastDisplayed"]);
if (member["role"] != "left")
membersRemaining.append(member["uri"]);
}
conversation.readOnly = membersRemaining == VectorString(1, accountURI);
conversation.participants = participants;
conversation.mode = mode;
conversation.unreadMessages = ConfigurationManager::instance()
.countInteractions(linked.owner.id, convId, lastRead, "");
if (mode == conversation::Mode::ONE_TO_ONE && !otherMember.isEmpty()) {
try {
conversation.confId = linked.owner.callModel->getConferenceFromURI(otherMember).id;
} catch (...) {
conversation.confId = "";
}
try {
conversation.callId = linked.owner.callModel->getCallFromURI(otherMember).id;
} catch (...) {
conversation.callId = "";
}
}
// If conversation has only one peer it is possible that non swarm conversation was created.
// remove non swarm conversation
auto& peers = peersForConversation(conversation);
if (peers.size() == 1) {
try {
auto& participantId = peers.front();
auto& conv = getConversationForPeerUri(participantId).get();
if (conv.mode == conversation::Mode::NON_SWARM) {
eraseConversation(conv.uid);
invalidateModel();
Q_EMIT linked.conversationRemoved(conv.uid);
storage::removeContactConversations(db, participantId);
}
} catch (...) {
}
}
if (details["syncing"] == "true") {
conversation.needsSyncing = true;
Q_EMIT linked.conversationUpdated(conversation.uid);
Q_EMIT linked.dataChanged(indexOf(conversation.uid));
}
emplaceBackConversation(std::move(conversation));
auto id = ConfigurationManager::instance().loadConversationMessages(linked.owner.id,
convId,
"",
5);
}
void
ConversationModelPimpl::addConversationWith(const QString& convId, const QString& contactUri)
{
conversation::Info conversation;
conversation.uid = convId;
conversation.accountId = linked.owner.id;
conversation.participants = {contactUri};
conversation.mode = conversation::Mode::NON_SWARM;
conversation.needsSyncing = false;
try {
conversation.confId = linked.owner.callModel->getConferenceFromURI(contactUri).id;
} catch (...) {
conversation.confId = "";
}
try {
conversation.callId = linked.owner.callModel->getCallFromURI(contactUri).id;
} catch (...) {
conversation.callId = "";
}
storage::getHistory(db, conversation);
std::vector<std::function<void(void)>> updateSlots;
{
std::lock_guard<std::mutex> lk(interactionsLocks[convId]);
for (auto& interaction : conversation.interactions) {
if (interaction.second.status != interaction::Status::SENDING) {
continue;
}
// Get the message status from daemon, else unknown
auto id = storage::getDaemonIdByInteractionId(db, interaction.first);
int status = 0;
if (id.isEmpty()) {
continue;
}
try {
auto msgId = std::stoull(id.toStdString());
status = ConfigurationManager::instance().getMessageStatus(msgId);
updateSlots.emplace_back([this, convId, contactUri, id, status]() -> void {
auto accId = linked.owner.id;
slotUpdateInteractionStatus(accId, convId, contactUri, id, status);
});
} catch (const std::exception& e) {
qDebug() << "message id was invalid";
}
}
}
for (const auto& s : updateSlots) {
s();
}
conversation.unreadMessages = getNumberOfUnreadMessagesFor(convId);
emplaceBackConversation(std::move(conversation));
invalidateModel();
}
int
ConversationModelPimpl::indexOf(const QString& uid) const
{
for (unsigned int i = 0; i < conversations.size(); ++i) {
if (conversations.at(i).uid == uid)
return i;
}
return -1;
}
std::reference_wrapper<conversation::Info>
ConversationModelPimpl::getConversation(const FilterPredicate& pred,
const bool searchResultIncluded) const
{
auto conv = std::find_if(conversations.cbegin(), conversations.cend(), pred);
if (conv != conversations.cend()) {
return std::remove_const_t<conversation::Info&>(*conv);
}
if (searchResultIncluded) {
auto sr = std::find_if(searchResults.cbegin(), searchResults.cend(), pred);
if (sr != searchResults.cend()) {
return std::remove_const_t<conversation::Info&>(*sr);
}
}
throw std::out_of_range("Conversation out of range");
}
std::reference_wrapper<conversation::Info>
ConversationModelPimpl::getConversationForUid(const QString& uid,
const bool searchResultIncluded) const
{
return getConversation([uid](const conversation::Info& conv) -> bool { return uid == conv.uid; },
searchResultIncluded);
}
std::reference_wrapper<conversation::Info>
ConversationModelPimpl::getConversationForPeerUri(const QString& uri,
const bool searchResultIncluded) const
{
return getConversation(
[this, uri](const conversation::Info& conv) -> bool {
if (!conv.isCoreDialog()) {
return false;
}
auto members = peersForConversation(conv);
if (members.isEmpty())
return false;
return members.indexOf(uri) != -1;
},
searchResultIncluded);
}
int
ConversationModelPimpl::indexOfContact(const QString& uri) const
{
for (unsigned int i = 0; i < conversations.size(); ++i) {
if (!conversations.at(i).isCoreDialog()) {
continue;
}
auto peers = peersForConversation(conversations.at(i));
if (peers.isEmpty())
continue;
if (peers.front() == uri)
return i;
}
return -1;
}
void
ConversationModelPimpl::slotIncomingCall(const QString& fromId, const QString& callId)
{
auto convIds = storage::getConversationsWithPeer(db, fromId);
if (convIds.empty()) {
// in case if we receive call after removing contact add conversation request;
try {
auto contact = linked.owner.contactModel->getContact(fromId);
if (contact.profileInfo.type == profile::Type::PENDING && !contact.isBanned
&& fromId != linked.owner.profileInfo.uri) {
addContactRequest(fromId);
}
} catch (const std::out_of_range&) {
}
}
auto conversationIdx = indexOfContact(fromId);
if (conversationIdx == -1) {
qDebug() << "ConversationModelPimpl::slotIncomingCall, but conversation not found";
return; // Not a contact
}
auto& conversation = conversations.at(conversationIdx);
qDebug() << "Add call to conversation with " << fromId;
conversation.callId = callId;
addOrUpdateCallMessage(callId, fromId, true);
emit behaviorController.showIncomingCallView(linked.owner.id, conversation.uid);
}
void
ConversationModelPimpl::slotCallStatusChanged(const QString& callId, int code)
{
Q_UNUSED(code)
// Get conversation
auto i = std::find_if(conversations.begin(),
conversations.end(),
[callId](const conversation::Info& conversation) {
return conversation.callId == callId;
});
try {
auto call = linked.owner.callModel->getCall(callId);
if (i == conversations.end()) {
// In this case, the user didn't pass through placeCall
// This means that a participant was invited to a call
// or a call was placed via dbus.
// We have to update the model
for (auto& conversation : conversations) {
auto& peers = peersForConversation(conversation);
if (peers.size() != 1) {
continue;
}
if (peers.front() == call.peerUri) {
conversation.callId = callId;
// Update interaction status
invalidateModel();
emit linked.conversationUpdated(conversation.uid);
Q_EMIT linked.dataChanged(indexOf(conversation.uid));
}
}
} else if (i != conversations.end()) {
if (call.status == call::Status::PEER_BUSY) {
emit behaviorController.showLeaveMessageView(linked.owner.id, i->uid);
}
// Update interaction status
invalidateModel();
emit linked.conversationUpdated(i->uid);
Q_EMIT linked.dataChanged(indexOf(i->uid));
}
} catch (std::out_of_range& e) {
qDebug() << "ConversationModelPimpl::slotCallStatusChanged can't get inexistant call";
}
}
void
ConversationModelPimpl::slotCallStarted(const QString& callId)
{
try {
auto call = linked.owner.callModel->getCall(callId);
addOrUpdateCallMessage(callId, call.peerUri.remove("ring:"), !call.isOutgoing);
} catch (std::out_of_range& e) {
qDebug() << "ConversationModelPimpl::slotCallStarted can't start inexistant call";
}
}
void
ConversationModelPimpl::slotCallEnded(const QString& callId)
{
try {
auto call = linked.owner.callModel->getCall(callId);
// get duration
std::time_t duration = 0;
if (call.startTime.time_since_epoch().count() != 0) {
auto duration_ns = std::chrono::steady_clock::now() - call.startTime;
duration = std::chrono::duration_cast<std::chrono::seconds>(duration_ns).count();
}
// add or update call interaction with duration
addOrUpdateCallMessage(callId, call.peerUri.remove("ring:"), !call.isOutgoing, duration);
/* Reset the callId stored in the conversation.
Do not call selectConversation() since it is already done in slotCallStatusChanged. */
for (auto& conversation : conversations)
if (conversation.callId == callId) {
conversation.callId = "";
conversation.confId = ""; // The participant is detached
invalidateModel();
emit linked.conversationUpdated(conversation.uid);
Q_EMIT linked.dataChanged(indexOf(conversation.uid));
}
} catch (std::out_of_range& e) {
qDebug() << "ConversationModelPimpl::slotCallEnded can't end inexistant call";
}
}
void
ConversationModelPimpl::addOrUpdateCallMessage(const QString& callId,
const QString& from,
bool incoming,
const std::time_t& duration)
{
// do not save call interaction for swarm conversation
auto convIds = storage::getConversationsWithPeer(db, from);
if (convIds.empty()) {
// in case if we receive call after removing contact add conversation request;
try {
auto contact = linked.owner.contactModel->getContact(from);
if (contact.profileInfo.type == profile::Type::PENDING && !contact.isBanned) {
addContactRequest(from);
}
} catch (const std::out_of_range&) {
}
return;
}
// Get conversation
auto conv_it = std::find_if(conversations.begin(),
conversations.end(),
[&callId](const conversation::Info& conversation) {
return conversation.callId == callId;
});
if (conv_it == conversations.end()) {
return;
}
auto uid = conv_it->uid;
auto uriString = incoming ? storage::prepareUri(from, linked.owner.profileInfo.type) : "";
auto msg = interaction::Info {uriString,
{},
std::time(nullptr),
duration,
interaction::Type::CALL,
interaction::Status::SUCCESS,
true};
// update the db
auto msgId = storage::addOrUpdateMessage(db, conv_it->uid, msg, callId);
// now set the formatted call message string in memory only
msg.body = storage::getCallInteractionString(uriString, duration);
auto newInteraction = conv_it->interactions.find(msgId) == conv_it->interactions.end();
if (newInteraction) {
conv_it->lastMessageUid = msgId;
std::lock_guard<std::mutex> lk(interactionsLocks[conv_it->uid]);
conv_it->interactions.emplace(msgId, msg);
} else {
std::lock_guard<std::mutex> lk(interactionsLocks[conv_it->uid]);
conv_it->interactions[msgId] = msg;
}
if (newInteraction)
emit linked.newInteraction(conv_it->uid, msgId, msg);
else
emit linked.interactionStatusUpdated(conv_it->uid, msgId, msg);
invalidateModel();
emit linked.modelChanged();
Q_EMIT linked.dataChanged(static_cast<int>(std::distance(conversations.begin(), conv_it)));
}
void
ConversationModelPimpl::slotNewAccountMessage(const QString& accountId,
const QString& peerId,
const QString& msgId,
const MapStringString& payloads)
{
if (accountId != linked.owner.id)
return;
for (const auto& payload : payloads.keys()) {
if (payload.contains("text/plain")) {
addIncomingMessage(peerId, payloads.value(payload), 0, msgId);
}
}
}
void
ConversationModelPimpl::slotIncomingCallMessage(const QString& callId,
const QString& from,
const QString& body)
{
if (not linked.owner.callModel->hasCall(callId))
return;
auto& call = linked.owner.callModel->getCall(callId);
if (call.type == call::Type::CONFERENCE) {
// Show messages in all conversations for conferences.
for (const auto& conversation : conversations) {
if (conversation.confId == callId) {
if (conversation.participants.empty()) {
continue;
}
addIncomingMessage(from, body);
}
}
} else {
addIncomingMessage(from, body);
}
}
QString
ConversationModelPimpl::addIncomingMessage(const QString& peerId,
const QString& body,
const uint64_t& timestamp,
const QString& daemonId)
{
auto convIds = storage::getConversationsWithPeer(db, peerId);
if (convIds.empty()) {
// in case if we receive a message after removing contact, add a conversation request
try {
auto contact = linked.owner.contactModel->getContact(peerId);
if (contact.profileInfo.type == profile::Type::PENDING && !contact.isBanned
&& peerId != linked.owner.profileInfo.uri) {
addContactRequest(peerId);
}
} catch (const std::out_of_range&) {
}
return "";
}
auto msg = interaction::Info {peerId,
body,
timestamp == 0 ? std::time(nullptr)
: static_cast<time_t>(timestamp),
0,
interaction::Type::TEXT,
interaction::Status::SUCCESS,
false};
auto msgId = storage::addMessageToConversation(db, convIds[0], msg);
if (!daemonId.isEmpty()) {
storage::addDaemonMsgId(db, msgId, daemonId);
}
auto conversationIdx = indexOf(convIds[0]);
// Add the conversation if not already here
if (conversationIdx == -1) {
addConversationWith(convIds[0], peerId);
emit linked.newConversation(convIds[0]);
} else {
{
std::lock_guard<std::mutex> lk(interactionsLocks[conversations[conversationIdx].uid]);
conversations[conversationIdx].interactions.emplace(msgId, msg);
}
conversations[conversationIdx].lastMessageUid = msgId;
conversations[conversationIdx].unreadMessages = getNumberOfUnreadMessagesFor(convIds[0]);
}
emit behaviorController.newUnreadInteraction(linked.owner.id, convIds[0], msgId, msg);
emit linked.newInteraction(convIds[0], msgId, msg);
invalidateModel();
emit linked.modelChanged();
Q_EMIT linked.dataChanged(conversationIdx);
return msgId;
}
void
ConversationModelPimpl::slotCallAddedToConference(const QString& callId, const QString& confId)
{
for (auto& conversation : conversations) {
if (conversation.callId == callId && conversation.confId != confId) {
conversation.confId = confId;
invalidateModel();
// Refresh the conference status only if attached
MapStringString confDetails = CallManager::instance().getConferenceDetails(confId);
if (confDetails["STATE"] == "ACTIVE_ATTACHED")
emit linked.selectConversation(conversation.uid);
}
}
}
void
ConversationModelPimpl::slotUpdateInteractionStatus(const QString& accountId,
const QString& conversationId,
const QString& peerId,
const QString& messageId,
int status)
{
if (accountId != linked.owner.id) {
return;
}
// it may be not swarm conversation check in db
if (conversationId.isEmpty() || conversationId == linked.owner.profileInfo.uri) {
auto convIds = storage::getConversationsWithPeer(db, peerId);
if (convIds.empty()) {
return;
}
auto conversationIdx = indexOf(convIds[0]);
auto& conversation = conversations[conversationIdx];
auto newStatus = interaction::Status::INVALID;
switch (static_cast<DRing::Account::MessageStates>(status)) {
case DRing::Account::MessageStates::SENDING:
newStatus = interaction::Status::SENDING;
break;
case DRing::Account::MessageStates::CANCELLED:
newStatus = interaction::Status::TRANSFER_CANCELED;
break;
case DRing::Account::MessageStates::SENT:
newStatus = interaction::Status::SUCCESS;
break;
case DRing::Account::MessageStates::FAILURE:
newStatus = interaction::Status::FAILURE;
break;
case DRing::Account::MessageStates::DISPLAYED:
newStatus = interaction::Status::DISPLAYED;
break;
case DRing::Account::MessageStates::UNKNOWN:
default:
newStatus = interaction::Status::UNKNOWN;
break;
}
auto idString = messageId;
// for not swarm conversation messageId in hexdesimal string format. Convert to normal string
// TODO messageId should be received from daemon in string format
if (static_cast<DRing::Account::MessageStates>(status)
== DRing::Account::MessageStates::DISPLAYED) {
std::istringstream ss(messageId.toStdString());
ss >> std::hex;
uint64_t id;
if (!(ss >> id))
return;
idString = QString::number(id);
}
// Update database
auto interactionId = storage::getInteractionIdByDaemonId(db, idString);
if (interactionId.isEmpty()) {
return;
}
auto msgId = interactionId;
storage::updateInteractionStatus(db, msgId, newStatus);
// Update conversations
interaction::Info itCopy;
bool emitUpdated = false;
bool updateDisplayedUid = false;
QString oldDisplayedUid = 0;
{
std::lock_guard<std::mutex> lk(interactionsLocks[conversation.uid]);
auto& interactions = conversation.interactions;
auto it = interactions.find(msgId);
auto messageId = conversation.lastDisplayedMessageUid.find(peerId);
if (it != interactions.end()) {
it->second.status = newStatus;
bool interactionDisplayed = newStatus == interaction::Status::DISPLAYED
&& isOutgoing(it->second);
if (messageId != conversation.lastDisplayedMessageUid.end()) {
auto lastDisplayedIt = interactions.find(messageId->second);
bool interactionIsLast = lastDisplayedIt == interactions.end()
|| lastDisplayedIt->second.timestamp
< it->second.timestamp;
updateDisplayedUid = interactionDisplayed && interactionIsLast;
if (updateDisplayedUid) {
oldDisplayedUid = messageId->second;
conversation.lastDisplayedMessageUid.at(peerId) = it->first;
}
} else {
oldDisplayedUid = "";
conversation.lastDisplayedMessageUid[peerId] = it->first;
updateDisplayedUid = true;
}
emitUpdated = true;
itCopy = it->second;
}
}
if (updateDisplayedUid) {
emit linked.displayedInteractionChanged(conversation.uid,
peerId,
oldDisplayedUid,
msgId);
}
if (emitUpdated) {
invalidateModel();
emit linked.interactionStatusUpdated(conversation.uid, msgId, itCopy);
}
return;
}
try {
auto& conversation = getConversationForUid(conversationId).get();
if (conversation.mode != conversation::Mode::NON_SWARM) {
if (static_cast<DRing::Account::MessageStates>(status)
== DRing::Account::MessageStates::DISPLAYED) {
if (conversation.lastDisplayedMessageUid.find(peerId)
== conversation.lastDisplayedMessageUid.end()) {
conversation.lastDisplayedMessageUid[peerId] = messageId;
emit linked.displayedInteractionChanged(conversationId, peerId, "", messageId);
} else if (conversation.interactions.indexOfMessage(
conversation.lastDisplayedMessageUid.find(peerId)->second)
< conversation.interactions.indexOfMessage(messageId)) {
auto lastDisplayedMsg = conversation.lastDisplayedMessageUid.find(peerId)->second;
conversation.lastDisplayedMessageUid[peerId] = messageId;
emit linked.displayedInteractionChanged(conversationId,
peerId,
lastDisplayedMsg,
messageId);
}
}
}
} catch (const std::out_of_range& e) {
qDebug() << "could not update message status for not existing conversation";
}
}
void
ConversationModelPimpl::slotConferenceRemoved(const QString& confId)
{
// Get conversation
for (auto& i : conversations) {
if (i.confId == confId) {
i.confId = "";
}
}
}
void
ConversationModelPimpl::slotComposingStatusChanged(const QString& accountId,
const QString& convId,
const QString& contactUri,
bool isComposing)
{
if (accountId != linked.owner.id)
return;
emit linked.composingStatusChanged(convId, contactUri, isComposing);
}
int
ConversationModelPimpl::getNumberOfUnreadMessagesFor(const QString& uid)
{
return storage::countUnreadFromInteractions(db, uid);
}
void
ConversationModel::setIsComposing(const QString& convUid, bool isComposing)
{
try {
auto& conversation = pimpl_->getConversationForUid(convUid).get();
QString to = conversation.mode != conversation::Mode::NON_SWARM
? "swarm:" + convUid
: "jami:" + pimpl_->peersForConversation(conversation).front();
ConfigurationManager::instance().setIsComposing(owner.id, to, isComposing);
} catch (...) {
}
}
void
ConversationModel::sendFile(const QString& convUid, const QString& path, const QString& filename)
{
try {
auto& conversation = pimpl_->getConversationForUid(convUid, true).get();
auto peers = pimpl_->peersForConversation(conversation);
if (peers.size() < 1) {
qDebug() << "send file error: could not send file in conversation with no participants";
return;
}
/* isTemporary, and conversationReady callback used only for non-swarm conversation,
because for swarm, conversation already configured at this point.
Conversations for new contact from search result are NON_SWARM but after receiving
conversationReady callback could be updated to ONE_TO_ONE. We still use conversationReady
callback for one_to_one conversation to check if contact is blocked*/
if (peers.size() > 1) {
owner.dataTransferModel->sendFile(owner.id, "", convUid, path, filename);
}
const auto peerId = peers.front();
bool isTemporary = peerId == convUid;
/* It is necessary to make a copy of convUid since it may very well point to
a field in the temporary conversation, which is going to be destroyed by
slotContactAdded() (indirectly triggered by sendContactrequest(). Not doing
so may result in use after free/crash. */
auto convUidCopy = convUid;
pimpl_->sendContactRequest(peerId);
auto cb = ([this, peerId, path, filename](QString conversationId) {
try {
auto conversationOpt = getConversationForUid(conversationId);
if (!conversationOpt.has_value()) {
qDebug() << "Can't send file";
return;
}
auto contactInfo = owner.contactModel->getContact(peerId);
if (contactInfo.isBanned) {
qDebug() << "ContactModel::sendFile: denied, contact is banned";
return;
}
auto& conversation = conversationOpt->get();
// for non swarm conversation id should be empty, so peerId will be takin in consideration
auto id = conversation.mode != conversation::Mode::NON_SWARM ? conversationId : "";
owner.dataTransferModel->sendFile(owner.id, peerId, id, path, filename);
} catch (...) {
}
});
if (isTemporary) {
QMetaObject::Connection* const connection = new QMetaObject::Connection;
*connection = connect(this,
&ConversationModel::conversationReady,
[cb, connection, convUidCopy](QString conversationId,
QString participantId) {
if (participantId != convUidCopy) {
return;
}
cb(conversationId);
QObject::disconnect(*connection);
if (connection) {
delete connection;
}
});
} else {
cb(convUidCopy);
}
} catch (const std::out_of_range& e) {
qDebug() << "could not send file to not existing conversation";
}
}
void
ConversationModel::acceptTransfer(const QString& convUid, const QString& interactionId)
{
lrc::api::datatransfer::Info info = {};
getTransferInfo(convUid, interactionId, info);
acceptTransfer(convUid, interactionId, info.displayName);
}
void
ConversationModel::acceptTransfer(const QString& convUid,
const QString& interactionId,
const QString& path)
{
pimpl_->acceptTransfer(convUid, interactionId, path);
}
void
ConversationModel::cancelTransfer(const QString& convUid, const QString& fileId)
{
// For this action, we change interaction status before effective canceling as daemon will
// emit Finished event code immediately (before leaving this method) in non-DBus mode.
auto conversationIdx = pimpl_->indexOf(convUid);
interaction::Info itCopy;
bool emitUpdated = false;
if (conversationIdx != -1) {
std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[convUid]);
auto& interactions = pimpl_->conversations[conversationIdx].interactions;
auto it = interactions.find(fileId);
if (it != interactions.end()) {
it->second.status = interaction::Status::TRANSFER_CANCELED;
// update information in the db
storage::updateInteractionStatus(pimpl_->db,
fileId,
interaction::Status::TRANSFER_CANCELED);
emitUpdated = true;
itCopy = it->second;
}
}
if (emitUpdated) {
// for swarm conversations we need to provide conversation id to accept file, for not swarm
// conversations we need peer uri
lrc::api::datatransfer::Info info = {};
getTransferInfo(convUid, fileId, info);
// Forward cancel action to daemon (will invoke slotTransferStatusCanceled)
owner.dataTransferModel->cancel(owner.id, convUid, fileId);
pimpl_->invalidateModel();
emit interactionStatusUpdated(convUid, fileId, itCopy);
emit pimpl_->behaviorController.newReadInteraction(owner.id, convUid, fileId);
}
}
void
ConversationModel::getTransferInfo(const QString& conversationId,
const QString& interactionId,
datatransfer::Info& info)
{
auto convOpt = getConversationForUid(conversationId);
if (!convOpt)
return;
auto fileId = owner.dataTransferModel->getFileIdFromInteractionId(interactionId);
if (convOpt->get().mode == conversation::Mode::NON_SWARM) {
try {
owner.dataTransferModel->transferInfo(owner.id, fileId, info);
} catch (...) {
info.status = datatransfer::Status::INVALID;
}
} else {
QString path;
qlonglong bytesProgress, totalSize;
owner.dataTransferModel
->fileTransferInfo(owner.id, conversationId, fileId, path, totalSize, bytesProgress);
info.path = path;
info.totalSize = totalSize;
info.progress = bytesProgress;
}
}
int
ConversationModel::getNumberOfUnreadMessagesFor(const QString& convUid)
{
return pimpl_->getNumberOfUnreadMessagesFor(convUid);
}
bool
ConversationModelPimpl::usefulDataFromDataTransfer(const QString& fileId,
const datatransfer::Info& info,
QString& interactionId,
QString& conversationId)
{
if (info.accountId != linked.owner.id)
return false;
try {
interactionId = linked.owner.dataTransferModel->getInteractionIdFromFileId(fileId);
conversationId = info.conversationId.isEmpty()
? storage::conversationIdFromInteractionId(db, interactionId)
: info.conversationId;
} catch (const std::out_of_range& e) {
qWarning() << "Couldn't get interaction from daemon Id: " << fileId;
return false;
}
return true;
}
void
ConversationModelPimpl::slotTransferStatusCreated(const QString& fileId, datatransfer::Info info)
{
// check if transfer is for the current account
if (info.accountId != linked.owner.id)
return;
const MapStringString accountDetails = ConfigurationManager::instance().getAccountDetails(
linked.owner.id);
if (accountDetails.empty())
return;
// create a new conversation if needed
auto convIds = storage::getConversationsWithPeer(db, info.peerUri);
if (convIds.empty()) {
// in case if we receive file after removing contact add conversation request. If we have
// swarm request this function will do nothing.
try {
auto contact = linked.owner.contactModel->getContact(info.peerUri);
if (contact.profileInfo.type == profile::Type::PENDING && !contact.isBanned
&& info.peerUri != linked.owner.profileInfo.uri) {
addContactRequest(info.peerUri);
}
} catch (const std::out_of_range&) {
}
return;
}
// add interaction to the db
const auto& convId = convIds[0];
auto interactionId = storage::addDataTransferToConversation(db, convId, info);
// map fileId and interactionId for latter retrivial from client (that only known the interactionId)
linked.owner.dataTransferModel->registerTransferId(fileId, interactionId);
auto interaction = interaction::Info {info.isOutgoing ? "" : info.peerUri,
info.isOutgoing ? info.path : info.displayName,
std::time(nullptr),
0,
interaction::Type::DATA_TRANSFER,
interaction::Status::TRANSFER_CREATED,
false};
// prepare interaction Info and emit signal for the client
auto conversationIdx = indexOf(convId);
if (conversationIdx == -1) {
addConversationWith(convId, info.peerUri);
emit linked.newConversation(convId);
} else {
{
std::lock_guard<std::mutex> lk(interactionsLocks[conversations[conversationIdx].uid]);
conversations[conversationIdx].interactions.emplace(interactionId, interaction);
}
conversations[conversationIdx].lastMessageUid = interactionId;
conversations[conversationIdx].unreadMessages = getNumberOfUnreadMessagesFor(convId);
}
emit behaviorController.newUnreadInteraction(linked.owner.id,
convId,
interactionId,
interaction);
emit linked.newInteraction(convId, interactionId, interaction);
invalidateModel();
emit linked.modelChanged();
Q_EMIT linked.dataChanged(conversationIdx);
}
void
ConversationModelPimpl::slotTransferStatusAwaitingPeer(const QString& fileId,
datatransfer::Info info)
{
if (info.accountId != linked.owner.id)
return;
bool intUpdated;
updateTransferStatus(fileId, info, interaction::Status::TRANSFER_AWAITING_PEER, intUpdated);
}
void
ConversationModelPimpl::slotTransferStatusAwaitingHost(const QString& fileId,
datatransfer::Info info)
{
if (info.accountId != linked.owner.id)
return;
awaitingHost(fileId, info);
}
bool
ConversationModelPimpl::hasOneOneSwarmWith(const QString& participant)
{
try {
auto& conversation = getConversationForPeerUri(participant).get();
return conversation.mode == conversation::Mode::ONE_TO_ONE;
} catch (std::out_of_range&) {
return false;
}
}
void
ConversationModelPimpl::awaitingHost(const QString& fileId, datatransfer::Info info)
{
if (info.accountId != linked.owner.id)
return;
QString interactionId;
QString conversationId;
if (not usefulDataFromDataTransfer(fileId, info, interactionId, conversationId))
return;
bool intUpdated;
if (!updateTransferStatus(fileId,
info,
interaction::Status::TRANSFER_AWAITING_HOST,
intUpdated)) {
return;
}
if (!intUpdated) {
return;
}
auto conversationIdx = indexOf(conversationId);
auto& peers = peersForConversation(conversations[conversationIdx]);
// Only accept if contact is added or it is a group conversation
if (linked.owner.accountModel->autoTransferFromUntrusted && peers.size() == 1) {
try {
auto contactUri = peers.front();
auto contactInfo = linked.owner.contactModel->getContact(contactUri);
if (contactInfo.profileInfo.type == profile::Type::PENDING)
return;
} catch (...) {
return;
}
}
handleIncomingFile(conversationId, interactionId, info.displayName, info.totalSize);
}
void
ConversationModelPimpl::handleIncomingFile(const QString& convId,
const QString& interactionId,
const QString& displayName,
int totalSize)
{
// If it's an accepted file type and less than 20 MB, accept transfer.
if (linked.owner.accountModel->autoTransferFromTrusted) {
if (linked.owner.accountModel->autoTransferSizeThreshold == 0
|| (totalSize > 0
&& static_cast<unsigned>(totalSize)
< linked.owner.accountModel->autoTransferSizeThreshold * 1024 * 1024)) {
acceptTransfer(convId, interactionId, displayName);
}
}
}
void
ConversationModelPimpl::acceptTransfer(const QString& convUid,
const QString& interactionId,
const QString& path)
{
auto& conversation = getConversationForUid(convUid).get();
if (conversation.isLegacy()) {
// This is a fallback, will be removed when swarm will be mandatory
auto destinationDir = linked.owner.accountModel->downloadDirectory;
if (destinationDir.isEmpty()) {
return;
}
#ifdef Q_OS_WIN
if (destinationDir.right(1) != '/') {
destinationDir += "/";
}
#endif
QDir dir = QFileInfo(destinationDir + path).absoluteDir();
if (!dir.exists())
dir.mkpath(".");
auto acceptedFilePath = linked.owner.dataTransferModel->accept(linked.owner.id,
interactionId,
destinationDir + path);
auto fileId = linked.owner.dataTransferModel->getFileIdFromInteractionId(interactionId);
if (transfIdToDbIntId.find(fileId) != transfIdToDbIntId.end()) {
auto dbInteractionId = transfIdToDbIntId[fileId];
storage::updateInteractionBody(db, dbInteractionId, acceptedFilePath);
storage::updateInteractionStatus(db,
dbInteractionId,
interaction::Status::TRANSFER_ACCEPTED);
} else {
storage::updateInteractionBody(db, interactionId, acceptedFilePath);
storage::updateInteractionStatus(db,
interactionId,
interaction::Status::TRANSFER_ACCEPTED);
}
// prepare interaction Info and emit signal for the client
auto conversationIdx = indexOf(convUid);
interaction::Info itCopy;
bool emitUpdated = false;
if (conversationIdx != -1) {
std::lock_guard<std::mutex> lk(interactionsLocks[convUid]);
auto& interactions = conversations[conversationIdx].interactions;
auto it = interactions.find(interactionId);
if (it != interactions.end()) {
it->second.body = acceptedFilePath;
it->second.status = interaction::Status::TRANSFER_ACCEPTED;
emitUpdated = true;
itCopy = it->second;
}
}
if (emitUpdated) {
if (conversations[conversationIdx].isCoreDialog()) {
sendContactRequest(peersForConversation(conversations[conversationIdx]).front());
}
invalidateModel();
emit linked.interactionStatusUpdated(convUid, interactionId, itCopy);
emit behaviorController.newReadInteraction(linked.owner.id, convUid, interactionId);
}
return;
}
auto interaction = conversation.interactions.find(interactionId);
if (interaction != conversation.interactions.end()) {
auto fileId = interaction->second.commit["fileId"];
if (fileId.isEmpty()) {
qWarning() << "Cannot download file without fileId";
return;
}
linked.owner.dataTransferModel->download(linked.owner.id, convUid, interactionId, fileId);
} else {
qWarning() << "Cannot download file without valid interaction";
}
}
void
ConversationModelPimpl::invalidateModel()
{
filteredConversations.invalidate();
customFilteredConversations.invalidate();
}
void
ConversationModelPimpl::emplaceBackConversation(conversation::Info&& conversation)
{
Q_EMIT linked.beginInsertRows(conversations.size());
conversations.emplace_back(std::move(conversation));
Q_EMIT linked.endInsertRows();
}
void
ConversationModelPimpl::eraseConversation(const QString& convId)
{
eraseConversation(indexOf(convId));
}
void
ConversationModelPimpl::eraseConversation(int index)
{
Q_EMIT linked.beginRemoveRows(index);
conversations.erase(conversations.begin() + index);
Q_EMIT linked.endRemoveRows();
}
void
ConversationModelPimpl::slotTransferStatusOngoing(const QString& fileId, datatransfer::Info info)
{
if (info.accountId != linked.owner.id)
return;
QString interactionId;
QString conversationId;
if (not usefulDataFromDataTransfer(fileId, info, interactionId, conversationId))
return;
bool intUpdated;
if (!updateTransferStatus(fileId, info, interaction::Status::TRANSFER_ONGOING, intUpdated)) {
return;
}
if (!intUpdated) {
return;
}
auto conversationIdx = indexOf(conversationId);
auto* timer = new QTimer();
connect(timer, &QTimer::timeout, [=] {
updateTransfer(timer, conversationId, conversationIdx, interactionId);
});
timer->start(1000);
}
void
ConversationModelPimpl::slotTransferStatusFinished(const QString& fileId, datatransfer::Info info)
{
if (info.accountId != linked.owner.id)
return;
QString interactionId;
QString conversationId;
if (not usefulDataFromDataTransfer(fileId, info, interactionId, conversationId))
return;
// prepare interaction Info and emit signal for the client
auto conversationIdx = indexOf(conversationId);
if (conversationIdx != -1) {
bool emitUpdated = false;
auto newStatus = interaction::Status::TRANSFER_FINISHED;
interaction::Info itCopy;
{
std::lock_guard<std::mutex> lk(interactionsLocks[conversationId]);
auto& interactions = conversations[conversationIdx].interactions;
auto it = interactions.find(interactionId);
if (it != interactions.end()) {
// We need to check if current status is ONGOING as CANCELED must not be
// transformed into FINISHED
if (it->second.status == interaction::Status::TRANSFER_ONGOING) {
emitUpdated = true;
it->second.status = newStatus;
itCopy = it->second;
}
}
}
if (emitUpdated) {
invalidateModel();
if (conversations[conversationIdx].mode != conversation::Mode::NON_SWARM) {
if (transfIdToDbIntId.find(fileId) != transfIdToDbIntId.end()) {
auto dbIntId = transfIdToDbIntId[fileId];
storage::updateInteractionStatus(db, dbIntId, newStatus);
}
} else {
storage::updateInteractionStatus(db, interactionId, newStatus);
}
emit linked.interactionStatusUpdated(conversationId, interactionId, itCopy);
transfIdToDbIntId.remove(fileId);
}
}
}
void
ConversationModelPimpl::slotTransferStatusCanceled(const QString& fileId, datatransfer::Info info)
{
if (info.accountId != linked.owner.id)
return;
bool intUpdated;
updateTransferStatus(fileId, info, interaction::Status::TRANSFER_CANCELED, intUpdated);
}
void
ConversationModelPimpl::slotTransferStatusError(const QString& fileId, datatransfer::Info info)
{
if (info.accountId != linked.owner.id)
return;
bool intUpdated;
updateTransferStatus(fileId, info, interaction::Status::TRANSFER_ERROR, intUpdated);
}
void
ConversationModelPimpl::slotTransferStatusUnjoinable(const QString& fileId, datatransfer::Info info)
{
if (info.accountId != linked.owner.id)
return;
bool intUpdated;
updateTransferStatus(fileId, info, interaction::Status::TRANSFER_UNJOINABLE_PEER, intUpdated);
}
void
ConversationModelPimpl::slotTransferStatusTimeoutExpired(const QString& fileId,
datatransfer::Info info)
{
if (info.accountId != linked.owner.id)
return;
bool intUpdated;
updateTransferStatus(fileId, info, interaction::Status::TRANSFER_TIMEOUT_EXPIRED, intUpdated);
}
bool
ConversationModelPimpl::updateTransferStatus(const QString& fileId,
datatransfer::Info info,
interaction::Status newStatus,
bool& updated)
{
QString interactionId;
QString conversationId;
if (not usefulDataFromDataTransfer(fileId, info, interactionId, conversationId)) {
return false;
}
auto conversationIdx = indexOf(conversationId);
if (conversationIdx < 0) {
return false;
}
auto& conversation = conversations[conversationIdx];
if (conversation.isLegacy()) {
storage::updateInteractionStatus(db, interactionId, newStatus);
}
bool emitUpdated = false;
interaction::Info itCopy;
{
std::lock_guard<std::mutex> lk(interactionsLocks[conversationId]);
auto& interactions = conversations[conversationIdx].interactions;
auto it = interactions.find(interactionId);
if (it != interactions.end()) {
emitUpdated = true;
it->second.status = newStatus;
if (conversation.isSwarm()) {
it->second.body = info.path;
}
itCopy = it->second;
}
}
if (emitUpdated) {
invalidateModel();
emit linked.interactionStatusUpdated(conversationId, interactionId, itCopy);
}
updated = emitUpdated;
return true;
}
void
ConversationModelPimpl::updateTransfer(QTimer* timer,
const QString& conversation,
int conversationIdx,
const QString& interactionId)
{
try {
bool emitUpdated = false;
interaction::Info itCopy;
{
std::lock_guard<std::mutex> lk(interactionsLocks[conversations[conversationIdx].uid]);
const auto& interactions = conversations[conversationIdx].interactions;
const auto& it = interactions.find(interactionId);
if (it != interactions.cend()
and it->second.status == interaction::Status::TRANSFER_ONGOING) {
emitUpdated = true;
itCopy = it->second;
}
}
if (emitUpdated) {
emit linked.interactionStatusUpdated(conversation, interactionId, itCopy);
return;
}
} catch (...) {
}
timer->stop();
timer->deleteLater();
}
} // namespace lrc
#include "api/moc_conversationmodel.cpp"
#include "conversationmodel.moc"