conversationmodel.cpp 43.1 KB
Newer Older
Nicolas Jager's avatar
Nicolas Jager committed
1 2
/****************************************************************************
 *   Copyright (C) 2017 Savoir-faire Linux                                  *
3 4 5
 *   Author: Nicolas Jäger <nicolas.jager@savoirfairelinux.com>             *
 *   Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>           *
 *   Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com>       *
Nicolas Jager's avatar
Nicolas Jager committed
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
 *                                                                          *
 *   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"

22 23 24
// daemon
#include <account_const.h>

Nicolas Jager's avatar
Nicolas Jager committed
25 26
// std
#include <regex>
27
#include <algorithm>
Nicolas Jager's avatar
Nicolas Jager committed
28 29

// LRC
30
#include "api/behaviorcontroller.h"
31
#include "api/contactmodel.h"
Nicolas Jager's avatar
Nicolas Jager committed
32 33 34
#include "api/newcallmodel.h"
#include "api/newaccountmodel.h"
#include "api/account.h"
35 36 37
#include "api/call.h"
#include "callbackshandler.h"
#include "authority/databasehelper.h"
Nicolas Jager's avatar
Nicolas Jager committed
38

39
#include "availableaccountmodel.h"
Nicolas Jager's avatar
Nicolas Jager committed
40
#include "namedirectory.h"
41 42 43 44 45
#include "phonedirectorymodel.h"
#include "contactmethod.h"

// Dbus
#include "dbus/configurationmanager.h"
46
#include "dbus/callmanager.h"
Nicolas Jager's avatar
Nicolas Jager committed
47 48 49 50

namespace lrc
{

51
using namespace authority;
Nicolas Jager's avatar
Nicolas Jager committed
52 53 54 55 56 57
using namespace api;

class ConversationModelPimpl : public QObject
{
    Q_OBJECT
public:
58 59
    ConversationModelPimpl(const ConversationModel& linked,
                           Database& db,
60 61
                           const CallbacksHandler& callbacksHandler,
                           const BehaviorController& behaviorController);
62

Nicolas Jager's avatar
Nicolas Jager committed
63 64 65
    ~ConversationModelPimpl();

    /**
66 67 68 69 70 71 72 73 74
     * 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.
Nicolas Jager's avatar
Nicolas Jager committed
75
     */
76
    int indexOfContact(const std::string& uri) const;
Nicolas Jager's avatar
Nicolas Jager committed
77 78 79 80 81 82 83 84
    /**
     * Initialize conversations_ and filteredConversations_
     */
    void initConversations();
    /**
     * Sort conversation by last action
     */
    void sortConversations();
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
    /**
     * 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
     */
101
    void addOrUpdateCallMessage(const std::string& callId, const std::string& body);
102 103 104 105
    /**
     * Add a new message from a peer in the database
     * @param from the peer uri
     * @param body the content of the message
106 107 108 109
     * @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="");
110 111 112 113 114 115 116 117 118 119
    /**
     * 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);
Nicolas Jager's avatar
Nicolas Jager committed
120

121
    const ConversationModel& linked;
122 123 124
    Database& db;
    const CallbacksHandler& callbacksHandler;
    const std::string accountProfileId;
125
    const BehaviorController& behaviorController;
Nicolas Jager's avatar
Nicolas Jager committed
126

127 128
    ConversationModel::ConversationQueue conversations; ///< non-filtered conversations
    ConversationModel::ConversationQueue filteredConversations;
Nicolas Jager's avatar
Nicolas Jager committed
129
    std::string filter;
130 131
    profile::Type typeFilter;
    bool dirtyConversations {true}; ///< true if filteredConversations must be regenerated
Nicolas Jager's avatar
Nicolas Jager committed
132 133

public Q_SLOTS:
134 135 136 137 138
    /**
     * Listen from contactModel when updated (like new alias, avatar, etc.)
     */
    void slotContactModelUpdated();
    /**
139
     * Listen from contactModel when a new contact is added
140 141 142
     * @param uri
     */
    void slotContactAdded(const std::string& uri);
143 144 145 146 147
    /**
     * Listen from contactModel when a pending contact is accepted
     * @param uri
     */
    void slotPendingContactAccepted(const std::string& uri);
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
    /**
     * 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);
190 191 192 193 194 195
    /**
     * 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);
Nicolas Jager's avatar
Nicolas Jager committed
196 197 198 199 200
    /**
     * Listen from CallbacksHandler when a conference is deleted.
     * @param confId
     */
    void slotConferenceRemoved(const std::string& confId);
Nicolas Jager's avatar
Nicolas Jager committed
201 202 203

};

204 205 206 207
ConversationModel::ConversationModel(const account::Info& owner,
                                     Database& db,
                                     const CallbacksHandler& callbacksHandler,
                                     const BehaviorController& behaviorController)
Nicolas Jager's avatar
Nicolas Jager committed
208
: QObject()
209
, pimpl_(std::make_unique<ConversationModelPimpl>(*this, db, callbacksHandler, behaviorController))
210
, owner(owner)
Nicolas Jager's avatar
Nicolas Jager committed
211 212 213 214 215 216 217 218 219 220
{

}

ConversationModel::~ConversationModel()
{

}

const ConversationModel::ConversationQueue&
221
ConversationModel::allFilteredConversations() const
Nicolas Jager's avatar
Nicolas Jager committed
222
{
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263
    if (!pimpl_->dirtyConversations)
        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 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 !contactInfo.profileInfo.alias.empty();
            } else {
                // We only want pending requests matching with the filter
                if (contactInfo.profileInfo.type != profile::Type::PENDING)
                    return false;
            }

            // Check contact
            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 = false;
    return pimpl_->filteredConversations;
Nicolas Jager's avatar
Nicolas Jager committed
264 265 266
}

conversation::Info
267
ConversationModel::filteredConversation(const unsigned int row) const
Nicolas Jager's avatar
Nicolas Jager committed
268
{
269 270 271 272
    const auto& conversations = allFilteredConversations();
    if (row >= conversations.size())
        return conversation::Info();
    return conversations.at(row);
Nicolas Jager's avatar
Nicolas Jager committed
273 274 275
}

void
276
ConversationModel::makePermanent(const std::string& uid)
Nicolas Jager's avatar
Nicolas Jager committed
277
{
278 279 280 281 282 283 284 285 286 287 288 289 290
    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());
Nicolas Jager's avatar
Nicolas Jager committed
291 292 293
}

void
294
ConversationModel::selectConversation(const std::string& uid) const
Nicolas Jager's avatar
Nicolas Jager committed
295
{
296 297 298 299 300 301 302 303 304 305
    // 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;
    }
Nicolas Jager's avatar
Nicolas Jager committed
306

307 308
    auto& conversation = pimpl_->conversations.at(conversationIdx);
    try  {
Nicolas Jager's avatar
Nicolas Jager committed
309 310 311 312 313
        if (not conversation.confId.empty()) {
            emit pimpl_->behaviorController.showCallView(owner.id, conversation);
        } else {
            auto call = owner.callModel->getCall(conversation.callId);
            switch (call.status) {
314 315 316 317 318
            case call::Status::INCOMING_RINGING:
            case call::Status::OUTGOING_RINGING:
            case call::Status::CONNECTING:
            case call::Status::SEARCHING:
                // We are currently in a call
319
                emit pimpl_->behaviorController.showIncomingCallView(owner.id, conversation);
320 321 322 323 324 325
                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
326
                emit pimpl_->behaviorController.showCallView(owner.id, conversation);
327 328 329 330 331 332 333 334 335
                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
336
                emit pimpl_->behaviorController.showChatView(owner.id, conversation);
Nicolas Jager's avatar
Nicolas Jager committed
337
            }
338 339
        }
    } catch (const std::out_of_range&) {
340
        emit pimpl_->behaviorController.showChatView(owner.id, conversation);
341
    }
Nicolas Jager's avatar
Nicolas Jager committed
342 343 344
}

void
345
ConversationModel::removeConversation(const std::string& uid, bool banned)
Nicolas Jager's avatar
Nicolas Jager committed
346
{
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362
    // 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);
Nicolas Jager's avatar
Nicolas Jager committed
363 364 365
}

void
366
ConversationModel::placeCall(const std::string& uid)
Nicolas Jager's avatar
Nicolas Jager committed
367
{
368 369 370 371 372 373 374 375 376 377 378 379
    auto conversationIdx = pimpl_->indexOf(uid);

    if (conversationIdx == -1)
        return;

    auto& conversation = pimpl_->conversations.at(conversationIdx);
    if (conversation.participants.empty()) {
        // Should not
        qDebug() << "ConversationModel::placeCall can't call a conversation without participant";
        return;
    }

380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406
    // Disallow multiple call
    if (!conversation.callId.empty()) {
        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:
                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&) {
        }
    }

407 408 409 410 411 412 413 414 415 416 417
    auto convId = uid;
    auto accountId = pimpl_->accountProfileId;

    auto participant = conversation.participants.front();
    auto contactInfo = owner.contactModel->getContact(participant);
    auto url = contactInfo.profileInfo.uri;
    if (url.empty()) return; // Incorrect item
    pimpl_->sendContactRequest(participant);
    if (owner.profileInfo.type != profile::Type::SIP) {
        url = "ring:" + url; // Add the ring: before or it will fail.
    }
418
    conversation.callId = owner.callModel->createCall(url);
419 420 421 422 423 424 425 426 427 428 429 430 431
    if (convId.empty()) {
        // The conversation has changed because it was with the temporary item
        auto contactProfileId = database::getProfileId(pimpl_->db, contactInfo.profileInfo.uri);
        auto common = database::getConversationsBetween(pimpl_->db, accountId, contactProfileId);
        if (common.empty()) return;
        convId = common.front();
        // Get new conversation
        conversationIdx = pimpl_->indexOf(convId);
        if (conversationIdx == -1)
            return;
        conversation = pimpl_->conversations.at(conversationIdx);
    }
    pimpl_->dirtyConversations = true;
432
    emit pimpl_->behaviorController.showIncomingCallView(owner.id, conversation);
Nicolas Jager's avatar
Nicolas Jager committed
433 434 435
}

void
436
ConversationModel::sendMessage(const std::string& uid, const std::string& body)
Nicolas Jager's avatar
Nicolas Jager committed
437
{
438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453
    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;

    // Send interaction to all participants
    // NOTE: conferences are not implemented yet, so we have only one participant
454
    uint64_t daemonMsgId = 0;
455 456 457
    for (const auto& participant: conversation.participants) {
        auto contactInfo = owner.contactModel->getContact(participant);
        pimpl_->sendContactRequest(participant);
458 459 460 461 462 463 464 465

        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()
466
            and (owner.callModel->getCall(conversation.callId).status != call::Status::IN_PROGRESS
467
            or owner.callModel->getCall(conversation.callId).status != call::Status::PAUSED)) {
468

469
            owner.callModel->sendSipMessage(conversation.callId, body);
470 471

        } else
472
            daemonMsgId = owner.contactModel->sendDhtMessage(contactInfo.profileInfo.uri, body);
473

474 475 476 477 478 479 480 481 482 483 484 485 486 487 488
        if (convId.empty()) {
            // The conversation has changed because it was with the temporary item
            auto contactProfileId = database::getProfileId(pimpl_->db, contactInfo.profileInfo.uri);
            auto common = database::getConversationsBetween(pimpl_->db, accountId, contactProfileId);
            if (common.empty()) return;
            convId = common.front();
            // Get new conversation
            conversationIdx = pimpl_->indexOf(convId);
            if (conversationIdx == -1)
                return;
            conversation = pimpl_->conversations.at(conversationIdx);
        }
    }

    // Add interaction to database
489
    auto status = (conversation.callId.empty()) ? interaction::Status::SENDING : interaction::Status::UNKNOWN;
490
    auto msg = interaction::Info {accountId, body, std::time(nullptr),
491
                                  interaction::Type::TEXT, status};
492 493
    int msgId = database::addMessageToConversation(pimpl_->db, accountId, convId, msg);
    // Update conversation
494 495 496 497 498
    if (conversation.callId.empty()) {
        // 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));
    }
    conversation.interactions.insert(std::pair<uint64_t, interaction::Info>(msgId, msg));
499
    conversation.lastMessageUid = msgId;
500
    pimpl_->dirtyConversations = true;
501
    // Emit this signal for chatview in the client
502
    emit newUnreadMessage(convId, msgId, msg);
503 504 505 506
    // 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();
Nicolas Jager's avatar
Nicolas Jager committed
507 508 509 510 511
}

void
ConversationModel::setFilter(const std::string& filter)
{
512 513 514 515 516 517
    pimpl_->filter = filter;
    pimpl_->dirtyConversations = true;
    // Will update the temporary contact in the contactModel
    owner.contactModel->searchContact(filter);
    emit filterChanged();
}
Nicolas Jager's avatar
Nicolas Jager committed
518

519 520 521 522 523 524 525
void
ConversationModel::setFilter(const profile::Type& filter)
{
    // Switch between PENDING, RING and SIP contacts.
    pimpl_->typeFilter = filter;
    pimpl_->dirtyConversations = true;
    emit filterChanged();
Nicolas Jager's avatar
Nicolas Jager committed
526 527 528
}

void
529
ConversationModel::joinConversations(const std::string& uidA, const std::string& uidB)
Nicolas Jager's avatar
Nicolas Jager committed
530
{
531 532 533 534 535 536
    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];
Nicolas Jager's avatar
Nicolas Jager committed
537

538 539
    if (conversationA.callId.empty() || conversationB.callId.empty())
        return;
Nicolas Jager's avatar
Nicolas Jager committed
540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556

    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;
        }
    }
Nicolas Jager's avatar
Nicolas Jager committed
557 558 559 560 561
}

void
ConversationModel::clearHistory(const std::string& uid)
{
562 563 564 565 566 567 568 569 570 571 572 573 574
    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
    conversation.interactions.clear();
    database::getHistory(pimpl_->db, conversation); // will contains "Conversation started"
    pimpl_->sortConversations();
    emit modelSorted();
    emit conversationCleared(uid);
Nicolas Jager's avatar
Nicolas Jager committed
575 576
}

577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594
void
ConversationModel::setInteractionRead(const std::string& convId, const uint64_t& msgId)
{
    auto conversationIdx = pimpl_->indexOf(convId);
    if (conversationIdx != -1) {
        auto& interactions = pimpl_->conversations[conversationIdx].interactions;
        auto it = interactions.find(msgId);
        if (it != interactions.end()) {
            if (it->second.status != interaction::Status::UNREAD) return;
            auto newStatus = interaction::Status::READ;
            it->second.status = newStatus;
            pimpl_->dirtyConversations = true;
            database::updateInteractionStatus(pimpl_->db, msgId, newStatus);
            emit interactionStatusUpdated(convId, msgId, it->second);
        }
    }
}

595 596
ConversationModelPimpl::ConversationModelPimpl(const ConversationModel& linked,
                                               Database& db,
597 598
                                               const CallbacksHandler& callbacksHandler,
                                               const BehaviorController& behaviorController)
599 600
: linked(linked)
, db(db)
601 602 603
, callbacksHandler(callbacksHandler)
, typeFilter(profile::Type::INVALID)
, accountProfileId(database::getProfileId(db, linked.owner.profileInfo.uri))
604
, behaviorController(behaviorController)
Nicolas Jager's avatar
Nicolas Jager committed
605
{
606 607 608 609 610 611 612
    initConversations();

    // Contact related
    connect(&*linked.owner.contactModel, &ContactModel::modelUpdated,
            this, &ConversationModelPimpl::slotContactModelUpdated);
    connect(&*linked.owner.contactModel, &ContactModel::contactAdded,
            this, &ConversationModelPimpl::slotContactAdded);
613 614
    connect(&*linked.owner.contactModel, &ContactModel::pendingContactAccepted,
            this, &ConversationModelPimpl::slotPendingContactAccepted);
615 616 617 618 619 620 621 622
    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);
623 624
    connect(&callbacksHandler, &CallbacksHandler::accountMessageStatusChanged,
            this, &ConversationModelPimpl::slotUpdateInteractionStatus);
625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643


    // 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);
644 645 646 647
    connect(&*linked.owner.callModel,
            &lrc::api::NewCallModel::callAddedToConference,
            this,
            &ConversationModelPimpl::slotCallAddedToConference);
Nicolas Jager's avatar
Nicolas Jager committed
648 649 650 651
    connect(&callbacksHandler,
            &CallbacksHandler::conferenceRemoved,
            this,
            &ConversationModelPimpl::slotConferenceRemoved);
Nicolas Jager's avatar
Nicolas Jager committed
652 653 654 655 656 657 658
}

ConversationModelPimpl::~ConversationModelPimpl()
{

}

659 660
void
ConversationModelPimpl::initConversations()
Nicolas Jager's avatar
Nicolas Jager committed
661
{
662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682
    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;
        }
683
        auto common = database::getConversationsBetween(db, accountProfileId, contactProfileId);
684 685 686 687 688 689 690 691 692 693 694 695
        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);
    }

    sortConversations();
    filteredConversations = conversations;
    dirtyConversations = false;
Nicolas Jager's avatar
Nicolas Jager committed
696 697 698
}

void
699
ConversationModelPimpl::sortConversations()
Nicolas Jager's avatar
Nicolas Jager committed
700
{
701 702 703 704
    std::sort(
        conversations.begin(), conversations.end(),
        [](const auto& conversationA, const auto& conversationB)
        {
705 706 707
            // A or B is a temporary contact
            if (conversationA.participants.empty()) return true;
            if (conversationB.participants.empty()) return false;
708 709 710
            auto historyA = conversationA.interactions;
            auto historyB = conversationB.interactions;
            // A or B is a new conversation (without CONTACT interaction)
711 712 713
            if (conversationA.uid.empty() || conversationB.uid.empty()) return conversationA.uid.empty();
            if (historyA.empty()) return false;
            if (historyB.empty()) return true;
714 715 716 717 718 719 720 721 722 723
            // 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";
724
                return false;
725 726 727 728
            }
        });
    dirtyConversations = true;
}
Nicolas Jager's avatar
Nicolas Jager committed
729

730 731 732 733 734 735 736
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);
Nicolas Jager's avatar
Nicolas Jager committed
737 738 739
}

void
740
ConversationModelPimpl::slotContactAdded(const std::string& uri)
Nicolas Jager's avatar
Nicolas Jager committed
741
{
742 743 744
    auto contactProfileId = database::getOrInsertProfile(db, uri);
    auto conv = database::getConversationsBetween(db, accountProfileId, contactProfileId);
    if (conv.empty()) {
745 746 747 748 749 750 751 752 753 754 755 756
        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 (...) {}
        conv.emplace_back(
            database::beginConversationsBetween(db, accountProfileId,
                contactProfileId, interaction
            )
        );
757 758 759 760 761 762 763 764 765 766 767 768 769
    }
    // Add the conversation if not already here
    if (indexOf(conv[0]) == -1)
        addConversationWith(conv[0], uri);
    emit linked.newConversation(conv[0]);
    sortConversations();
    auto firstContactUri = conversations.front().participants.front();
    if (firstContactUri.empty()) {
        conversations.pop_front();
        dirtyConversations = true;
    }
    emit linked.modelSorted();
}
Nicolas Jager's avatar
Nicolas Jager committed
770

771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800
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 conversationIdx = indexOf(conv[0]);
            conversations[conversationIdx].interactions.emplace(msgId, msg);
            dirtyConversations = true;
            emit linked.newUnreadMessage(conv[0], msgId, msg);
        } catch (std::out_of_range& e) {
            qDebug() << "ConversationModelPimpl::slotContactAdded can't find contact";
        }
    }
}


801 802 803 804 805 806 807 808 809 810 811 812 813
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;
    emit linked.conversationRemoved(conversationUid);
    emit linked.modelSorted();
Nicolas Jager's avatar
Nicolas Jager committed
814 815 816
}

void
817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866
ConversationModelPimpl::slotContactModelUpdated()
{
    // 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 empty.
        if (!temporaryContact.profileInfo.alias.empty()) {
            if (!conversations.empty()) {
                auto firstContactUri = conversations.front().participants.front();
                if (!firstContactUri.empty()) {
                    conversations.emplace_front(conversationInfo);
                }
            } else {
                conversations.emplace_front(conversationInfo);
            }
        }
        dirtyConversations = true;
    } else {
        // No filter, so we can remove the newConversationItem
        if (!conversations.empty()) {
            auto firstContactUri = conversations.front().participants.front();
            if (firstContactUri.empty()) {
                conversations.pop_front();
                dirtyConversations = true;
            }
        }
    }
    emit linked.modelSorted();
}

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);
867 868 869 870 871 872 873 874 875 876 877 878 879 880
    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);
            }
            slotUpdateInteractionStatus(linked.owner.id, std::stoull(id),
                                        contactUri, status);
        }
    }

881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917
    conversations.emplace_front(conversation);
    dirtyConversations = 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;
918
    emit behaviorController.showIncomingCallView(linked.owner.id, conversation);
919 920 921 922
}

void
ConversationModelPimpl::slotCallStatusChanged(const std::string& callId)
Nicolas Jager's avatar
Nicolas Jager committed
923
{
924 925 926 927 928 929 930 931 932 933 934 935 936
    // 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);
}
Nicolas Jager's avatar
Nicolas Jager committed
937

938 939 940
void
ConversationModelPimpl::slotCallStarted(const std::string& callId)
{
941 942 943 944 945 946 947 948 949
    try {
        auto call = linked.owner.callModel->getCall(callId);
        if (call.isOutoging)
            addOrUpdateCallMessage(callId, QObject::tr("📞 Outgoing call").toStdString());
        else
            addOrUpdateCallMessage(callId, QObject::tr("📞 Incoming call").toStdString());
    } catch (std::out_of_range& e) {
        qDebug() << "ConversationModelPimpl::slotCallEnded can't end inexistant call";
    }
Nicolas Jager's avatar
Nicolas Jager committed
950 951 952
}

void
953
ConversationModelPimpl::slotCallEnded(const std::string& callId)
Nicolas Jager's avatar
Nicolas Jager committed
954
{
955 956 957 958 959 960 961 962 963 964 965 966 967 968
    try {
        auto call = linked.owner.callModel->getCall(callId);
        if (call.startTime.time_since_epoch().count() != 0) {
            if (call.isOutoging)
                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.isOutoging)
                addOrUpdateCallMessage(callId, QObject::tr("🕽 Missed outgoing call").toStdString());
            else
                addOrUpdateCallMessage(callId, QObject::tr("🕽 Missed incoming call").toStdString());
969
        }
970 971 972 973 974 975

        // reset the callId stored in the conversation
        for (auto& conversation: conversations)
            if (conversation.callId == callId) {
                conversation.callId = "";
                dirtyConversations = true;
976
                linked.selectConversation(conversation.uid);
977 978 979 980
            }
    } catch (std::out_of_range& e) {
        qDebug() << "ConversationModelPimpl::slotCallEnded can't end inexistant call";
    }
981
}
Nicolas Jager's avatar
Nicolas Jager committed
982

983
void
984
ConversationModelPimpl::addOrUpdateCallMessage(const std::string& callId, const std::string& body)
985 986
{
    // Get conversation
987 988 989 990 991
    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};
992 993 994 995 996 997 998 999
            int msgId = database::addOrUpdateMessage(db, accountProfileId, conversation.uid, msg, callId);
            auto newInteraction = conversation.interactions.find(msgId) == conversation.interactions.end();
            if (newInteraction) {
                conversation.lastMessageUid = msgId;
                conversation.interactions.emplace(msgId, msg);
            } else {
                conversation.interactions[msgId] = msg;
            }
1000
            dirtyConversations = true;
1001 1002 1003 1004
            if (newInteraction)
                emit linked.newUnreadMessage(conversation.uid, msgId, msg);
            else
                emit linked.interactionStatusUpdated(conversation.uid, msgId, msg);
1005 1006 1007 1008
            sortConversations();
            emit linked.modelSorted();
        }
    }
Nicolas Jager's avatar
Nicolas Jager committed
1009 1010 1011
}

void
1012 1013 1014
ConversationModelPimpl::slotNewAccountMessage(std::string& accountId,
                                              std::string& from,
                                              std::map<std::string,std::string> payloads)
Nicolas Jager's avatar
Nicolas Jager committed
1015
{
1016 1017
    if (accountId != linked.owner.id)
        return;
Nicolas Jager's avatar
Nicolas Jager committed
1018

1019
    addIncomingMessage(from, payloads["text/plain"]);
Nicolas Jager's avatar
Nicolas Jager committed
1020 1021 1022
}

void
1023
ConversationModelPimpl::slotIncomingCallMessage(const std::string& callId, const std::string& from, const std::string& body)
Nicolas Jager's avatar
Nicolas Jager committed
1024
{
1025 1026 1027
    if (not linked.owner.callModel->hasCall(callId))
        return;

1028 1029 1030 1031
    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) {
Nicolas Jager's avatar
Nicolas Jager committed
1032
            if (conversation.confId == callId) {
1033 1034 1035 1036 1037 1038 1039 1040 1041
                if (conversation.participants.empty()) continue;
                auto authorProfileId = database::getOrInsertProfile(db, from);
                addIncomingMessage(conversation.participants.front(), body, authorProfileId);
            }
        }
    } else {
        addIncomingMessage(from, body);
    }

1042
}
Nicolas Jager's avatar
Nicolas Jager committed
1043

1044
void
1045 1046 1047
ConversationModelPimpl::addIncomingMessage(const std::string& from,
                                           const std::string& body,
                                           const std::string& authorProfileId)
1048 1049 1050 1051 1052
{
    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()) {
1053 1054 1055 1056
        conv.emplace_back(database::beginConversationsBetween(
            db, accountProfileId, contactProfileId,
            QObject::tr("Invitation received").toStdString()
        ));
1057
    }
1058 1059
    auto authorId = authorProfileId.empty()? contactProfileId: authorProfileId;
    auto msg = interaction::Info {authorId, body, std::time(nullptr),
1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071
                                  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 {
        conversations[conversationIdx].interactions.emplace(msgId, msg);
        conversations[conversationIdx].lastMessageUid = msgId;
    }
    dirtyConversations = true;
1072
    emit linked.newUnreadMessage(conv[0], msgId, msg);
1073 1074
    sortConversations();
    emit linked.modelSorted();
Nicolas Jager's avatar
Nicolas Jager committed
1075 1076
}

1077 1078 1079 1080 1081
void
ConversationModelPimpl::slotCallAddedToConference(const std::string& callId, const std::string& confId)
{
    for (auto& conversation: conversations) {
        if (conversation.callId == callId) {
Nicolas Jager's avatar
Nicolas Jager committed
1082
            conversation.confId = confId;
1083 1084 1085 1086 1087 1088
            dirtyConversations = true;
            emit linked.selectConversation(conversation.uid);
        }
    }
}

1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136
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]);
        if (conversationIdx != -1) {
            auto& interactions = conversations[conversationIdx].interactions;
            auto it = interactions.find(msgId);
            if (it != interactions.end()) {
                it->second.status = newStatus;
                emit linked.interactionStatusUpdated(conv[0], msgId, it->second);
            }
        }
    }
}
1137

Nicolas Jager's avatar
Nicolas Jager committed
1138 1139 1140 1141 1142 1143 1144 1145 1146 1147
void
ConversationModelPimpl::slotConferenceRemoved(const std::string& confId)
{
    // Get conversation
    for(auto& i : conversations){
        if (i.confId == confId)
            i.confId = "";
    }
}

Nicolas Jager's avatar
Nicolas Jager committed
1148 1149 1150 1151
} // namespace lrc

#include "api/moc_conversationmodel.cpp"
#include "conversationmodel.moc"