-
Andreas Traczyk authored
Change-Id: I5e3f66be4aa559105f5b80acd0e422499d9aa2f6 Reviewed-by:
Hugo Lefeuvre <hugo.lefeuvre@savoirfairelinux.com>
Andreas Traczyk authoredChange-Id: I5e3f66be4aa559105f5b80acd0e422499d9aa2f6 Reviewed-by:
Hugo Lefeuvre <hugo.lefeuvre@savoirfairelinux.com>
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
conversationmodel.cpp 76.96 KiB
/****************************************************************************
* Copyright (C) 2017-2018 Savoir-faire Linux *
* Author: Nicolas Jäger <nicolas.jager@savoirfairelinux.com> *
* Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> *
* Author: Guillaume Roguez <guillaume.roguez@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"
//Qt
#include <QtCore/QTimer>
#include <QFileInfo>
// daemon
#include <account_const.h>
#include <datatransfer_interface.h>
// std
#include <algorithm>
#include <mutex>
#include <regex>
// 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 "authority/databasehelper.h"
#include "availableaccountmodel.h"
#include "namedirectory.h"
#include "phonedirectorymodel.h"
#include "contactmethod.h"
// Dbus
#include "dbus/configurationmanager.h"
#include "dbus/callmanager.h"
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();
/**
* 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 std::string& uid) 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 std::string& uri) const;
/**
* Initialize conversations_ and filteredConversations_
*/
void initConversations();
/**
* Sort conversation by last action
*/
void sortConversations();
/**
* Call contactModel.addContact if necessary
* @param contactUri
*/
void sendContactRequest(const std::string& contactUri);
/**
* Add a conversation with contactUri
* @param convId
* @param contactUri
*/
void addConversationWith(const std::string& convId, const std::string& contactUri);
/**
* Add call interaction for conversation with callId
* @param callId
* @param body
*/
void addOrUpdateCallMessage(const std::string& callId, const std::string& body);
/**
* Add a new message from a peer in the database
* @param from the peer uri
* @param body the content of the message
* @param authorProfileId override the author of the message (if empty it's from)*/
void addIncomingMessage(const std::string& from,
const std::string& body,
const std::string& authorProfileId="");
/**
* Change the status of an interaction. Listen from callbacksHandler
* @param accountId, account linked
* @param id, interaction to update
* @param to, peer uri
* @param status, new status for this interaction
*/
void slotUpdateInteractionStatus(const std::string& accountId,
const uint64_t id,
const std::string& to, 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 std::string& uid, bool isAudioOnly = false);
/**
* get number of unread messages
*/
int getNumberOfUnreadMessagesFor(const std::string& uid);
/**
* Handle data transfer progression
*/
void updateTransfer(QTimer* timer, const std::string& conversation, int conversationIdx,
int interactionId);
bool usefulDataFromDataTransfer(long long dringId, const datatransfer::Info& info,
int& interactionId, std::string& convId);
/**
* accept a file transfer
* @param convUid
* @param interactionId
* @param final name of the file
*/
void acceptTransfer(const std::string& convUid, uint64_t interactionId, const std::string& path);
const ConversationModel& linked;
Lrc& lrc;
Database& db;
const CallbacksHandler& callbacksHandler;
const std::string accountProfileId;
const BehaviorController& behaviorController;
ConversationModel::ConversationQueue conversations; ///< non-filtered conversations
ConversationModel::ConversationQueue filteredConversations;
ConversationModel::ConversationQueue customFilteredConversations;
std::string filter;
profile::Type typeFilter;
profile::Type customTypeFilter;
std::pair<bool, bool> dirtyConversations {true, true}; ///< true if filteredConversations/customFilteredConversations must be regenerated
std::map<std::string, std::mutex> interactionsLocks; ///< {convId, mutex}
public Q_SLOTS:
/**
* Listen from contactModel when updated (like new alias, avatar, etc.)
*/
void slotContactModelUpdated(const std::string& uri, bool needsSorted);
/**
* Listen from contactModel when a new contact is added
* @param uri
*/
void slotContactAdded(const std::string& uri);
/**
* Listen from contactModel when a pending contact is accepted
* @param uri
*/
void slotPendingContactAccepted(const std::string& uri);
/**
* Listen from contactModel when aa new contact is removed
* @param uri
*/
void slotContactRemoved(const std::string& uri);
/**
* Listen from callmodel for new calls.
* @param fromId caller uri
* @param callId
*/
void slotIncomingCall(const std::string& fromId, const std::string& callId);
/**
* Listen from callmodel for calls status changed.
* @param callId
*/
void slotCallStatusChanged(const std::string& callId);
/**
* Listen from callmodel for writing "Call started"
* @param callId
*/
void slotCallStarted(const std::string& callId);
/**
* Listen from callmodel for writing "Call ended"
* @param callId
*/
void slotCallEnded(const std::string& callId);
/**
* Listen from CallbacksHandler for new incoming interactions;
* @param accountId
* @param from uri
* @param payloads body
*/
void slotNewAccountMessage(std::string& accountId,
std::string& from,
std::map<std::string,std::string> 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 std::string& callId, const std::string& from, const std::string& body);
/**
* Listen from CallModel when a call is added to a conference
* @param callId
* @param confId
*/
void slotCallAddedToConference(const std::string& callId, const std::string& confId);
/**
* Listen from CallbacksHandler when a conference is deleted.
* @param confId
*/
void slotConferenceRemoved(const std::string& confId);
void slotTransferStatusCreated(long long dringId, api::datatransfer::Info info);
void slotTransferStatusCanceled(long long dringId, api::datatransfer::Info info);
void slotTransferStatusAwaitingPeer(long long dringId, api::datatransfer::Info info);
void slotTransferStatusAwaitingHost(long long dringId, api::datatransfer::Info info);
void slotTransferStatusOngoing(long long dringId, api::datatransfer::Info info);
void slotTransferStatusFinished(long long dringId, api::datatransfer::Info info);
void slotTransferStatusError(long long dringId, api::datatransfer::Info info);
void slotTransferStatusTimeoutExpired(long long dringId, api::datatransfer::Info info);
void slotTransferStatusUnjoinable(long long dringId, api::datatransfer::Info info);
void updateTransferStatus(long long dringId, api::datatransfer::Info info, interaction::Status newStatus);
};
ConversationModel::ConversationModel(const account::Info& owner,
Lrc& lrc,
Database& db,
const CallbacksHandler& callbacksHandler,
const BehaviorController& behaviorController)
: QObject()
, pimpl_(std::make_unique<ConversationModelPimpl>(*this, lrc, db, callbacksHandler, behaviorController))
, owner(owner)
{
}
ConversationModel::~ConversationModel()
{
}
const ConversationModel::ConversationQueue&
ConversationModel::allFilteredConversations() const
{
if (!pimpl_->dirtyConversations.first)
return pimpl_->filteredConversations;
pimpl_->filteredConversations = pimpl_->conversations;
auto it = std::copy_if(
pimpl_->conversations.begin(), pimpl_->conversations.end(),
pimpl_->filteredConversations.begin(),
[this] (const conversation::Info& entry) {
auto contactInfo = owner.contactModel->getContact(entry.participants.front());
/* Check contact */
// If contact is banned, only match if filter is a perfect match
if (contactInfo.isBanned) {
if (pimpl_->filter == "") return false;
return contactInfo.profileInfo.uri == pimpl_->filter
|| contactInfo.profileInfo.alias == pimpl_->filter
|| contactInfo.registeredName == pimpl_->filter;
}
/* Check type */
if (pimpl_->typeFilter != profile::Type::PENDING) {
// Remove pending contacts and get the temporary item if filter is not empty
if (contactInfo.profileInfo.type == profile::Type::PENDING)
return false;
if (contactInfo.profileInfo.type == profile::Type::TEMPORARY)
return not contactInfo.profileInfo.alias.empty() || not contactInfo.profileInfo.uri.empty();
} else {
// We only want pending requests matching with the filter
if (contactInfo.profileInfo.type != profile::Type::PENDING)
return false;
}
// Otherwise perform usual regex search
try {
auto regexFilter = std::regex(pimpl_->filter, std::regex_constants::icase);
bool result = std::regex_search(contactInfo.profileInfo.uri, regexFilter)
| std::regex_search(contactInfo.profileInfo.alias, regexFilter)
| std::regex_search(contactInfo.registeredName, regexFilter);
return result;
} catch(std::regex_error&) {
// If the regex is incorrect, just test if filter is a substring
// of the uri or the alias.
return contactInfo.profileInfo.alias.find(pimpl_->filter) != std::string::npos
&& contactInfo.profileInfo.uri.find(pimpl_->filter) != std::string::npos
&& contactInfo.registeredName.find(pimpl_->filter) != std::string::npos;
}
});
pimpl_->filteredConversations.resize(std::distance(pimpl_->filteredConversations.begin(), it));
pimpl_->dirtyConversations.first = false;
return pimpl_->filteredConversations;
}
const ConversationModel::ConversationQueue&
ConversationModel::getFilteredConversations(const profile::Type& filter, bool forceUpdate, const bool includeBanned) const
{
if (pimpl_->customTypeFilter == filter && !pimpl_->dirtyConversations.second && !forceUpdate)
return pimpl_->customFilteredConversations;
pimpl_->customTypeFilter = filter;
pimpl_->customFilteredConversations = pimpl_->conversations;
auto it = std::copy_if(
pimpl_->conversations.begin(), pimpl_->conversations.end(),
pimpl_->customFilteredConversations.begin(),
[this, &includeBanned] (const conversation::Info& entry) {
auto contactInfo = owner.contactModel->getContact(entry.participants.front());
if (!includeBanned && contactInfo.isBanned) return false;
return (contactInfo.profileInfo.type == pimpl_->customTypeFilter);
});
pimpl_->customFilteredConversations.resize(std::distance(pimpl_->customFilteredConversations.begin(), it));
pimpl_->dirtyConversations.second = false;
return pimpl_->customFilteredConversations;
}
conversation::Info
ConversationModel::filteredConversation(const unsigned int row) const
{
const auto& conversations = allFilteredConversations();
if (row >= conversations.size())
return conversation::Info();
auto conversationInfo = conversations.at(row);
conversationInfo.unreadMessages = pimpl_->getNumberOfUnreadMessagesFor(conversationInfo.uid);
return conversationInfo;
}
void
ConversationModel::makePermanent(const std::string& uid)
{
auto conversationIdx = pimpl_->indexOf(uid);
if (conversationIdx == -1)
return;
auto& conversation = pimpl_->conversations.at(conversationIdx);
if (conversation.participants.empty()) {
// Should not
qDebug() << "ConversationModel::addConversation can't add a conversation with no participant";
return;
}
// Send contact request if non used
pimpl_->sendContactRequest(conversation.participants.front());
}
void
ConversationModel::selectConversation(const std::string& uid) const
{
// Get conversation
auto conversationIdx = pimpl_->indexOf(uid);
if (conversationIdx == -1)
return;
if (uid.empty() && owner.contactModel->getContact("").profileInfo.uri.empty()) {
// if we select the temporary contact, check if its a valid contact.
return;
}
auto& conversation = pimpl_->conversations.at(conversationIdx);
try {
if (not conversation.confId.empty()) {
emit pimpl_->behaviorController.showCallView(owner.id, conversation);
} else {
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);
break;
case call::Status::PAUSED:
case call::Status::PEER_PAUSED:
case call::Status::CONNECTED:
case call::Status::IN_PROGRESS:
// We are currently receiving a call
emit pimpl_->behaviorController.showCallView(owner.id, conversation);
break;
case call::Status::INVALID:
case call::Status::OUTGOING_REQUESTED:
case call::Status::INACTIVE:
case call::Status::ENDED:
case call::Status::TERMINATING:
case call::Status::AUTO_ANSWERING:
default:
// We are not in a call, show the chatview
emit pimpl_->behaviorController.showChatView(owner.id, conversation);
}
}
} catch (const std::out_of_range&) {
emit pimpl_->behaviorController.showChatView(owner.id, conversation);
}
}
void
ConversationModel::removeConversation(const std::string& 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;
}
// Remove contact from daemon
// NOTE: this will also remove the conversation into the database.
for (const auto& participant: conversation.participants)
owner.contactModel->removeContact(participant, 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);
database::deleteObsoleteHistory(pimpl_->db, date);
}
void
ConversationModelPimpl::placeCall(const std::string& uid, bool isAudioOnly)
{
auto conversationIdx = indexOf(uid);
if (conversationIdx == -1)
return;
auto& conversation = conversations.at(conversationIdx);
if (conversation.participants.empty()) {
// Should not
qDebug() << "ConversationModel::placeCall can't call a conversation without participant";
return;
}
// Disallow multiple call
if (!conversation.callId.empty()) {
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::PEER_PAUSED:
case call::Status::CONNECTED:
case call::Status::IN_PROGRESS:
case call::Status::OUTGOING_REQUESTED:
case call::Status::AUTO_ANSWERING:
return;
case call::Status::INVALID:
case call::Status::INACTIVE:
case call::Status::ENDED:
case call::Status::TERMINATING:
default:
break;
}
} catch (const std::out_of_range&) {
}
}
auto convId = uid;
auto accountId = accountProfileId;
auto participant = conversation.participants.front();
bool isTemporary = participant.empty();
auto contactInfo = linked.owner.contactModel->getContact(participant);
auto url = contactInfo.profileInfo.uri;
if (url.empty())
return; // Incorrect item
// Don't call banned contact
if (contactInfo.isBanned) {
qDebug() << "ContactModel::placeCall: denied, contact is banned";
return;
}
sendContactRequest(participant);
if (linked.owner.profileInfo.type != profile::Type::SIP) {
url = "ring:" + url; // Add the ring: before or it will fail.
}
// If call is with temporary contact, conversation has been removed and must be updated
int contactIndex;
if (isTemporary && (contactIndex = indexOfContact(convId)) < 0) {
qDebug() << "Can't place call: Other participant is not a contact";
return;
}
auto& newConv = isTemporary ? conversations.at(contactIndex) : conversation;
convId = newConv.uid;
newConv.callId = linked.owner.callModel->createCall(url, isAudioOnly);
dirtyConversations = {true, true};
emit behaviorController.showIncomingCallView(linked.owner.id, newConv);
}
void
ConversationModel::placeAudioOnlyCall(const std::string& uid)
{
pimpl_->placeCall(uid, true);
}
void
ConversationModel::placeCall(const std::string& uid)
{
pimpl_->placeCall(uid);
}
void
ConversationModel::sendMessage(const std::string& uid, const std::string& body)
{
auto conversationIdx = pimpl_->indexOf(uid);
if (conversationIdx == -1)
return;
auto& conversation = pimpl_->conversations.at(conversationIdx);
if (conversation.participants.empty()) {
// Should not
qDebug() << "ConversationModel::sendMessage can't send a interaction to a conversation with no participant";
return;
}
auto convId = uid;
auto accountId = pimpl_->accountProfileId;
bool isTemporary = conversation.participants.front() == "";
// Send interaction to all participants
// NOTE: conferences are not implemented yet, so we have only one participant
uint64_t daemonMsgId = 0;
auto status = interaction::Status::SENDING;
for (const auto& participant: conversation.participants) {
auto contactInfo = owner.contactModel->getContact(participant);
if (contactInfo.isBanned) {
qDebug() << "ContactModel::sendMessage: denied, contact is banned";
return;
}
pimpl_->sendContactRequest(participant);
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 conversation.callId.empty() and not callLists.contains(conversation.callId.c_str()))
conversation.callId.clear();
if (not conversation.callId.empty()
and call::canSendSIPMessage(owner.callModel->getCall(conversation.callId))) {
status = interaction::Status::UNKNOWN;
owner.callModel->sendSipMessage(conversation.callId, body);
} else
daemonMsgId = owner.contactModel->sendDhtMessage(contactInfo.profileInfo.uri, body);
}
// If first interaction with temporary contact, we have to update the conversations info
// at this stage
int contactIndex;
if (isTemporary && (contactIndex = pimpl_->indexOfContact(convId)) < 0) {
qDebug() << "Can't send message: Other participant is not a contact";
return;
}
auto& newConv = isTemporary ? pimpl_->conversations.at(contactIndex) : conversation;
convId = newConv.uid;
// Add interaction to database
auto msg = interaction::Info {accountId, body, std::time(nullptr),
interaction::Type::TEXT, status};
int msgId = database::addMessageToConversation(pimpl_->db, accountId, convId, msg);
// Update conversation
if (status == interaction::Status::SENDING) {
// Because the daemon already give an id for the message, we need to store it.
database::addDaemonMsgId(pimpl_->db, std::to_string(msgId), std::to_string(daemonMsgId));
}
{
std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[convId]);
newConv.interactions.insert(std::pair<uint64_t, interaction::Info>(msgId, msg));
}
newConv.lastMessageUid = msgId;
pimpl_->dirtyConversations = {true, true};
// Emit this signal for chatview in the client
emit newInteraction(convId, msgId, msg);
// This conversation is now at the top of the list
pimpl_->sortConversations();
// The order has changed, informs the client to redraw the list
emit modelSorted();
}
void
ConversationModel::refreshFilter()
{
pimpl_->dirtyConversations = {true, true};
emit filterChanged();
}
void
ConversationModel::setFilter(const std::string& filter)
{
pimpl_->filter = filter;
pimpl_->dirtyConversations = {true, true};
// Will update the temporary contact in the contactModel
owner.contactModel->searchContact(filter);
emit filterChanged();
}
void
ConversationModel::setFilter(const profile::Type& filter)
{
// Switch between PENDING, RING and SIP contacts.
pimpl_->typeFilter = filter;
pimpl_->dirtyConversations = {true, true};
emit filterChanged();
}
void
ConversationModel::joinConversations(const std::string& uidA, const std::string& uidB)
{
auto conversationAIdx = pimpl_->indexOf(uidA);
auto conversationBIdx = pimpl_->indexOf(uidB);
if (conversationAIdx == -1 || conversationBIdx == -1)
return;
auto& conversationA = pimpl_->conversations[conversationAIdx];
auto& conversationB = pimpl_->conversations[conversationBIdx];
if (conversationA.callId.empty() || conversationB.callId.empty())
return;
if (conversationA.confId.empty()) {
if(conversationB.confId.empty()){
owner.callModel->joinCalls(conversationA.callId, conversationB.callId);
}else{
owner.callModel->joinCalls(conversationA.callId, conversationB.confId);
conversationA.confId = conversationB.confId;
}
} else {
if(conversationB.confId.empty()){
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 std::string& uid)
{
auto conversationIdx = pimpl_->indexOf(uid);
if (conversationIdx == -1)
return;
auto& conversation = pimpl_->conversations.at(conversationIdx);
// Remove all TEXT interactions from database
database::clearHistory(pimpl_->db, uid);
// Update conversation
{
std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[uid]);
conversation.interactions.clear();
}
database::getHistory(pimpl_->db, conversation); // will contains "Conversation started"
pimpl_->sortConversations();
emit modelSorted();
emit conversationCleared(uid);
}
void
ConversationModel::clearInteractionFromConversation(const std::string& convId, const uint64_t& interactionId)
{
auto conversationIdx = pimpl_->indexOf(convId);
if (conversationIdx == -1)
return;
auto erased_keys = 0;
bool lastInteractionUpdated = false;
{
std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[convId]);
try
{
auto& conversation = pimpl_->conversations.at(conversationIdx);
database::clearInteractionFromConversation(pimpl_->db, convId, interactionId);
erased_keys = conversation.interactions.erase(interactionId);
if (conversation.lastMessageUid == interactionId) {
// Update lastMessageUid
auto newLastId = 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 (erased_keys > 0) {
pimpl_->dirtyConversations.first = true;
emit interactionRemoved(convId, interactionId);
}
if (lastInteractionUpdated) {
// last interaction as changed, so the order can changes.
pimpl_->sortConversations();
emit modelSorted();
}
}
void
ConversationModel::retryInteraction(const std::string& convId, const uint64_t& interactionId)
{
auto conversationIdx = pimpl_->indexOf(convId);
if (conversationIdx == -1)
return;
auto interactionType = interaction::Type::INVALID;
auto body = std::string();
{
std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[convId]);
try {
auto& conversation = pimpl_->conversations.at(conversationIdx);
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::OUTGOING_DATA_TRANSFER) {
body = it->second.body;
interactionType = it->second.type;
} else
return;
database::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.c_str());
sendFile(convId, body, f.fileName().toStdString());
}
}
void
ConversationModel::clearAllHistory()
{
database::clearAllHistoryFor(pimpl_->db, owner.profileInfo.uri);
for (auto& conversation : pimpl_->conversations) {
{
std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[conversation.uid]);
conversation.interactions.clear();
}
database::getHistory(pimpl_->db, conversation);
}
}
void
ConversationModel::setInteractionRead(const std::string& convId,
const uint64_t& interactionId)
{
auto conversationIdx = pimpl_->indexOf(convId);
if (conversationIdx == -1) {
return;
}
bool emitUpdated = false;
interaction::Info itCopy;
auto newStatus = interaction::Status::READ;
{
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.status != interaction::Status::UNREAD) return;
it->second.status = newStatus;
itCopy = it->second;
}
}
if (emitUpdated) {
pimpl_->dirtyConversations = {true, true};
database::updateInteractionStatus(pimpl_->db, interactionId, newStatus);
emit interactionStatusUpdated(convId, interactionId, itCopy);
}
}
void
ConversationModel::clearUnreadInteractions(const std::string& convId) {
auto conversationIdx = pimpl_->indexOf(convId);
if (conversationIdx == -1) {
return;
}
bool emitUpdated = false;
{
std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[convId]);
auto& interactions = pimpl_->conversations[conversationIdx].interactions;
std::for_each(interactions.begin(), interactions.end(),
[&] (decltype(*interactions.begin())& it) {
if (it.second.type == lrc::api::interaction::Type::TEXT &&
it.second.status == lrc::api::interaction::Status::UNREAD) {
emitUpdated = true;
it.second.status = interaction::Status::READ;
database::updateInteractionStatus(pimpl_->db, it.first, interaction::Status::READ);
}
});
}
if (emitUpdated) {
pimpl_->dirtyConversations = {true, true};
emit conversationUpdated(convId);
}
}
ConversationModelPimpl::ConversationModelPimpl(const ConversationModel& linked,
Lrc& lrc,
Database& db,
const CallbacksHandler& callbacksHandler,
const BehaviorController& behaviorController)
: linked(linked)
, lrc {lrc}
, db(db)
, callbacksHandler(callbacksHandler)
, typeFilter(profile::Type::INVALID)
, customTypeFilter(profile::Type::INVALID)
, accountProfileId(database::getProfileId(db, linked.owner.profileInfo.uri))
, behaviorController(behaviorController)
{
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::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.callModel, &NewCallModel::newIncomingCall,
this, &ConversationModelPimpl::slotIncomingCall);
connect(&*linked.owner.contactModel, &ContactModel::incomingCallFromPending,
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);
// 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);
}
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::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.callModel, &NewCallModel::newIncomingCall,
this, &ConversationModelPimpl::slotIncomingCall);
disconnect(&*linked.owner.contactModel, &ContactModel::incomingCallFromPending,
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);
// 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);
}
void
ConversationModelPimpl::initConversations()
{
auto* account = AccountModel::instance().getById(linked.owner.id.c_str());
if (!account)
return;
// Fill conversations
if (accountProfileId.empty()) {
// Should not, NewAccountModel must create this profile before.
qDebug() << "ConversationModelPimpl::initConversations(), account not in db";
return;
}
for (auto const& c : linked.owner.contactModel->getAllContacts())
{
if(linked.owner.profileInfo.uri == c.second.profileInfo.uri)
continue;
auto contactProfileId = database::getProfileId(db, c.second.profileInfo.uri);
if (contactProfileId.empty()) {
// Should not, ContactModel must create profiles before.
qDebug() << "ConversationModelPimpl::initConversations(), contact not in db";
continue;
}
auto common = database::getConversationsBetween(db, accountProfileId, contactProfileId);
if (common.empty()) {
// Can't find a conversation with this contact. Start it.
auto newConversationsId = database::beginConversationsBetween(db, accountProfileId, contactProfileId);
common.emplace_back(std::move(newConversationsId));
}
addConversationWith(common[0], c.first);
auto convIdx = indexOf(common[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
database::updateInteractionStatus(db, interaction.first, interaction::Status::TRANSFER_ERROR);
interaction.second.status = interaction::Status::TRANSFER_ERROR;
}
}
}
sortConversations();
filteredConversations = conversations;
dirtyConversations.first = false;
}
void
ConversationModelPimpl::sortConversations()
{
std::sort(
conversations.begin(), conversations.end(),
[this](const auto& conversationA, const auto& conversationB)
{
// A or B is a temporary contact
if (conversationA.participants.empty()) return true;
if (conversationB.participants.empty()) return false;
if (conversationA.uid == conversationB.uid) return false;
auto& mtxA = interactionsLocks[conversationA.uid];
auto& mtxB = interactionsLocks[conversationB.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 = conversationA.interactions;
auto historyB = conversationB.interactions;
// A or B is a new conversation (without CONTACT interaction)
if (conversationA.uid.empty() || conversationB.uid.empty()) return conversationA.uid.empty();
if (historyA.empty()) return false;
if (historyB.empty()) return true;
// Sort by last Interaction
try
{
auto lastMessageA = historyA.at(conversationA.lastMessageUid);
auto lastMessageB = historyB.at(conversationB.lastMessageUid);
return lastMessageA.timestamp > lastMessageB.timestamp;
}
catch (const std::exception& e)
{
qDebug() << "ConversationModel::sortConversations(), can't get lastMessage";
return false;
}
});
dirtyConversations = {true, true};
}
void
ConversationModelPimpl::sendContactRequest(const std::string& 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::slotContactAdded(const std::string& uri)
{
auto contactProfileId = database::getOrInsertProfile(db, uri);
auto conv = database::getConversationsBetween(db, accountProfileId, contactProfileId);
if (conv.empty()) {
std::string interaction = "";
try {
auto contact = linked.owner.contactModel->getContact(uri);
interaction = contact.profileInfo.type == profile::Type::PENDING ?
QObject::tr("Invitation received").toStdString() :
QObject::tr("Contact added").toStdString();
} catch (...) {}
// pass conversation UID through only element
conv.emplace_back(
database::beginConversationsBetween(db, accountProfileId,
contactProfileId, interaction
)
);
}
// Add the conversation if not already here
if (indexOf(conv[0]) == -1) {
addConversationWith(conv[0], uri);
emit linked.newConversation(conv[0]);
}
// delete temporary conversation if it exists and it has the uri of the added contact as uid
if (indexOf(uri) >= 0) {
conversations.erase(conversations.begin() + indexOf(uri));
}
sortConversations();
emit linked.modelSorted();
}
void
ConversationModelPimpl::slotPendingContactAccepted(const std::string& uri)
{
auto contactProfileId = database::getOrInsertProfile(db, uri);
auto conv = database::getConversationsBetween(db, accountProfileId, contactProfileId);
if (conv.empty()) {
conv.emplace_back(
database::beginConversationsBetween(db, accountProfileId,
contactProfileId, QObject::tr("Invitation accepted").toStdString()
)
);
} else {
try {
auto contact = linked.owner.contactModel->getContact(uri);
auto msg = interaction::Info {accountProfileId,
QObject::tr("Invitation accepted").toStdString(),
std::time(nullptr), interaction::Type::CONTACT,
interaction::Status::SUCCEED};
auto msgId = database::addMessageToConversation(db, accountProfileId, conv[0], msg);
auto convIdx = indexOf(conv[0]);
{
std::lock_guard<std::mutex> lk(interactionsLocks[conversations[convIdx].uid]);
conversations[convIdx].interactions.emplace(msgId, msg);
}
dirtyConversations = {true, true};
emit linked.newInteraction(conv[0], msgId, msg);
} catch (std::out_of_range& e) {
qDebug() << "ConversationModelPimpl::slotContactAdded can't find contact";
}
}
}
void
ConversationModelPimpl::slotContactRemoved(const std::string& uri)
{
auto conversationIdx = indexOfContact(uri);
if (conversationIdx == -1) {
qDebug() << "ConversationModelPimpl::slotContactRemoved, but conversation not found";
return; // Not a contact
}
auto& conversationUid = conversations[conversationIdx].uid;
conversations.erase(conversations.begin() + conversationIdx);
dirtyConversations = {true, true};
emit linked.conversationRemoved(conversationUid);
emit linked.modelSorted();
}
void
ConversationModelPimpl::slotContactModelUpdated(const std::string& uri, bool needsSorted)
{
// We don't create newConversationItem if we already filter on pending
conversation::Info newConversationItem;
if (!filter.empty()) {
// Create a conversation with the temporary item
conversation::Info conversationInfo;
auto& temporaryContact = linked.owner.contactModel->getContact("");
conversationInfo.uid = temporaryContact.profileInfo.uri;
conversationInfo.participants.emplace_back("");
conversationInfo.accountId = linked.owner.id;
// if temporary contact is already present, its alias is not empty (namely "Searching ..."),
// or its registeredName is set because it was found on the nameservice.
if (not temporaryContact.profileInfo.alias.empty() || not temporaryContact.registeredName.empty()) {
if (!conversations.empty()) {
auto firstContactUri = conversations.front().participants.front();
//if first conversation has uri it is already a contact
// then we must add temporary item
if (not firstContactUri.empty()) {
conversations.emplace_front(conversationInfo);
} else if (not conversationInfo.uid.empty()) {
// If firstContactUri is empty it means that we have to update
// this element as it is the temporary.
// Only when we have found an uri.
conversations.front() = conversationInfo;
} else if (not conversations.front().uid.empty()) {
//update conversation when uri not found
//but conversation have uri from previous search
conversations.front() = conversationInfo;
}
} else {
// no conversation, add temporaryItem
conversations.emplace_front(conversationInfo);
}
dirtyConversations = {true, true};
if (needsSorted) {
emit linked.modelSorted();
} else {
emit linked.conversationUpdated(conversations.front().uid);
}
return;
}
} else {
// No filter, so we can remove the newConversationItem
if (!conversations.empty()) {
auto firstContactUri = conversations.front().participants.front();
if (firstContactUri.empty() && needsSorted) {
conversations.pop_front();
dirtyConversations = {true, true};
emit linked.modelSorted();
return;
}
}
}
// trigger dirtyConversation in all cases to flush emptied temporary element due to filtered contact present in list
// TL:DR : avoid duplicates and empty elements
dirtyConversations = {true, true};
int index = indexOfContact(uri);
if (index != -1) {
if (!conversations.empty() && conversations.front().participants.front().empty() &&
needsSorted) {
// In this case, contact is present in list, so temporary item does not longer exists
emit linked.modelSorted();
} else {
// In this case, a presence is updated
emit linked.conversationUpdated(conversations.at(index).uid);
}
}
}
void
ConversationModelPimpl::addConversationWith(const std::string& convId,
const std::string& contactUri)
{
conversation::Info conversation;
conversation.uid = convId;
conversation.accountId = linked.owner.id;
conversation.participants = {contactUri};
try {
conversation.callId = linked.owner.callModel->getCallFromURI(contactUri).id;
} catch (...) {
conversation.callId = "";
}
database::getHistory(db, conversation);
std::vector<std::function<void(void)>> slotLambdas;
{
std::lock_guard<std::mutex> lk(interactionsLocks[convId]);
for (auto& interaction: conversation.interactions) {
if (interaction.second.status == interaction::Status::SENDING) {
// Get the message status from daemon, else unknown
auto id = database::getDaemonIdByInteractionId(db, std::to_string(interaction.first));
int status = 0;
if (!id.empty()) {
auto msgId = std::stoull(id);
status = ConfigurationManager::instance().getMessageStatus(msgId);
}
slotLambdas.emplace_back([=]() -> void {
slotUpdateInteractionStatus(linked.owner.id, std::stoull(id), contactUri, status);
});
}
}
}
for (const auto& l: slotLambdas) { l(); }
conversation.unreadMessages = getNumberOfUnreadMessagesFor(convId);
conversations.emplace_back(conversation);
dirtyConversations = {true, true};
}
int
ConversationModelPimpl::indexOf(const std::string& uid) const
{
for (unsigned int i = 0; i < conversations.size(); ++i) {
if (conversations.at(i).uid == uid) return i;
}
return -1;
}
int
ConversationModelPimpl::indexOfContact(const std::string& uri) const
{
for (unsigned int i = 0; i < conversations.size(); ++i) {
if (conversations.at(i).participants.front() == uri) return i;
}
return -1;
}
void
ConversationModelPimpl::slotIncomingCall(const std::string& fromId, const std::string& callId)
{
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.c_str();
conversation.callId = callId;
dirtyConversations = {true, true};
emit behaviorController.showIncomingCallView(linked.owner.id, conversation);
}
void
ConversationModelPimpl::slotCallStatusChanged(const std::string& callId)
{
// Get conversation
auto i = std::find_if(
conversations.begin(), conversations.end(),
[callId](const conversation::Info& conversation) {
return conversation.callId == callId;
});
if (i == conversations.end()) return;
auto& conversation = *i;
auto uid = conversation.uid;
linked.selectConversation(uid);
}
void
ConversationModelPimpl::slotCallStarted(const std::string& callId)
{
try {
auto call = linked.owner.callModel->getCall(callId);
if (call.isOutgoing)
addOrUpdateCallMessage(callId, QObject::tr("📞 Outgoing call").toStdString());
else
addOrUpdateCallMessage(callId, QObject::tr("📞 Incoming call").toStdString());
} catch (std::out_of_range& e) {
qDebug() << "ConversationModelPimpl::slotCallStarted can't start inexistant call";
}
}
void
ConversationModelPimpl::slotCallEnded(const std::string& callId)
{
try {
auto call = linked.owner.callModel->getCall(callId);
if (call.startTime.time_since_epoch().count() != 0) {
if (call.isOutgoing)
addOrUpdateCallMessage(callId, QObject::tr("📞 Outgoing call - ").toStdString()
+ linked.owner.callModel->getFormattedCallDuration(callId));
else
addOrUpdateCallMessage(callId, QObject::tr("📞 Incoming call - ").toStdString()
+ linked.owner.callModel->getFormattedCallDuration(callId));
} else {
if (call.isOutgoing)
addOrUpdateCallMessage(callId, QObject::tr("🕽 Missed outgoing call").toStdString());
else
addOrUpdateCallMessage(callId, QObject::tr("🕽 Missed incoming call").toStdString());
}
// reset the callId stored in the conversation
for (auto& conversation: conversations)
if (conversation.callId == callId) {
try {
conversation.callId = linked.owner.callModel->getCallFromURI(conversation.participants[0], true).id;
} catch (std::out_of_range& e) {
conversation.callId = "";
}
dirtyConversations = {true, true};
linked.selectConversation(conversation.uid);
}
} catch (std::out_of_range& e) {
qDebug() << "ConversationModelPimpl::slotCallEnded can't end inexistant call";
}
}
void
ConversationModelPimpl::addOrUpdateCallMessage(const std::string& callId, const std::string& body)
{
// Get conversation
for (auto& conversation: conversations) {
if (conversation.callId == callId) {
auto uid = conversation.uid;
auto msg = interaction::Info {accountProfileId, body, std::time(nullptr),
interaction::Type::CALL, interaction::Status::SUCCEED};
int msgId = database::addOrUpdateMessage(db, accountProfileId, conversation.uid, msg, callId);
auto newInteraction = conversation.interactions.find(msgId) == conversation.interactions.end();
if (newInteraction) {
conversation.lastMessageUid = msgId;
std::lock_guard<std::mutex> lk(interactionsLocks[conversation.uid]);
conversation.interactions.emplace(msgId, msg);
} else {
std::lock_guard<std::mutex> lk(interactionsLocks[conversation.uid]);
conversation.interactions[msgId] = msg;
}
dirtyConversations = {true, true};
if (newInteraction)
emit linked.newInteraction(conversation.uid, msgId, msg);
else
emit linked.interactionStatusUpdated(conversation.uid, msgId, msg);
sortConversations();
emit linked.modelSorted();
}
}
}
void
ConversationModelPimpl::slotNewAccountMessage(std::string& accountId,
std::string& from,
std::map<std::string,std::string> payloads)
{
if (accountId != linked.owner.id)
return;
addIncomingMessage(from, payloads["text/plain"]);
}
void
ConversationModelPimpl::slotIncomingCallMessage(const std::string& callId, const std::string& from, const std::string& 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;
auto authorProfileId = database::getOrInsertProfile(db, from);
addIncomingMessage(conversation.participants.front(), body, authorProfileId);
}
}
} else {
addIncomingMessage(from, body);
}
}
void
ConversationModelPimpl::addIncomingMessage(const std::string& from,
const std::string& body,
const std::string& authorProfileId)
{
auto contactProfileId = database::getOrInsertProfile(db, from);
auto accountProfileId = database::getProfileId(db, linked.owner.profileInfo.uri);
auto conv = database::getConversationsBetween(db, accountProfileId, contactProfileId);
if (conv.empty()) {
conv.emplace_back(database::beginConversationsBetween(
db, accountProfileId, contactProfileId,
QObject::tr("Invitation received").toStdString()
));
}
auto authorId = authorProfileId.empty()? contactProfileId: authorProfileId;
auto msg = interaction::Info {authorId, body, std::time(nullptr),
interaction::Type::TEXT, interaction::Status::UNREAD};
int msgId = database::addMessageToConversation(db, accountProfileId, conv[0], msg);
auto conversationIdx = indexOf(conv[0]);
// Add the conversation if not already here
if (conversationIdx == -1) {
addConversationWith(conv[0], from);
emit linked.newConversation(conv[0]);
} else {
{
std::lock_guard<std::mutex> lk(interactionsLocks[conversations[conversationIdx].uid]);
conversations[conversationIdx].interactions.emplace(msgId, msg);
}
conversations[conversationIdx].lastMessageUid = msgId;
}
dirtyConversations = {true, true};
emit linked.newInteraction(conv[0], msgId, msg);
sortConversations();
emit linked.modelSorted();
}
void
ConversationModelPimpl::slotCallAddedToConference(const std::string& callId, const std::string& confId)
{
for (auto& conversation: conversations) {
if (conversation.callId == callId) {
conversation.confId = confId;
dirtyConversations = {true, true};
emit linked.selectConversation(conversation.uid);
}
}
}
void
ConversationModelPimpl::slotUpdateInteractionStatus(const std::string& accountId,
const uint64_t id,
const std::string& to,
int status)
{
if (accountId != linked.owner.id) return;
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::SENT:
newStatus = interaction::Status::SUCCEED;
break;
case DRing::Account::MessageStates::FAILURE:
newStatus = interaction::Status::FAILED;
break;
case DRing::Account::MessageStates::READ:
newStatus = interaction::Status::READ;
break;
case DRing::Account::MessageStates::UNKNOWN:
default:
newStatus = interaction::Status::UNKNOWN;
break;
}
// Update database
auto interactionId = database::getInteractionIdByDaemonId(db, std::to_string(id));
if (interactionId.empty()) return;
auto msgId = std::stoull(interactionId);
database::updateInteractionStatus(db, msgId, newStatus);
// Update conversations
auto contactProfileId = database::getProfileId(db, to);
auto accountProfileId = database::getProfileId(db, linked.owner.profileInfo.uri);
auto conv = database::getConversationsBetween(db, accountProfileId, contactProfileId);
if (!conv.empty()) {
auto conversationIdx = indexOf(conv[0]);
interaction::Info itCopy;
bool emitUpdated = false;
if (conversationIdx != -1) {
std::lock_guard<std::mutex> lk(interactionsLocks[conversations[conversationIdx].uid]);
auto& interactions = conversations[conversationIdx].interactions;
auto it = interactions.find(msgId);
if (it != interactions.end()) {
it->second.status = newStatus;
emitUpdated = true;
itCopy = it->second;
}
}
if (emitUpdated)
emit linked.interactionStatusUpdated(conv[0], msgId, itCopy);
}
}
void
ConversationModelPimpl::slotConferenceRemoved(const std::string& confId)
{
// Get conversation
for(auto& i : conversations){
if (i.confId == confId)
i.confId = "";
}
}
int
ConversationModelPimpl::getNumberOfUnreadMessagesFor(const std::string& uid)
{
return database::countUnreadFromInteractions(db, uid);
}
void
ConversationModel::sendFile(const std::string& convUid,
const std::string& path,
const std::string& filename)
{
auto conversationIdx = pimpl_->indexOf(convUid);
if (conversationIdx == -1)
return;
const auto& peerUri = pimpl_->conversations[conversationIdx].participants.front();
if (peerUri.empty())
return;
pimpl_->sendContactRequest(peerUri);
pimpl_->lrc.getDataTransferModel().sendFile(owner.id.c_str(),
peerUri.c_str(),
path.c_str(),
filename.c_str());
}
void
ConversationModel::acceptTransfer(const std::string& convUid, uint64_t interactionId, const std::string& path)
{
pimpl_->acceptTransfer(convUid, interactionId, path);
}
void
ConversationModel::cancelTransfer(const std::string& convUid, uint64_t interactionId)
{
// For this action, we change interaction status before effective canceling as daemon will
// emit Finished event code immediatly (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(interactionId);
if (it != interactions.end()) {
it->second.status = interaction::Status::TRANSFER_CANCELED;
// update information in the db
database::updateInteractionStatus(pimpl_->db, interactionId, interaction::Status::TRANSFER_CANCELED);
emitUpdated = true;
itCopy = it->second;
}
}
if (emitUpdated) {
// Forward cancel action to daemon (will invoke slotTransferStatusCanceled)
pimpl_->lrc.getDataTransferModel().cancel(interactionId);
pimpl_->dirtyConversations = {true, true};
emit interactionStatusUpdated(convUid, interactionId, itCopy);
}
}
void
ConversationModel::getTransferInfo(uint64_t interactionId, datatransfer::Info& info)
{
try {
auto dringId = pimpl_->lrc.getDataTransferModel().getDringIdFromInteractionId(interactionId);
pimpl_->lrc.getDataTransferModel().transferInfo(dringId, info);
} catch (...) {
info.status = datatransfer::Status::INVALID;
}
}
int
ConversationModel::getNumberOfUnreadMessagesFor(const std::string& convUid)
{
return pimpl_->getNumberOfUnreadMessagesFor(convUid);
}
bool
ConversationModelPimpl::usefulDataFromDataTransfer(long long dringId, const datatransfer::Info& info,
int& interactionId, std::string& convId)
{
try {
interactionId = lrc.getDataTransferModel().getInteractionIdFromDringId(dringId);
} catch (...) {
return false;
}
convId = database::conversationIdFromInteractionId(db, interactionId);
return true;
}
void
ConversationModelPimpl::slotTransferStatusCreated(long long dringId, datatransfer::Info info)
{
// check if transfer is for the current account
if (info.accountId != linked.owner.id) return;
const auto* account = AccountModel::instance().getById(info.accountId.c_str());
if (not account)
return;
auto contactProfileId = database::getOrInsertProfile(db, info.peerUri);
auto accountProfileId = database::getProfileId(db, linked.owner.profileInfo.uri);
// create a new conversation if needed
auto conversation_list = database::getConversationsBetween(db, accountProfileId, contactProfileId);
if (conversation_list.empty()) {
conversation_list.emplace_back(database::beginConversationsBetween(
db, accountProfileId, contactProfileId,
QObject::tr("Invitation received").toStdString()));
}
// add interaction to the db
const auto& convId = conversation_list[0];
auto interactionId = database::addDataTransferToConversation(db, accountProfileId, convId, info);
// map dringId and interactionId for latter retrivial from client (that only known the interactionId)
lrc.getDataTransferModel().registerTransferId(dringId, interactionId);
auto interactioType = info.isOutgoing ?
interaction::Type::OUTGOING_DATA_TRANSFER :
interaction::Type::INCOMING_DATA_TRANSFER;
auto interaction = interaction::Info {info.isOutgoing? accountProfileId : contactProfileId,
info.isOutgoing? info.path : info.displayName,
std::time(nullptr),
interactioType,
interaction::Status::TRANSFER_CREATED};
// 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;
}
dirtyConversations = {true, true};
emit linked.newInteraction(convId, interactionId, interaction);
sortConversations();
emit linked.modelSorted();
}
void
ConversationModelPimpl::slotTransferStatusAwaitingPeer(long long dringId, datatransfer::Info info)
{
updateTransferStatus(dringId, info, interaction::Status::TRANSFER_AWAITING_PEER);
}
void
ConversationModelPimpl::slotTransferStatusAwaitingHost(long long dringId, datatransfer::Info info)
{
int interactionId;
std::string convId;
if (not usefulDataFromDataTransfer(dringId, info, interactionId, convId))
return;
auto newStatus = interaction::Status::TRANSFER_AWAITING_HOST;
database::updateInteractionStatus(db, interactionId, newStatus);
auto conversationIdx = indexOf(convId);
if (conversationIdx != -1) {
bool emitUpdated = false;
interaction::Info itCopy;
{
std::lock_guard<std::mutex> lk(interactionsLocks[convId]);
auto& interactions = conversations[conversationIdx].interactions;
auto it = interactions.find(interactionId);
if (it != interactions.end()) {
emitUpdated = true;
it->second.status = newStatus;
itCopy = it->second;
}
}
if (emitUpdated) {
dirtyConversations = {true, true};
emit linked.interactionStatusUpdated(convId, interactionId, itCopy);
// If it's an image < 20 Mb, accept transfer.
auto extensionIdx = info.displayName.find_last_of(".");
if (extensionIdx == std::string::npos) return;
auto extension = info.displayName.substr(extensionIdx);
std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);
auto imageExtensions = {".gif", ".jpg", ".jpeg", ".png"};
auto isImage = std::find(imageExtensions.begin(), imageExtensions.end(), extension) != imageExtensions.end();
auto destinationDir = lrc.getDataTransferModel().downloadDirectory;
if (info.totalSize < 20 * 1024 * 1024 && isImage && !destinationDir.empty()) {
acceptTransfer(convId, interactionId, destinationDir + info.displayName);
}
}
}
}
void
ConversationModelPimpl::acceptTransfer(const std::string& convUid, uint64_t interactionId, const std::string& path)
{
lrc.getDataTransferModel().accept(interactionId, path, 0);
database::updateInteractionBody(db, interactionId, path);
database::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 = path;
it->second.status = interaction::Status::TRANSFER_ACCEPTED;
emitUpdated = true;
itCopy = it->second;
}
}
if (emitUpdated) {
sendContactRequest(conversations[conversationIdx].participants.front());
dirtyConversations = {true, true};
emit linked.interactionStatusUpdated(convUid, interactionId, itCopy);
}
}
void
ConversationModelPimpl::slotTransferStatusOngoing(long long dringId, datatransfer::Info info)
{
int interactionId;
std::string convId;
if (not usefulDataFromDataTransfer(dringId, info, interactionId, convId))
return;
auto newStatus = interaction::Status::TRANSFER_ONGOING;
database::updateInteractionStatus(db, interactionId, newStatus);
auto conversationIdx = indexOf(convId);
if (conversationIdx != -1) {
bool emitUpdated = false;
interaction::Info itCopy;
{
std::lock_guard<std::mutex> lk(interactionsLocks[convId]);
auto& interactions = conversations[conversationIdx].interactions;
auto it = interactions.find(interactionId);
if (it != interactions.end()) {
emitUpdated = true;
it->second.status = newStatus;
itCopy = it->second;
}
}
if (emitUpdated) {
auto* timer = new QTimer();
connect(timer, &QTimer::timeout,
[=] { updateTransfer(timer, convId, conversationIdx, interactionId); });
timer->start(1000);
dirtyConversations = {true, true};
emit linked.interactionStatusUpdated(convId, interactionId, itCopy);
}
}
}
void
ConversationModelPimpl::slotTransferStatusFinished(long long dringId, datatransfer::Info info)
{
int interactionId;
std::string convId;
if (not usefulDataFromDataTransfer(dringId, info, interactionId, convId))
return;
// prepare interaction Info and emit signal for the client
auto conversationIdx = indexOf(convId);
if (conversationIdx != -1) {
bool emitUpdated = false;
auto newStatus = interaction::Status::TRANSFER_FINISHED;
interaction::Info itCopy;
{
std::lock_guard<std::mutex> lk(interactionsLocks[convId]);
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) {
dirtyConversations = {true, true};
database::updateInteractionStatus(db, interactionId, newStatus);
emit linked.interactionStatusUpdated(convId, interactionId, itCopy);
}
}
}
void
ConversationModelPimpl::slotTransferStatusCanceled(long long dringId, datatransfer::Info info)
{
updateTransferStatus(dringId, info, interaction::Status::TRANSFER_CANCELED);
}
void
ConversationModelPimpl::slotTransferStatusError(long long dringId, datatransfer::Info info)
{
updateTransferStatus(dringId, info, interaction::Status::TRANSFER_ERROR);
}
void
ConversationModelPimpl::slotTransferStatusUnjoinable(long long dringId, datatransfer::Info info)
{
updateTransferStatus(dringId, info, interaction::Status::TRANSFER_UNJOINABLE_PEER);
}
void
ConversationModelPimpl::slotTransferStatusTimeoutExpired(long long dringId, datatransfer::Info info)
{
updateTransferStatus(dringId, info, interaction::Status::TRANSFER_TIMEOUT_EXPIRED);
}
void
ConversationModelPimpl::updateTransferStatus(long long dringId, datatransfer::Info info, interaction::Status newStatus)
{
int interactionId;
std::string convId;
if (not usefulDataFromDataTransfer(dringId, info, interactionId, convId))
return;
// update information in the db
database::updateInteractionStatus(db, interactionId, newStatus);
// prepare interaction Info and emit signal for the client
auto conversationIdx = indexOf(convId);
if (conversationIdx != -1) {
bool emitUpdated = false;
interaction::Info itCopy;
{
std::lock_guard<std::mutex> lk(interactionsLocks[convId]);
auto& interactions = conversations[conversationIdx].interactions;
auto it = interactions.find(interactionId);
if (it != interactions.end()) {
emitUpdated = true;
it->second.status = newStatus;
itCopy = it->second;
}
}
if (emitUpdated) {
dirtyConversations = {true, true};
emit linked.interactionStatusUpdated(convId, interactionId, itCopy);
}
}
}
void
ConversationModelPimpl::updateTransfer(QTimer* timer, const std::string& conversation,
int conversationIdx, int 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 != std::cend(interactions)
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();
delete timer;
}
} // namespace lrc
#include "api/moc_conversationmodel.cpp"
#include "conversationmodel.moc"