conversationmodel.cpp 97.7 KB
Newer Older
Nicolas Jager's avatar
Nicolas Jager committed
1
/****************************************************************************
2
 *    Copyright (C) 2017-2020 Savoir-faire Linux Inc.                       *
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>       *
6
 *   Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>       *
Nicolas Jager's avatar
Nicolas Jager committed
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
 *                                                                          *
 *   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"

23
 // LRC
Anthony Léonard's avatar
Anthony Léonard committed
24
#include "api/lrc.h"
25
#include "api/behaviorcontroller.h"
26
#include "api/contactmodel.h"
Nicolas Jager's avatar
Nicolas Jager committed
27 28 29
#include "api/newcallmodel.h"
#include "api/newaccountmodel.h"
#include "api/account.h"
30
#include "api/call.h"
Nicolas Jager's avatar
Nicolas Jager committed
31 32
#include "api/datatransfer.h"
#include "api/datatransfermodel.h"
33
#include "callbackshandler.h"
34
#include "authority/storagehelper.h"
35
#include "uri.h"
36 37 38

// Dbus
#include "dbus/configurationmanager.h"
39
#include "dbus/callmanager.h"
Nicolas Jager's avatar
Nicolas Jager committed
40

41 42 43 44 45 46 47 48 49 50 51 52 53 54
// daemon
#include <account_const.h>
#include <datatransfer_interface.h>

//Qt
#include <QtCore/QTimer>
#include <QFileInfo>

// std
#include <algorithm>
#include <mutex>
#include <regex>
#include <fstream>

Nicolas Jager's avatar
Nicolas Jager committed
55 56 57
namespace lrc
{

58
using namespace authority;
Nicolas Jager's avatar
Nicolas Jager committed
59 60 61 62 63 64
using namespace api;

class ConversationModelPimpl : public QObject
{
    Q_OBJECT
public:
65
    ConversationModelPimpl(const ConversationModel& linked,
Anthony Léonard's avatar
Anthony Léonard committed
66
                           Lrc& lrc,
67
                           Database& db,
68 69
                           const CallbacksHandler& callbacksHandler,
                           const BehaviorController& behaviorController);
70

Nicolas Jager's avatar
Nicolas Jager committed
71 72 73
    ~ConversationModelPimpl();

    /**
74 75 76 77
     * return a conversation index from conversations or -1 if no index is found.
     * @param uid of the contact to search.
     * @return an int.
     */
78
    int indexOf(const QString& uid) const;
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
79 80 81 82 83 84 85 86
    /**
     * return a reference to a conversation with given uid.
     * @param conversation uid.
     * @param searchResultIncluded if need to search in contacts and userSearch.
     * @return a reference to a conversation with given uid.
     */
    conversation::Info& getConversation(const QString& uid, const bool searchResultIncluded = false);

87 88 89 90
    /**
     * 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
91
     */
92
    int indexOfContact(const QString& uri) const;
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
93 94 95 96 97 98 99
    /**
     * return a reference to a conversation with participant.
     * @param participant uri.
     * @param searchResultIncluded if need to search in contacts and userSearch.
     * @return a reference to a conversation with participant.
     */
    conversation::Info& getConversationForContact(const QString& uid, const bool searchResultIncluded = false);
Nicolas Jager's avatar
Nicolas Jager committed
100 101 102 103 104 105 106 107
    /**
     * Initialize conversations_ and filteredConversations_
     */
    void initConversations();
    /**
     * Sort conversation by last action
     */
    void sortConversations();
108 109 110 111
    /**
     * Call contactModel.addContact if necessary
     * @param contactUri
     */
112
    void sendContactRequest(const QString& contactUri);
113 114 115 116 117
    /**
     * Add a conversation with contactUri
     * @param convId
     * @param contactUri
     */
118
    void addConversationWith(const QString& convId, const QString& contactUri);
119 120 121
    /**
     * Add call interaction for conversation with callId
     * @param callId
122
     * @param duration
123
     */
124 125
    void addOrUpdateCallMessage(const QString& callId,
                                const QString& from = {},
126
                                const std::time_t& duration = -1);
127 128
    /**
     * Add a new message from a peer in the database
129 130 131 132
     * @param from          the author uri
     * @param body          the content of the message
     * @param timestamp     the timestamp of the message
     * @param daemonId      the daemon id
133
     * @return msgId generated (in db)
134
     */
135 136
    int addIncomingMessage(const QString& from,
                           const QString& body,
137 138
                           const uint64_t& timestamp = 0,
                           const QString& daemonId = "");
139 140 141 142 143 144 145
    /**
     * 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
     */
146
    void slotUpdateInteractionStatus(const QString& accountId,
147
                                     const uint64_t id,
148
                                     const QString& to, int status);
Nicolas Jager's avatar
Nicolas Jager committed
149

150 151 152 153 154
    /**
     * place a call
     * @param uid, conversation id
     * @param isAudioOnly, allow to specify if the call is only audio. Set to false by default.
     */
155
    void placeCall(const QString& uid, bool isAudioOnly = false);
156

Nicolas Jager's avatar
Nicolas Jager committed
157 158 159
    /**
     * get number of unread messages
     */
160
    int getNumberOfUnreadMessagesFor(const QString& uid);
Nicolas Jager's avatar
Nicolas Jager committed
161

Anthony Léonard's avatar
Anthony Léonard committed
162 163 164
    /**
     * Handle data transfer progression
     */
165
    void updateTransfer(QTimer* timer, const QString& conversation, int conversationIdx,
Anthony Léonard's avatar
Anthony Léonard committed
166 167 168
                        int interactionId);

    bool usefulDataFromDataTransfer(long long dringId, const datatransfer::Info& info,
169
                                    int& interactionId, QString& convId);
Anthony Léonard's avatar
Anthony Léonard committed
170

171 172 173 174 175 176
    /**
     * accept a file transfer
     * @param convUid
     * @param interactionId
     * @param final name of the file
     */
177
    void acceptTransfer(const QString& convUid, uint64_t interactionId, const QString& path);
178

179
    const ConversationModel& linked;
Anthony Léonard's avatar
Anthony Léonard committed
180
    Lrc& lrc;
181 182
    Database& db;
    const CallbacksHandler& callbacksHandler;
183
    const BehaviorController& behaviorController;
Nicolas Jager's avatar
Nicolas Jager committed
184

185 186
    ConversationModel::ConversationQueue conversations; ///< non-filtered conversations
    ConversationModel::ConversationQueue filteredConversations;
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
187
    ConversationModel::ConversationQueue searchResults;
188
    ConversationModel::ConversationQueue customFilteredConversations;
189
    QString filter;
190
    profile::Type typeFilter;
191 192
    profile::Type customTypeFilter;
    std::pair<bool, bool> dirtyConversations {true, true}; ///< true if filteredConversations/customFilteredConversations must be regenerated
193
    std::map<QString, std::mutex> interactionsLocks; ///< {convId, mutex}
Nicolas Jager's avatar
Nicolas Jager committed
194 195

public Q_SLOTS:
196 197 198
    /**
     * Listen from contactModel when updated (like new alias, avatar, etc.)
     */
199
    void slotContactModelUpdated(const QString& uri, bool needsSorted);
200
    /**
201
     * Listen from contactModel when a new contact is added
202 203
     * @param uri
     */
204
    void slotContactAdded(const QString& contactUri);
205 206 207 208
    /**
     * Listen from contactModel when a pending contact is accepted
     * @param uri
     */
209
    void slotPendingContactAccepted(const QString& uri);
210 211 212 213
    /**
     * Listen from contactModel when aa new contact is removed
     * @param uri
     */
214
    void slotContactRemoved(const QString& uri);
215 216 217 218 219
    /**
     * Listen from callmodel for new calls.
     * @param fromId caller uri
     * @param callId
     */
220
    void slotIncomingCall(const QString& fromId, const QString& callId);
221 222 223 224
    /**
     * Listen from callmodel for calls status changed.
     * @param callId
     */
225
    void slotCallStatusChanged(const QString& callId, int code);
226 227 228 229
    /**
     * Listen from callmodel for writing "Call started"
     * @param callId
     */
230
    void slotCallStarted(const QString& callId);
231 232 233 234
    /**
     * Listen from callmodel for writing "Call ended"
     * @param callId
     */
235
    void slotCallEnded(const QString& callId);
236 237 238
    /**
     * Listen from CallbacksHandler for new incoming interactions;
     * @param accountId
239
     * @param msgId
240 241 242
     * @param from uri
     * @param payloads body
     */
243 244 245 246
    void slotNewAccountMessage(const QString& accountId,
                               const QString& msgId,
                               const QString& from,
                               const MapStringString& payloads);
247 248 249 250 251 252
    /**
     * 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
     */
253
    void slotIncomingCallMessage(const QString& callId, const QString& from, const QString& body);
254 255 256 257 258
    /**
     * Listen from CallModel when a call is added to a conference
     * @param callId
     * @param confId
     */
259
    void slotCallAddedToConference(const QString& callId, const QString& confId);
Nicolas Jager's avatar
Nicolas Jager committed
260 261 262 263
    /**
     * Listen from CallbacksHandler when a conference is deleted.
     * @param confId
     */
264
    void slotConferenceRemoved(const QString& confId);
265 266 267 268 269 270 271
    /**
     * Listen for when a contact is composing
     * @param accountId
     * @param contactUri
     * @param isComposing
     */
    void slotComposingStatusChanged(const QString& accountId, const QString& contactUri, bool isComposing);
Nicolas Jager's avatar
Nicolas Jager committed
272

Anthony Léonard's avatar
Anthony Léonard committed
273 274
    void slotTransferStatusCreated(long long dringId, api::datatransfer::Info info);
    void slotTransferStatusCanceled(long long dringId, api::datatransfer::Info info);
275 276
    void slotTransferStatusAwaitingPeer(long long dringId, api::datatransfer::Info info);
    void slotTransferStatusAwaitingHost(long long dringId, api::datatransfer::Info info);
Anthony Léonard's avatar
Anthony Léonard committed
277 278 279
    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);
280
    void slotTransferStatusTimeoutExpired(long long dringId, api::datatransfer::Info info);
281
    void slotTransferStatusUnjoinable(long long dringId, api::datatransfer::Info info);
282
    void updateTransferStatus(long long dringId, api::datatransfer::Info info, interaction::Status newStatus);
Nicolas Jager's avatar
Nicolas Jager committed
283 284
};

285
ConversationModel::ConversationModel(const account::Info& owner,
Anthony Léonard's avatar
Anthony Léonard committed
286
                                     Lrc& lrc,
287 288 289
                                     Database& db,
                                     const CallbacksHandler& callbacksHandler,
                                     const BehaviorController& behaviorController)
290
: QObject(nullptr)
Anthony Léonard's avatar
Anthony Léonard committed
291
, pimpl_(std::make_unique<ConversationModelPimpl>(*this, lrc, db, callbacksHandler, behaviorController))
292
, owner(owner)
Nicolas Jager's avatar
Nicolas Jager committed
293 294 295 296 297 298 299 300 301 302
{

}

ConversationModel::~ConversationModel()
{

}

const ConversationModel::ConversationQueue&
303
ConversationModel::allFilteredConversations() const
Nicolas Jager's avatar
Nicolas Jager committed
304
{
305
    if (!pimpl_->dirtyConversations.first)
306 307 308 309 310 311 312 313
        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) {
314 315 316 317
            try {
                auto contactInfo = owner.contactModel->getContact(entry.participants.front());

                auto filter = pimpl_->filter;
318
                auto uri = URI(filter);
319
                bool stripScheme = (uri.schemeType() < URI::SchemeType::COUNT__);
320 321 322 323
                FlagPack<URI::Section> flags = URI::Section::USER_INFO | URI::Section::HOSTNAME | URI::Section::PORT;
                if (!stripScheme) {
                    flags |= URI::Section::SCHEME;
                }
324

325
                filter = uri.format(flags);
326

327 328 329 330 331 332 333 334
                /* Check contact */
                // If contact is banned, only match if filter is a perfect match
                if (contactInfo.isBanned) {
                    if (filter == "") return false;
                    return contactInfo.profileInfo.uri == filter
                           || contactInfo.profileInfo.alias == filter
                           || contactInfo.registeredName == filter;
                }
335

336 337 338
                std::regex regexFilter;
                auto isValidReFilter = true;
                try {
339
                    regexFilter = std::regex(filter.toStdString(), std::regex_constants::icase);
340 341 342
                } catch(std::regex_error&) {
                    isValidReFilter = false;
                }
343

344
                auto filterUriAndReg = [regexFilter, isValidReFilter](auto contact, auto filter) {
345 346
                    auto result = contact.profileInfo.uri.contains(filter)
                        || contact.registeredName.contains(filter);
347
                    if (!result) {
348 349 350
                        auto regexFound = isValidReFilter? (!contact.profileInfo.uri.isEmpty()
                               && std::regex_search(contact.profileInfo.uri.toStdString(), regexFilter))
                               || std::regex_search(contact.registeredName.toStdString(), regexFilter) : false;
351 352 353 354 355 356 357 358 359
                        result |= regexFound;
                    }
                    return result;
                };

                /* Check type */
                if (pimpl_->typeFilter != profile::Type::PENDING) {
                    // Remove pending contacts and get the temporary item if filter is not empty
                    switch (contactInfo.profileInfo.type) {
360
                    case profile::Type::COUNT__:
361 362 363 364 365
                    case profile::Type::INVALID:
                    case profile::Type::PENDING:
                        return false;
                    case profile::Type::TEMPORARY:
                        return filterUriAndReg(contactInfo, filter);
Sébastien Blin's avatar
Sébastien Blin committed
366 367 368
                    case profile::Type::SIP:
                    case profile::Type::RING:
                        break;
369 370 371 372 373
                    }
                } else {
                    // We only want pending requests matching with the filter
                    if (contactInfo.profileInfo.type != profile::Type::PENDING)
                        return false;
374
                }
375 376

                // Otherwise perform usual regex search
377 378 379 380 381
                bool result = contactInfo.profileInfo.alias.contains(filter);
                if (!result && isValidReFilter)
                    result |= std::regex_search(contactInfo.profileInfo.alias.toStdString(), regexFilter);
                if (!result)
                    result |= filterUriAndReg(contactInfo, filter);
382
                return result;
383 384 385
            } catch (std::out_of_range&) {
                // getContact() failed
                return false;
386
            }
387
    });
388
    pimpl_->filteredConversations.resize(std::distance(pimpl_->filteredConversations.begin(), it));
389
    pimpl_->dirtyConversations.first = false;
390
    return pimpl_->filteredConversations;
Nicolas Jager's avatar
Nicolas Jager committed
391 392
}

393 394
QMap<ConferenceableItem, ConferenceableValue>
ConversationModel::getConferenceableConversations(const QString& convId, const QString& filter) const
395 396 397 398 399
{
    auto conversationIdx = pimpl_->indexOf(convId);
    if (conversationIdx == -1 || !owner.enabled) {
        return {};
    }
400
    QMap<ConferenceableItem, ConferenceableValue> result;
401 402 403 404 405 406 407 408 409 410 411
    ConferenceableValue callsVector, contactsVector;

    auto currentConfId = pimpl_->conversations.at(conversationIdx).confId;
    auto currentCallId = pimpl_->conversations.at(conversationIdx).callId;
    auto calls = pimpl_->lrc.getCalls();
    auto conferences = pimpl_->lrc.getConferences();
    auto conversations = pimpl_->conversations;
    auto currentAccountID = pimpl_->linked.owner.id;
    //add contacts for current account
    for (const auto &conv : conversations) {
        // conversations with calls will be added in call section
412
        if(!conv.callId.isEmpty() || !conv.confId.isEmpty()) {
413 414 415 416 417 418
            continue;
        }
        auto contact = owner.contactModel->getContact(conv.participants.front());
        if(contact.isBanned || contact.profileInfo.type == profile::Type::PENDING) {
            continue;
        }
419
        QVector<AccountConversation> cv;
420
        AccountConversation accConv = {conv.uid, currentAccountID};
421 422 423
        cv.push_back(accConv);
        if (filter.isEmpty()) {
            contactsVector.push_back(cv);
424 425
            continue;
        }
426 427 428
        bool result = contact.profileInfo.alias.contains(filter) ||
                      contact.profileInfo.uri.contains(filter) ||
                      contact.registeredName.contains(filter);
429
        if (result) {
430
            contactsVector.push_back(cv);
431 432 433 434
        }
    }

    if (calls.empty() && conferences.empty()) {
435
        result.insert(ConferenceableItem::CONTACT, contactsVector);
436 437 438 439 440
        return result;
    }

    //filter out calls from conference
    for (const auto& c : conferences) {
441
        for (const auto& subcal : pimpl_->lrc.getConferenceSubcalls(c)) {
442 443 444 445 446 447 448 449
            auto position = std::find(calls.begin(), calls.end(), subcal);
            if (position != calls.end()) {
                calls.erase(position);
            }
        }
    }

    //found conversations and account for calls and conferences
450
    QMap<QString, QVector<AccountConversation>> tempConferences;
451 452 453 454 455
    for (const auto &account_id : pimpl_->lrc.getAccountModel().getAccountList()) {
        try {
            auto &accountInfo = pimpl_->lrc.getAccountModel().getAccountInfo(account_id);
            auto accountConv = accountInfo.conversationModel->getFilteredConversations(accountInfo.profileInfo.type);
            for (const auto &conv : accountConv) {
456
                bool confFilterPredicate = !conv.confId.isEmpty() && conv.confId != currentConfId &&
457
                    std::find(conferences.begin(), conferences.end(), conv.confId) != conferences.end();
458
                bool callFilterPredicate = !conv.callId.isEmpty() && conv.callId != currentCallId &&
459 460 461 462 463 464 465 466
                std::find(calls.begin(), calls.end(), conv.callId) != calls.end();

                if (!confFilterPredicate && !callFilterPredicate) {
                    continue;
                }

                // vector of conversationID accountID pair
                // for call has only one entry, for conference multyple
467
                QVector<AccountConversation> cv;
468
                AccountConversation accConv = {conv.uid, account_id};
469
                cv.push_back(accConv);
470

471
                bool isConference = !conv.confId.isEmpty();
472 473 474 475 476 477 478 479 480 481
                //call could be added if it is not conference and in active state
                bool shouldAddCall = false;
                if (!isConference && accountInfo.callModel->hasCall(conv.callId)) {
                    const auto& call = accountInfo.callModel->getCall(conv.callId);
                    shouldAddCall = call.status == lrc::api::call::Status::PAUSED ||
                                    call.status == lrc::api::call::Status::IN_PROGRESS;
                }

                auto contact = accountInfo.contactModel->getContact(conv.participants.front());
                //check if contact satisfy filter
482 483 484 485
                bool result = (filter.isEmpty() || isConference) ? true :
                              (contact.profileInfo.alias.contains(filter) ||
                              contact.profileInfo.uri.contains(filter) ||
                              contact.registeredName.contains(filter));
486 487 488 489
                if (!result) {
                    continue;
                }
                if (isConference && tempConferences.count(conv.confId)) {
490
                    tempConferences.find(conv.confId).value().push_back(accConv);
491
                } else if (isConference) {
492
                    tempConferences.insert(conv.confId, cv);
493
                } else if (shouldAddCall) {
494
                    callsVector.push_back(cv);
495 496 497 498
                }
            }
        } catch (...) {}
    }
499 500 501
    for(auto it : tempConferences.toStdMap()) {
        if (filter.isEmpty()) {
            callsVector.push_back(it.second);
502 503 504 505 506 507 508
            continue;
        }
        for(AccountConversation accConv : it.second) {
            try {
                auto &account = pimpl_->lrc.getAccountModel().getAccountInfo(accConv.accountId);
                auto conv = account.conversationModel->getConversationForUID(accConv.convId);
                auto cont = account.contactModel->getContact(conv.participants.front());
509 510 511 512
                if ( cont.profileInfo.alias.contains(filter) ||
                     cont.profileInfo.uri.contains(filter) ||
                     cont.registeredName.contains(filter)) {
                     callsVector.push_back(it.second);
513 514 515 516 517
                    continue;
                }
            } catch (...) {}
        }
    }
518 519
    result.insert(ConferenceableItem::CALL, callsVector);
    result.insert(ConferenceableItem::CONTACT, contactsVector);
520 521 522
    return result;
}

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
523 524 525 526 527 528
const ConversationModel::ConversationQueue&
ConversationModel::getAllSearchResults() const
{
    return pimpl_->searchResults;
}

529
const ConversationModel::ConversationQueue&
530
ConversationModel::getFilteredConversations(const profile::Type& filter, bool forceUpdate, const bool includeBanned) const
531
{
532
    if (pimpl_->customTypeFilter == filter && !pimpl_->dirtyConversations.second && !forceUpdate)
533 534 535 536 537 538 539 540
        return pimpl_->customFilteredConversations;

    pimpl_->customTypeFilter = filter;
    pimpl_->customFilteredConversations = pimpl_->conversations;

    auto it = std::copy_if(
        pimpl_->conversations.begin(), pimpl_->conversations.end(),
        pimpl_->customFilteredConversations.begin(),
541
        [this, &includeBanned] (const conversation::Info& entry) {
542
            auto contactInfo = owner.contactModel->getContact(entry.participants.front());
543
            if (!includeBanned && contactInfo.isBanned) return false;
544 545 546 547 548 549 550
            return (contactInfo.profileInfo.type == pimpl_->customTypeFilter);
        });
    pimpl_->customFilteredConversations.resize(std::distance(pimpl_->customFilteredConversations.begin(), it));
    pimpl_->dirtyConversations.second = false;
    return pimpl_->customFilteredConversations;
}

551
conversation::Info
552
ConversationModel::getConversationForUID(const QString& uid) const
553 554
{
    try {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
555 556 557
        return pimpl_->getConversation(uid, true);
    }
    catch (const std::out_of_range& e) {
558 559 560 561
        return {};
    }
}

Nicolas Jager's avatar
Nicolas Jager committed
562
conversation::Info
563
ConversationModel::filteredConversation(const unsigned int row) const
Nicolas Jager's avatar
Nicolas Jager committed
564
{
565 566 567
    const auto& conversations = allFilteredConversations();
    if (row >= conversations.size())
        return conversation::Info();
Nicolas Jager's avatar
Nicolas Jager committed
568 569 570 571

    auto conversationInfo = conversations.at(row);

    return conversationInfo;
Nicolas Jager's avatar
Nicolas Jager committed
572 573
}

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
574 575 576 577 578 579 580 581 582 583
conversation::Info
ConversationModel::searchResultForRow(const unsigned int row) const
{
    const auto& results = pimpl_->searchResults;
    if (row >= results.size())
        return conversation::Info();

    return results.at(row);
}

Nicolas Jager's avatar
Nicolas Jager committed
584
void
585
ConversationModel::makePermanent(const QString& uid)
Nicolas Jager's avatar
Nicolas Jager committed
586
{
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
587 588
    try {
        auto& conversation = pimpl_->getConversation(uid, true);
589

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
590 591 592 593 594
        if (conversation.participants.empty()) {
            // Should not
            qDebug() << "ConversationModel::addConversation can't add a conversation with no participant";
            return;
        }
595

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
596 597 598 599 600
        // Send contact request if non used
        pimpl_->sendContactRequest(conversation.participants.front());
    } catch (const std::out_of_range& e) {
        qDebug() << "make permanent failed. conversation not found";
    }
Nicolas Jager's avatar
Nicolas Jager committed
601 602 603
}

void
604
ConversationModel::selectConversation(const QString& uid) const
Nicolas Jager's avatar
Nicolas Jager committed
605
{
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
606 607
    try {
        auto& conversation = pimpl_->getConversation(uid, true);
Nicolas Jager's avatar
Nicolas Jager committed
608

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
609 610 611 612 613 614 615
        bool callEnded = true;
        if (!conversation.callId.isEmpty()) {
            try  {
                auto call = owner.callModel->getCall(conversation.callId);
                callEnded = call.status == call::Status::ENDED;
            } catch (...) {}
        }
616 617 618 619 620 621
        if (!conversation.confId.isEmpty()
        && owner.confProperties.isRendezVous) {
            // If we are on a rendez vous account and we select the conversation,
            // attach to the call.
            CallManager::instance().unholdConference(conversation.confId);
        }
622

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660
        if (not callEnded and not conversation.confId.isEmpty()) {
            emit pimpl_->behaviorController.showCallView(owner.id, conversation);
        } else if (callEnded) {
            emit pimpl_->behaviorController.showChatView(owner.id, conversation);
        } else {
            try  {
                auto call = owner.callModel->getCall(conversation.callId);
                switch (call.status) {
                case call::Status::INCOMING_RINGING:
                case call::Status::OUTGOING_RINGING:
                case call::Status::CONNECTING:
                case call::Status::SEARCHING:
                    // We are currently in a call
                    emit pimpl_->behaviorController.showIncomingCallView(owner.id, conversation);
                    break;
                case call::Status::PAUSED:
                case call::Status::CONNECTED:
                case call::Status::IN_PROGRESS:
                    // We are currently receiving a call
                    emit pimpl_->behaviorController.showCallView(owner.id, conversation);
                    break;
                case call::Status::PEER_BUSY:
                    emit pimpl_->behaviorController.showLeaveMessageView(owner.id, conversation);
                    break;
                case call::Status::TIMEOUT:
                case call::Status::TERMINATING:
                case call::Status::INVALID:
                case call::Status::INACTIVE:
                    // call just ended
                    emit pimpl_->behaviorController.showChatView(owner.id, conversation);
                    break;
                case call::Status::ENDED:
                default: // ENDED
                    // nothing to do
                    break;
                }
            } catch (const std::out_of_range&) {
                // Should not happen
661
                emit pimpl_->behaviorController.showChatView(owner.id, conversation);
Nicolas Jager's avatar
Nicolas Jager committed
662
            }
663
        }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
664 665
    } catch (const std::out_of_range& e) {
        qDebug() << "select conversation failed. conversation not exists";
666
    }
Nicolas Jager's avatar
Nicolas Jager committed
667 668 669
}

void
670
ConversationModel::removeConversation(const QString& uid, bool banned)
Nicolas Jager's avatar
Nicolas Jager committed
671
{
672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687
    // 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
688 689
}

Nicolas Jager's avatar
Nicolas Jager committed
690 691 692 693 694 695 696 697 698
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);

699
    storage::deleteObsoleteHistory(pimpl_->db, date);
Nicolas Jager's avatar
Nicolas Jager committed
700 701
}

Nicolas Jager's avatar
Nicolas Jager committed
702
void
703
ConversationModelPimpl::placeCall(const QString& uid, bool isAudioOnly)
Nicolas Jager's avatar
Nicolas Jager committed
704
{
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
705 706 707 708 709 710 711
    try {
        auto& conversation = getConversation(uid, true);
        if (conversation.participants.empty()) {
            // Should not
            qDebug() << "ConversationModel::placeCall can't call a conversation without participant";
            return;
        }
712

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735
        // Disallow multiple call
        if (!conversation.callId.isEmpty()) {
            try  {
                auto call = linked.owner.callModel->getCall(conversation.callId);
                switch (call.status) {
                    case call::Status::INCOMING_RINGING:
                    case call::Status::OUTGOING_RINGING:
                    case call::Status::CONNECTING:
                    case call::Status::SEARCHING:
                    case call::Status::PAUSED:
                    case call::Status::IN_PROGRESS:
                    case call::Status::CONNECTED:
                        return;
                    case call::Status::INVALID:
                    case call::Status::INACTIVE:
                    case call::Status::ENDED:
                    case call::Status::PEER_BUSY:
                    case call::Status::TIMEOUT:
                    case call::Status::TERMINATING:
                    default:
                        break;
                }
            } catch (const std::out_of_range&) {
736 737 738
            }
        }

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
739
        auto convId = uid;
740

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
741 742 743 744
        auto participant = conversation.participants.front();
        bool isTemporary = participant == convId;
        auto contactInfo = linked.owner.contactModel->getContact(participant);
        auto uri = contactInfo.profileInfo.uri;
Olivier Soldano's avatar
Olivier Soldano committed
745

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
746 747
        if (uri.isEmpty())
            return; // Incorrect item
Olivier Soldano's avatar
Olivier Soldano committed
748

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
749 750 751 752 753
        // Don't call banned contact
        if (contactInfo.isBanned) {
            qDebug() << "ContactModel::placeCall: denied, contact is banned";
            return;
        }
754

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
755 756 757
        if (linked.owner.profileInfo.type != profile::Type::SIP) {
            uri = "ring:" + uri; // Add the ring: before or it will fail.
        }
Olivier Soldano's avatar
Olivier Soldano committed
758

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
759 760
        auto cb = std::function<void(QString)>(
                                               [this, isTemporary, uri, isAudioOnly, &conversation](QString convId) {
761 762 763 764 765
            int contactIndex;
            if (isTemporary && (contactIndex = indexOfContact(convId)) < 0) {
                qDebug() << "Can't place call: Other participant is not a contact (removed while placing call ?)";
                return;
            }
766

767 768
            auto& newConv = isTemporary ? conversations.at(contactIndex) : conversation;
            convId = newConv.uid;
769

770
            newConv.callId = linked.owner.callModel->createCall(uri, isAudioOnly);
771
            if (newConv.callId.isEmpty()) {
772 773 774
                qDebug() << "Can't place call (daemon side failure ?)";
                return;
            }
Olivier Soldano's avatar
Olivier Soldano committed
775

776 777 778
            dirtyConversations = { true, true };
            emit behaviorController.showIncomingCallView(linked.owner.id, newConv);
        });
Olivier Soldano's avatar
Olivier Soldano committed
779

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
780 781 782 783
        if (isTemporary) {
            QMetaObject::Connection* const connection = new QMetaObject::Connection;
            *connection = connect(&this->linked, &ConversationModel::conversationReady,
                                  [cb, connection](QString convId) {
784
                cb(convId);
785 786 787 788
                QObject::disconnect(*connection);
                if (connection) {
                    delete connection;
                }
789
            });
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
790
        }
791

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
792
        sendContactRequest(participant);
793

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
794 795 796 797 798
        if (!isTemporary) {
            cb(convId);
        }
    } catch (const std::out_of_range& e) {
        qDebug() << "could not place call to not existing conversation";
799
    }
800 801 802
}

void
803
ConversationModel::placeAudioOnlyCall(const QString& uid)
804 805 806 807 808
{
    pimpl_->placeCall(uid, true);
}

void
809
ConversationModel::placeCall(const QString& uid)
810 811
{
    pimpl_->placeCall(uid);
Nicolas Jager's avatar
Nicolas Jager committed
812 813 814
}

void
815
ConversationModel::sendMessage(const QString& uid, const QString& body)
Nicolas Jager's avatar
Nicolas Jager committed
816
{
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
817 818
    try {
        auto& conversation = pimpl_->getConversation(uid, true);
819

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
820 821 822 823 824
        if (conversation.participants.empty()) {
            // Should not
            qDebug() << "ConversationModel::sendMessage can't send a interaction to a conversation with no participant";
            return;
        }
825

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
826 827 828
        auto convId = uid;
        //for temporary contact conversation id is the same as participant uri
        bool isTemporary = conversation.participants.front() == uid;
829

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
830 831 832
        /* Make a copy of participants list: if current conversation is temporary,
         it might me destroyed while we are reading it */
        const auto participants = conversation.participants;
833

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
834 835
        auto cb = std::function<void(QString)>(
                                               [this, isTemporary, body, &conversation](QString convId) {
836
            /* Now we should be able to retrieve the final conversation, in case the previous
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
837 838
             one was temporary */
            // FIXME potential race condition between index check and at() call
839 840 841 842 843
            int contactIndex;
            if (isTemporary && (contactIndex = pimpl_->indexOfContact(convId)) < 0) {
                qDebug() << "Can't send message: Other participant is not a contact";
                return;
            }
844

845 846
            uint64_t daemonMsgId = 0;
            auto status = interaction::Status::SENDING;
Olivier Soldano's avatar
Olivier Soldano committed
847

848 849
            auto& newConv = isTemporary ? pimpl_->conversations.at(contactIndex) : conversation;
            convId = newConv.uid;
Olivier Soldano's avatar
Olivier Soldano committed
850

851 852 853
            // Send interaction to each participant
            for (const auto& participant : newConv.participants) {
                auto contactInfo = owner.contactModel->getContact(participant);
854

855 856 857
                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.
858
                if (not newConv.callId.isEmpty() and not callLists.contains(newConv.callId))
859
                    newConv.callId.clear();
860

861
                if (not newConv.callId.isEmpty()
862 863 864
                    and call::canSendSIPMessage(owner.callModel->getCall(newConv.callId))) {
                    status = interaction::Status::UNKNOWN;
                    owner.callModel->sendSipMessage(newConv.callId, body);
865

866 867 868
                } else {
                    daemonMsgId = owner.contactModel->sendDhtMessage(contactInfo.profileInfo.uri, body);
                }
869

870
            }
871

872
            // Add interaction to database
873 874 875 876 877 878 879 880 881
            interaction::Info msg {
                {},
                body, std::time(nullptr),
                0,
                interaction::Type::TEXT,
                status,
                true
            };
            int msgId = storage::addMessageToConversation(pimpl_->db, convId, msg);
882

883 884 885
            // Update conversation
            if (status == interaction::Status::SENDING) {
                // Because the daemon already give an id for the message, we need to store it.
886
                storage::addDaemonMsgId(pimpl_->db, toQString(msgId), toQString(daemonMsgId));
887
            }
888

889
            bool ret = false;
890

891 892 893 894
            {
                std::lock_guard<std::mutex> lk(pimpl_->interactionsLocks[convId]);
                ret = newConv.interactions.insert(std::pair<uint64_t, interaction::Info>(msgId, msg)).second;
            }
895

896 897 898 899
            if (!ret) {
                qDebug("ConversationModel::sendMessage failed to send message because an existing key was already present in the database (key = %d)", msgId);
                return;
            }
900

901 902 903 904 905 906 907 908 909
            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();
        });
910

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
911 912 913 914
        if (isTemporary) {
            QMetaObject::Connection* const connection = new QMetaObject::Connection;
            *connection = connect(this, &ConversationModel::conversationReady,
                                  [cb, connection](QString convId) {
915
                cb(convId);
916 917 918 919
                QObject::disconnect(*connection);
                if (connection) {
                    delete connection;
                }
920
            });
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
921
        }
922

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
923 924 925 926
        /* Check participants list, send contact request if needed.
         NOTE: conferences are not implemented yet, so we have only one participant */
        for (const auto& participant: participants) {
            auto contactInfo = owner.contactModel->getContact(participant);
927

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
928 929 930 931
            if (contactInfo.isBanned) {
                qDebug() << "ContactModel::sendMessage: denied, contact is banned";
                return;
            }
932

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
933 934
            pimpl_->sendContactRequest(participant);
        }
935

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
936 937 938 939 940
        if (!isTemporary) {
            cb(convId);
        }
    }catch (const std::out_of_range& e) {
        qDebug() << "could not send message to not existing conversation";
941
    }
Nicolas Jager's avatar
Nicolas Jager committed
942 943
}

944 945 946 947 948 949 950
void
ConversationModel::refreshFilter()
{
    pimpl_->dirtyConversations = {true, true};
    emit filterChanged();
}

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
951 952 953 954 955 956
void
ConversationModel::updateSearchStatus(const QString& status) const
{
    emit searchStatusChanged(status);
}

Nicolas Jager's avatar
Nicolas Jager committed
957
void
958
ConversationModel::setFilter(const QString& filter)
Nicolas Jager's avatar
Nicolas Jager committed
959
{
960
    pimpl_->filter = filter;
961
    pimpl_->dirtyConversations = {true, true};
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
962 963
    pimpl_->searchResults.clear();
    emit searchResultUpdated();
964 965 966
    owner.contactModel->searchContact(filter);
    emit filterChanged();
}
Nicolas Jager's avatar
Nicolas Jager committed
967

968 969 970 971 972
void
ConversationModel::setFilter(const profile::Type& filter)
{
    // Switch between PENDING, RING and SIP contacts.
    pimpl_->typeFilter = filter;
973
    pimpl_->dirtyConversations = {true, true};
974
    emit filterChanged();
Nicolas Jager's avatar
Nicolas Jager committed
975 976 977
}

void
978
ConversationModel::joinConversations(const QString& uidA, const QString& uidB)
Nicolas Jager's avatar
Nicolas Jager committed
979
{
980 981
    auto conversationAIdx = pimpl_->indexOf(uidA);
    auto conversationBIdx = pimpl_->indexOf(uidB);
982
    if (conversationAIdx == -1 || conversationBIdx == -1 || !owner.enabled)
983 984 985
        return;
    auto& conversationA = pimpl_->conversations[conversationAIdx];
    auto& conversationB = pimpl_->conversations[conversationBIdx];