~ci-train-bot/history-service/history-service-ubuntu-zesty-2629

« back to all changes in this revision

Viewing changes to plugins/sqlite/sqlitehistoryplugin.cpp

  • Committer: Bileto Bot
  • Author(s): Gustavo Pichorim Boiko
  • Date: 2016-11-30 15:13:58 UTC
  • mfrom: (230.2.23 staging)
  • Revision ID: ci-train-bot@canonical.com-20161130151358-jy3mqj0ir0b6ncxe
Improve group chat support.

Approved by: Roberto Mier Escandón , Tiago Salem Herrmann

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
/*
2
 
 * Copyright (C) 2013-2015 Canonical, Ltd.
 
2
 * Copyright (C) 2013-2016 Canonical, Ltd.
3
3
 *
4
4
 * Authors:
5
5
 *  Gustavo Pichorim Boiko <gustavo.boiko@canonical.com>
35
35
#include <QStringList>
36
36
#include <QSqlError>
37
37
#include <QDBusMetaType>
 
38
#include <QCryptographicHash>
38
39
 
39
 
QString generateThreadMapKey(const History::Thread &thread)
40
 
{
41
 
    return thread.accountId() + thread.threadId();
42
 
}
 
40
static const QLatin1String timestampFormat("yyyy-MM-ddTHH:mm:ss.zzz");
43
41
 
44
42
QString generateThreadMapKey(const QString &accountId, const QString &threadId)
45
43
{
46
44
    return accountId + threadId;
47
45
}
48
46
 
 
47
QString generateThreadMapKey(const History::Thread &thread)
 
48
{
 
49
    return generateThreadMapKey(thread.accountId(), thread.threadId());
 
50
}
 
51
 
49
52
SQLiteHistoryPlugin::SQLiteHistoryPlugin(QObject *parent) :
50
53
    QObject(parent), mInitialised(false)
51
54
{
81
84
        // so instead we just convert to UTC here on the cache and convert back to local time
82
85
        // when returning
83
86
        QDateTime timestamp = QDateTime::fromString(properties[History::FieldTimestamp].toString(), Qt::ISODate);
84
 
        properties[History::FieldTimestamp] = timestamp.toUTC().toString("yyyy-MM-ddTHH:mm:ss.zzz");
 
87
        properties[History::FieldTimestamp] = timestamp.toUTC().toString(timestampFormat);
85
88
 
86
89
        // the same for readTimestamp
87
90
        timestamp = QDateTime::fromString(properties[History::FieldReadTimestamp].toString(), Qt::ISODate);
88
 
        properties[History::FieldReadTimestamp] = timestamp.toUTC().toString("yyyy-MM-ddTHH:mm:ss.zzz");
 
91
        properties[History::FieldReadTimestamp] = timestamp.toUTC().toString(timestampFormat);
89
92
 
90
93
        History::Thread thread = History::Thread::fromProperties(properties);
91
94
        const QString &threadKey = generateThreadMapKey(thread);
92
95
 
93
96
        if (thread.type() != History::EventTypeText) {
94
97
             continue;
95
 
        } else if (!History::Utils::shouldGroupAccount(thread.accountId())) {
 
98
        } else if (!History::Utils::shouldGroupThread(thread)) {
96
99
            // never group non phone accounts
97
100
            mConversationsCache[threadKey] = History::Threads() << thread;
98
101
            mConversationsCacheKeys[threadKey] = threadKey;
119
122
            const QString &conversationKey = it.key();
120
123
            History::Threads groupedThreads = it.value();
121
124
            Q_FOREACH(const History::Thread &groupedThread, groupedThreads) {
 
125
                if (!History::Utils::shouldGroupThread(groupedThread) || thread.chatType() != groupedThread.chatType()) {
 
126
                    continue;
 
127
                }
122
128
                found = History::Utils::compareNormalizedParticipants(thread.participants().identifiers(), groupedThread.participants().identifiers(), History::MatchPhoneNumber);
123
129
                if (found) {
124
130
                    Q_FOREACH(const History::Thread &groupedThread, groupedThreads) {
176
182
    History::Thread thread = History::Thread::fromProperties(properties);
177
183
    QString threadKey = generateThreadMapKey(thread);
178
184
 
179
 
    if (thread.type() != History::EventTypeText || !History::Utils::shouldGroupAccount(thread.accountId())) {
 
185
    if (thread.type() != History::EventTypeText || !History::Utils::shouldGroupThread(thread)) {
180
186
        mConversationsCache.remove(threadKey);
181
187
        mConversationsCacheKeys.remove(threadKey);
182
188
        return;
259
265
    time.start();
260
266
    qDebug() << "---- HistoryService: start generating cached content";
261
267
    QSqlQuery query(SQLiteDatabase::instance()->database());
262
 
    if (!query.exec("SELECT DISTINCT accountId, normalizedId FROM thread_participants")) {
 
268
    if (!query.exec("SELECT DISTINCT accountId, normalizedId, alias, state FROM thread_participants")) {
263
269
        qWarning() << "Failed to generate contact cache:" << query.lastError().text();
264
270
        return;
265
271
    }
267
273
    while (query.next()) {
268
274
        QString accountId = query.value(0).toString();
269
275
        QString participantId = query.value(1).toString();
 
276
        QString alias = query.value(2).toString();
 
277
        QVariantMap properties;
 
278
        if (!alias.isEmpty()) {
 
279
            properties[History::FieldAlias] = alias;
 
280
        }
270
281
        // we don't care about the results, as long as the contact data is present in the cache for
271
282
        // future usage.
272
 
        History::ContactMatcher::instance()->contactInfo(accountId, participantId, true);
 
283
        History::ContactMatcher::instance()->contactInfo(accountId, participantId, true, properties);
273
284
    }
274
285
 
275
286
    updateGroupedThreadsCache();
295
306
    return new SQLiteHistoryEventView(this, type, sort, filter);
296
307
}
297
308
 
 
309
QVariantMap SQLiteHistoryPlugin::threadForProperties(const QString &accountId,
 
310
                                                       History::EventType type,
 
311
                                                       const QVariantMap &properties,
 
312
                                                       History::MatchFlags matchFlags)
 
313
{
 
314
    if (properties.isEmpty()) {
 
315
        return QVariantMap();
 
316
    }
 
317
 
 
318
    QSqlQuery query(SQLiteDatabase::instance()->database());
 
319
 
 
320
    History::ChatType chatType = (History::ChatType)properties[History::FieldChatType].toUInt();
 
321
 
 
322
    if (chatType == History::ChatTypeRoom) {
 
323
        QString threadId = properties[History::FieldThreadId].toString();
 
324
        if (threadId.isEmpty()) {
 
325
            return QVariantMap();
 
326
        }
 
327
        return getSingleThread(type, accountId, threadId);
 
328
    }
 
329
 
 
330
    History::Participants participants = History::Participants::fromVariant(properties[History::FieldParticipantIds]);
 
331
    // if chatType != Room, then we select the thread based on the participant list.
 
332
    return threadForParticipants(accountId, type, participants.identifiers(), matchFlags);
 
333
}
 
334
 
298
335
QVariantMap SQLiteHistoryPlugin::threadForParticipants(const QString &accountId,
299
336
                                                       History::EventType type,
300
337
                                                       const QStringList &participants,
310
347
    // select all the threads the first participant is listed in, and from that list
311
348
    // check if any of the threads has all the other participants listed
312
349
    // FIXME: find a better way to do this
313
 
    QString queryString("SELECT threadId FROM thread_participants WHERE %1 AND type=:type AND accountId=:accountId");
 
350
    QString queryString("SELECT threadId FROM thread_participants WHERE %1 AND type=:type AND accountId=:accountId "
 
351
                        "AND (SELECT chatType FROM threads WHERE threads.threadId=thread_participants.threadId AND "
 
352
                        "      threads.type=thread_participants.type)!=:chatType");
314
353
 
315
354
    // FIXME: for now we just compare differently when using MatchPhoneNumber
316
355
    QString firstParticipant = participants.first();
324
363
    query.bindValue(":participantId", firstParticipant);
325
364
    query.bindValue(":type", type);
326
365
    query.bindValue(":accountId", accountId);
 
366
    // we don't want to accidentally return a chat room for a multi-recipient conversation
 
367
    query.bindValue(":chatType", (int)History::ChatTypeRoom);
 
368
 
327
369
    if (!query.exec()) {
328
370
        qCritical() << "Error:" << query.lastError() << query.lastQuery();
329
371
        return QVariantMap();
469
511
    return result;
470
512
}
471
513
 
472
 
// Writer
473
 
QVariantMap SQLiteHistoryPlugin::createThreadForParticipants(const QString &accountId, History::EventType type, const QStringList &participants)
 
514
bool SQLiteHistoryPlugin::updateRoomParticipants(const QString &accountId, const QString &threadId, History::EventType type, const QVariantList &participants)
 
515
{
 
516
    QSqlQuery query(SQLiteDatabase::instance()->database());
 
517
    if (accountId.isEmpty() || threadId.isEmpty()) {
 
518
        return false;
 
519
    }
 
520
 
 
521
    SQLiteDatabase::instance()->beginTransation();
 
522
    QString deleteString("DELETE FROM thread_participants WHERE threadId=:threadId AND type=:type AND accountId=:accountId");
 
523
    query.prepare(deleteString);
 
524
    query.bindValue(":accountId", accountId);
 
525
    query.bindValue(":threadId", threadId);
 
526
    query.bindValue(":type", type);
 
527
    if (!query.exec()) {
 
528
        qCritical() << "Error removing old participants:" << query.lastError() << query.lastQuery();
 
529
        SQLiteDatabase::instance()->rollbackTransaction();
 
530
        return false;
 
531
    }
 
532
 
 
533
    // and insert the participants
 
534
    Q_FOREACH(const QVariant &participantVariant, participants) {
 
535
        QVariantMap participant = participantVariant.toMap();
 
536
        query.prepare("INSERT INTO thread_participants (accountId, threadId, type, participantId, normalizedId, alias, state, roles)"
 
537
                      "VALUES (:accountId, :threadId, :type, :participantId, :normalizedId, :alias, :state, :roles)");
 
538
        query.bindValue(":accountId", accountId);
 
539
        query.bindValue(":threadId", threadId);
 
540
        query.bindValue(":type", type);
 
541
        query.bindValue(":participantId", participant["identifier"].toString());
 
542
        query.bindValue(":normalizedId", participant["identifier"].toString());
 
543
        query.bindValue(":alias", participant["alias"].toString());
 
544
        query.bindValue(":state", participant["state"].toUInt());
 
545
        query.bindValue(":roles", participant["roles"].toUInt());
 
546
        if (!query.exec()) {
 
547
            qCritical() << "Error:" << query.lastError() << query.lastQuery();
 
548
            SQLiteDatabase::instance()->rollbackTransaction();
 
549
            return false;
 
550
        }
 
551
    }
 
552
 
 
553
    if (!SQLiteDatabase::instance()->finishTransaction()) {
 
554
        qCritical() << "Failed to commit the transaction.";
 
555
        return false;
 
556
    }
 
557
 
 
558
    QVariantMap existingThread = getSingleThread(type,
 
559
                                                 accountId,
 
560
                                                 threadId,
 
561
                                                 QVariantMap());
 
562
 
 
563
    if (!existingThread.isEmpty()) {
 
564
        addThreadsToCache(QList<QVariantMap>() << existingThread);
 
565
    }
 
566
 
 
567
    return true;
 
568
}
 
569
 
 
570
bool SQLiteHistoryPlugin::updateRoomParticipantsRoles(const QString &accountId, const QString &threadId, History::EventType type, const QVariantMap &participantsRoles)
 
571
{
 
572
    QSqlQuery query(SQLiteDatabase::instance()->database());
 
573
    if (accountId.isEmpty() || threadId.isEmpty()) {
 
574
        return false;
 
575
    }
 
576
 
 
577
    SQLiteDatabase::instance()->beginTransation();
 
578
    Q_FOREACH(const QString &participantId, participantsRoles.keys()) {
 
579
        query.prepare("UPDATE thread_participants SET roles=:roles WHERE accountId=:accountId AND threadId=:threadId AND type=:type AND participantId=:participantId");
 
580
        query.bindValue(":roles", participantsRoles.value(participantId).toUInt());
 
581
        query.bindValue(":accountId", accountId);
 
582
        query.bindValue(":threadId", threadId);
 
583
        query.bindValue(":type", type);
 
584
        query.bindValue(":participantId", participantId);
 
585
        if (!query.exec()) {
 
586
            qCritical() << "Error:" << query.lastError() << query.lastQuery();
 
587
            SQLiteDatabase::instance()->rollbackTransaction();
 
588
            return false;
 
589
        }
 
590
    }
 
591
 
 
592
    if (!SQLiteDatabase::instance()->finishTransaction()) {
 
593
        qCritical() << "Failed to commit the transaction.";
 
594
        return false;
 
595
    }
 
596
 
 
597
    QVariantMap existingThread = getSingleThread(type,
 
598
                                                 accountId,
 
599
                                                 threadId,
 
600
                                                 QVariantMap());
 
601
 
 
602
    if (!existingThread.isEmpty()) {
 
603
        addThreadsToCache(QList<QVariantMap>() << existingThread);
 
604
    }
 
605
 
 
606
    return true;
 
607
}
 
608
 
 
609
bool SQLiteHistoryPlugin::updateRoomInfo(const QString &accountId, const QString &threadId, History::EventType type, const QVariantMap &properties, const QStringList &invalidated)
 
610
{
 
611
    QSqlQuery query(SQLiteDatabase::instance()->database());
 
612
 
 
613
    if (threadId.isEmpty() || accountId.isEmpty()) {
 
614
        return false;
 
615
    }
 
616
 
 
617
    SQLiteDatabase::instance()->beginTransation();
 
618
 
 
619
    QDateTime creationTimestamp = QDateTime::fromTime_t(properties["CreationTimestamp"].toUInt());
 
620
    QDateTime timestamp = QDateTime::fromTime_t(properties["Timestamp"].toUInt());
 
621
 
 
622
    QVariantMap propertyMapping;
 
623
    propertyMapping["RoomName"] = "roomName";
 
624
    propertyMapping["Server"] = "server";
 
625
    propertyMapping["Creator"] = "creator";
 
626
    propertyMapping["CreationTimestamp"] = "creationTimestamp";
 
627
    propertyMapping["Anonymous"] = "anonymous";
 
628
    propertyMapping["InviteOnly"] = "inviteOnly";
 
629
    propertyMapping["Limit"] = "participantLimit";
 
630
    propertyMapping["Moderated"] = "moderated";
 
631
    propertyMapping["Title"] = "title";
 
632
    propertyMapping["Description"] = "description";
 
633
    propertyMapping["Persistent"] = "persistent";
 
634
    propertyMapping["Private"] = "private";
 
635
    propertyMapping["PasswordProtected"] = "passwordProtected";
 
636
    propertyMapping["Password"] = "password";
 
637
    propertyMapping["PasswordHint"] = "passwordHint";
 
638
    propertyMapping["CanUpdateConfiguration"] = "canUpdateConfiguration";
 
639
    propertyMapping["Subject"] = "subject";
 
640
    propertyMapping["Actor"] = "actor";
 
641
    propertyMapping["Timestamp"] = "timestamp";
 
642
    propertyMapping["Joined"] = "joined";
 
643
    propertyMapping["SelfRoles"] = "selfRoles";
 
644
 
 
645
    QStringList changedPropListValues;
 
646
    // populate sql query
 
647
    Q_FOREACH (const QString &key, properties.keys()) {
 
648
        if (propertyMapping.contains(key)) {
 
649
            QString prop = propertyMapping[key].toString();
 
650
            changedPropListValues << QString(prop+"=:"+ prop);
 
651
        }
 
652
    }
 
653
    if (changedPropListValues.isEmpty()) {
 
654
       return false;
 
655
    }
 
656
 
 
657
    query.prepare("UPDATE chat_room_info SET "+ changedPropListValues.join(", ")+" WHERE accountId=:accountId AND threadId=:threadId AND type=:type");
 
658
    query.bindValue(":accountId", accountId);
 
659
    query.bindValue(":threadId", threadId);
 
660
    query.bindValue(":type", (int) type);
 
661
    query.bindValue(":roomName", properties["RoomName"].toString());
 
662
    query.bindValue(":server", properties["Server"].toString());
 
663
    query.bindValue(":creator", properties["Creator"].toString());
 
664
    query.bindValue(":creationTimestamp", creationTimestamp.toUTC().toString(timestampFormat));
 
665
    query.bindValue(":anonymous", properties["Anonymous"].toBool());
 
666
    query.bindValue(":inviteOnly", properties["InviteOnly"].toBool());
 
667
    query.bindValue(":participantLimit", properties["Limit"].toInt());
 
668
    query.bindValue(":moderated", properties["Moderated"].toBool());
 
669
    query.bindValue(":title", properties["Title"].toString());
 
670
    query.bindValue(":description", properties["Description"].toString());
 
671
    query.bindValue(":persistent", properties["Persistent"].toBool());
 
672
    query.bindValue(":private", properties["Private"].toBool());
 
673
    query.bindValue(":passwordProtected", properties["PasswordProtected"].toBool());
 
674
    query.bindValue(":password", properties["Password"].toString());
 
675
    query.bindValue(":passwordHint", properties["PasswordHint"].toString());
 
676
    query.bindValue(":canUpdateConfiguration", properties["CanUpdateConfiguration"].toBool());
 
677
    query.bindValue(":subject", properties["Subject"].toString());
 
678
    query.bindValue(":actor", properties["Actor"].toString());
 
679
    query.bindValue(":timestamp", timestamp.toUTC().toString(timestampFormat));
 
680
    query.bindValue(":joined", properties["Joined"].toBool());
 
681
    query.bindValue(":selfRoles", properties["SelfRoles"].toInt());
 
682
 
 
683
    if (!query.exec()) {
 
684
        qCritical() << "Error:" << query.lastError() << query.lastQuery();
 
685
        SQLiteDatabase::instance()->rollbackTransaction();
 
686
        return false;
 
687
    }
 
688
 
 
689
    if (!SQLiteDatabase::instance()->finishTransaction()) {
 
690
        qCritical() << "Failed to commit the transaction.";
 
691
        return false;
 
692
    }
 
693
 
 
694
    QVariantMap existingThread = getSingleThread(type,
 
695
                                                 accountId,
 
696
                                                 threadId,
 
697
                                                 QVariantMap());
 
698
 
 
699
    if (!existingThread.isEmpty()) {
 
700
        addThreadsToCache(QList<QVariantMap>() << existingThread);
 
701
    }
 
702
 
 
703
    return true;
 
704
}
 
705
 
 
706
QVariantMap SQLiteHistoryPlugin::createThreadForProperties(const QString &accountId, History::EventType type, const QVariantMap &properties)
474
707
{
475
708
    // WARNING: this function does NOT test to check if the thread is already created, you should check using HistoryReader::threadForParticipants()
476
709
 
477
710
    QVariantMap thread;
 
711
    History::Participants participants = History::Participants::fromVariant(properties[History::FieldParticipantIds]);
478
712
 
479
713
    // Create a new thread
480
714
    // FIXME: define what the threadId will be
481
 
    QString threadId = participants.join("%");
 
715
    QString threadId;
 
716
    History::ChatType chatType = (History::ChatType)properties[History::FieldChatType].toInt();
 
717
    QVariantMap chatRoomInfo;
 
718
 
 
719
    SQLiteDatabase::instance()->beginTransation();
 
720
 
 
721
    if (chatType == History::ChatTypeRoom) {
 
722
        threadId = properties[History::FieldThreadId].toString();
 
723
        // we cannot save chat room without threadId
 
724
        if (accountId.isEmpty() || threadId.isEmpty()) {
 
725
            SQLiteDatabase::instance()->rollbackTransaction();
 
726
            return thread;
 
727
        }
 
728
        chatRoomInfo = properties[History::FieldChatRoomInfo].toMap();
 
729
        QSqlQuery query(SQLiteDatabase::instance()->database());
 
730
 
 
731
        QDateTime creationTimestamp = QDateTime::fromTime_t(chatRoomInfo["CreationTimestamp"].toUInt());
 
732
        QDateTime timestamp = QDateTime::fromTime_t(chatRoomInfo["Timestamp"].toUInt());
 
733
 
 
734
        query.prepare("INSERT INTO chat_room_info (accountId, threadId, type, roomName, server, creator, creationTimestamp, anonymous, inviteOnly, participantLimit, moderated, title, description, persistent, private, passwordProtected, password, passwordHint, canUpdateConfiguration, subject, actor, timestamp, joined, selfRoles) "
 
735
                      "VALUES (:accountId, :threadId, :type, :roomName, :server, :creator, :creationTimestamp, :anonymous, :inviteOnly, :participantLimit, :moderated, :title, :description, :persistent, :private, :passwordProtected, :password, :passwordHint, :canUpdateConfiguration, :subject, :actor, :timestamp, :joined, :selfRoles)");
 
736
        query.bindValue(":accountId", accountId);
 
737
        query.bindValue(":threadId", threadId);
 
738
        query.bindValue(":type", (int) type);
 
739
        query.bindValue(":roomName", chatRoomInfo["RoomName"].toString());
 
740
        query.bindValue(":server", chatRoomInfo["Server"].toString());
 
741
        query.bindValue(":creator", chatRoomInfo["Creator"].toString());
 
742
        query.bindValue(":creationTimestamp", creationTimestamp.toUTC().toString(timestampFormat));
 
743
        query.bindValue(":anonymous", chatRoomInfo["Anonymous"].toBool());
 
744
        query.bindValue(":inviteOnly", chatRoomInfo["InviteOnly"].toBool());
 
745
        query.bindValue(":participantLimit", chatRoomInfo["Limit"].toInt());
 
746
        query.bindValue(":moderated", chatRoomInfo["Moderated"].toBool());
 
747
        query.bindValue(":title", chatRoomInfo["Title"].toString());
 
748
        query.bindValue(":description", chatRoomInfo["Description"].toString());
 
749
        query.bindValue(":persistent", chatRoomInfo["Persistent"].toBool());
 
750
        query.bindValue(":private", chatRoomInfo["Private"].toBool());
 
751
        query.bindValue(":passwordProtected", chatRoomInfo["PasswordProtected"].toBool());
 
752
        query.bindValue(":password", chatRoomInfo["Password"].toString());
 
753
        query.bindValue(":passwordHint", chatRoomInfo["PasswordHint"].toString());
 
754
        query.bindValue(":canUpdateConfiguration", chatRoomInfo["CanUpdateConfiguration"].toBool());
 
755
        query.bindValue(":subject", chatRoomInfo["Subject"].toString());
 
756
        query.bindValue(":actor", chatRoomInfo["Actor"].toString());
 
757
        query.bindValue(":timestamp", timestamp.toUTC().toString(timestampFormat));
 
758
        query.bindValue(":joined", chatRoomInfo["Joined"].toBool());
 
759
        query.bindValue(":selfRoles", chatRoomInfo["SelfRoles"].toInt());
 
760
 
 
761
        if (!query.exec()) {
 
762
            qCritical() << "Error:" << query.lastError() << query.lastQuery();
 
763
            SQLiteDatabase::instance()->rollbackTransaction();
 
764
            return QVariantMap();
 
765
        }
 
766
        for (QVariantMap::iterator iter = chatRoomInfo.begin(); iter != chatRoomInfo.end();) {
 
767
            if (!iter.value().isValid()) {
 
768
                iter = chatRoomInfo.erase(iter);
 
769
            } else {
 
770
                iter++;
 
771
            }
 
772
        }
 
773
        thread[History::FieldChatRoomInfo] = chatRoomInfo;
 
774
    } else if (chatType == History::ChatTypeContact) {
 
775
        threadId = participants.identifiers().join("%");
 
776
    } else {
 
777
        threadId = QString("broadcast:%1").arg(QString(QCryptographicHash::hash(participants.identifiers().join(";").toLocal8Bit(),QCryptographicHash::Md5).toHex()));;
 
778
    }
482
779
 
483
780
    QSqlQuery query(SQLiteDatabase::instance()->database());
484
 
    query.prepare("INSERT INTO threads (accountId, threadId, type, count, unreadCount)"
485
 
                  "VALUES (:accountId, :threadId, :type, :count, :unreadCount)");
 
781
    query.prepare("INSERT INTO threads (accountId, threadId, type, count, unreadCount, chatType, lastEventTimestamp)"
 
782
                  "VALUES (:accountId, :threadId, :type, :count, :unreadCount, :chatType, :lastEventTimestamp)");
486
783
    query.bindValue(":accountId", accountId);
487
784
    query.bindValue(":threadId", threadId);
488
785
    query.bindValue(":type", (int) type);
489
786
    query.bindValue(":count", 0);
490
787
    query.bindValue(":unreadCount", 0);
 
788
    query.bindValue(":chatType", (int) chatType);
 
789
    // make sure threads are created with an up-to-date timestamp
 
790
    query.bindValue(":lastEventTimestamp", QDateTime::currentDateTimeUtc().toString(timestampFormat));
491
791
    if (!query.exec()) {
492
792
        qCritical() << "Error:" << query.lastError() << query.lastQuery();
 
793
        SQLiteDatabase::instance()->rollbackTransaction();
493
794
        return QVariantMap();
494
795
    }
495
796
 
496
797
    // and insert the participants
497
 
    Q_FOREACH(const QString &participant, participants) {
498
 
        query.prepare("INSERT INTO thread_participants (accountId, threadId, type, participantId, normalizedId)"
499
 
                      "VALUES (:accountId, :threadId, :type, :participantId, :normalizedId)");
 
798
    Q_FOREACH(const History::Participant &participant, participants) {
 
799
        query.prepare("INSERT INTO thread_participants (accountId, threadId, type, participantId, normalizedId, alias, state, roles)"
 
800
                      "VALUES (:accountId, :threadId, :type, :participantId, :normalizedId, :alias, :state, :roles)");
500
801
        query.bindValue(":accountId", accountId);
501
802
        query.bindValue(":threadId", threadId);
502
803
        query.bindValue(":type", type);
503
 
        query.bindValue(":participantId", participant);
504
 
        query.bindValue(":normalizedId", History::Utils::normalizeId(accountId, participant));
 
804
        query.bindValue(":participantId", participant.identifier());
 
805
        query.bindValue(":normalizedId", History::Utils::normalizeId(accountId, participant.identifier()));
 
806
        query.bindValue(":alias", participant.alias());
 
807
        query.bindValue(":state", participant.state());
 
808
        query.bindValue(":roles", participant.roles());
505
809
        if (!query.exec()) {
506
810
            qCritical() << "Error:" << query.lastError() << query.lastQuery();
 
811
            SQLiteDatabase::instance()->rollbackTransaction();
507
812
            return QVariantMap();
508
813
        }
509
814
    }
510
815
 
 
816
    if (!SQLiteDatabase::instance()->finishTransaction()) {
 
817
        qCritical() << "Failed to commit the transaction.";
 
818
        return QVariantMap();
 
819
    }
 
820
 
511
821
    // and finally create the thread
512
822
    thread[History::FieldAccountId] = accountId;
513
823
    thread[History::FieldThreadId] = threadId;
514
824
    thread[History::FieldType] = (int) type;
515
 
    thread[History::FieldParticipants] = History::ContactMatcher::instance()->contactInfo(accountId, participants, true);
 
825
    QVariantList contactList;
 
826
    QVariantList contactInfo = History::ContactMatcher::instance()->contactInfo(accountId, participants.identifiers(), true);
 
827
    for (int i = 0; i < participants.count(); ++i) {
 
828
        QVariantMap map = contactInfo[i].toMap();
 
829
        History::Participant participant = participants[i];
 
830
        map["state"] = participant.state();
 
831
        map["roles"] = participant.roles();
 
832
        contactList << map;
 
833
    }
 
834
    thread[History::FieldParticipants] = contactList;
516
835
    thread[History::FieldCount] = 0;
517
836
    thread[History::FieldUnreadCount] = 0;
 
837
    thread[History::FieldChatType] = (int)chatType;
518
838
 
519
839
    addThreadsToCache(QList<QVariantMap>() << thread);
520
840
 
521
841
    return thread;
522
842
}
523
843
 
 
844
// Writer
 
845
QVariantMap SQLiteHistoryPlugin::createThreadForParticipants(const QString &accountId, History::EventType type, const QStringList &participants)
 
846
{
 
847
    QVariantMap properties;
 
848
    properties[History::FieldParticipantIds] = participants;
 
849
    properties[History::FieldChatType] = participants.size() != 1 ? History::ChatTypeNone : History::ChatTypeContact;
 
850
    return createThreadForProperties(accountId, type, properties);
 
851
}
 
852
 
524
853
bool SQLiteHistoryPlugin::removeThread(const QVariantMap &thread)
525
854
{
526
855
    QSqlQuery query(SQLiteDatabase::instance()->database());
550
879
                                               event[History::FieldThreadId].toString(),
551
880
                                               event[History::FieldEventId].toString());
552
881
 
 
882
    SQLiteDatabase::instance()->beginTransation();
 
883
 
553
884
    History::EventWriteResult result;
554
885
    if (existingEvent.isEmpty()) {
555
886
        // create new
556
 
        query.prepare("INSERT INTO text_events (accountId, threadId, eventId, senderId, timestamp, newEvent, message, messageType, messageStatus, readTimestamp, subject) "
557
 
                      "VALUES (:accountId, :threadId, :eventId, :senderId, :timestamp, :newEvent, :message, :messageType, :messageStatus, :readTimestamp, :subject)");
 
887
        query.prepare("INSERT INTO text_events (accountId, threadId, eventId, senderId, timestamp, newEvent, message, messageType, messageStatus, readTimestamp, subject, informationType)"
 
888
                      "VALUES (:accountId, :threadId, :eventId, :senderId, :timestamp, :newEvent, :message, :messageType, :messageStatus, :readTimestamp, :subject, :informationType)");
558
889
        result = History::EventWriteCreated;
559
890
    } else {
560
891
        // update existing event
561
 
        query.prepare("UPDATE text_events SET senderId=:senderId, timestamp=:timestamp, newEvent=:newEvent, message=:message, messageType=:messageType,"
562
 
                      "messageStatus=:messageStatus, readTimestamp=:readTimestamp, subject=:subject WHERE accountId=:accountId AND threadId=:threadId AND eventId=:eventId");
 
892
        query.prepare("UPDATE text_events SET senderId=:senderId, timestamp=:timestamp, newEvent=:newEvent, message=:message, messageType=:messageType, informationType=:informationType, "
 
893
                      "messageStatus=:messageStatus, readTimestamp=:readTimestamp, subject=:subject, informationType=:informationType WHERE accountId=:accountId AND threadId=:threadId AND eventId=:eventId");
563
894
        result = History::EventWriteModified;
564
895
    }
565
896
 
574
905
    query.bindValue(":messageStatus", event[History::FieldMessageStatus]);
575
906
    query.bindValue(":readTimestamp", event[History::FieldReadTimestamp].toDateTime().toUTC());
576
907
    query.bindValue(":subject", event[History::FieldSubject].toString());
 
908
    query.bindValue(":informationType", event[History::FieldInformationType].toInt());
577
909
 
578
910
    if (!query.exec()) {
579
911
        qCritical() << "Failed to save the text event: Error:" << query.lastError() << query.lastQuery();
 
912
        SQLiteDatabase::instance()->rollbackTransaction();
580
913
        return History::EventWriteError;
581
914
    }
582
915
 
592
925
            query.bindValue(":eventId", event[History::FieldEventId]);
593
926
            if (!query.exec()) {
594
927
                qCritical() << "Could not erase previous attachments. Error:" << query.lastError() << query.lastQuery();
 
928
                SQLiteDatabase::instance()->rollbackTransaction();
595
929
                return History::EventWriteError;
596
930
            }
597
931
        }
608
942
            query.bindValue(":status", attachment[History::FieldStatus]);
609
943
            if (!query.exec()) {
610
944
                qCritical() << "Failed to save attachment to database" << query.lastError() << attachment;
 
945
                SQLiteDatabase::instance()->rollbackTransaction();
611
946
                return History::EventWriteError;
612
947
            }
613
948
        }
614
949
    }
615
950
 
 
951
    if (!SQLiteDatabase::instance()->finishTransaction()) {
 
952
        qCritical() << "Failed to commit transaction.";
 
953
        return History::EventWriteError;
 
954
    }
 
955
 
616
956
    if (result == History::EventWriteModified || result == History::EventWriteCreated) {
617
957
        QVariantMap existingThread = getSingleThread((History::EventType) event[History::FieldType].toInt(),
618
958
                                                     event[History::FieldAccountId].toString(),
750
1090
           << "threads.threadId"
751
1091
           << "threads.lastEventId"
752
1092
           << "threads.count"
753
 
           << "threads.unreadCount";
 
1093
           << "threads.unreadCount"
 
1094
           << "threads.lastEventTimestamp";
754
1095
 
755
1096
    // get the participants in the query already
756
1097
    fields << "(SELECT group_concat(thread_participants.participantId,  \"|,|\") "
758
1099
              "AND thread_participants.threadId=threads.threadId "
759
1100
              "AND thread_participants.type=threads.type GROUP BY accountId,threadId,type) as participants";
760
1101
 
 
1102
    fields << "(SELECT group_concat(thread_participants.state,  \"|,|\") "
 
1103
              "FROM thread_participants WHERE thread_participants.accountId=threads.accountId "
 
1104
              "AND thread_participants.threadId=threads.threadId "
 
1105
              "AND thread_participants.type=threads.type GROUP BY accountId,threadId,type) as state";
 
1106
 
 
1107
    fields << "(SELECT group_concat(thread_participants.roles,  \"|,|\") "
 
1108
              "FROM thread_participants WHERE thread_participants.accountId=threads.accountId "
 
1109
              "AND thread_participants.threadId=threads.threadId "
 
1110
              "AND thread_participants.type=threads.type GROUP BY accountId,threadId,type) as roles";
 
1111
 
761
1112
    QStringList extraFields;
762
1113
    QString table;
763
1114
 
764
1115
    switch (type) {
765
1116
    case History::EventTypeText:
766
1117
        table = "text_events";
767
 
        extraFields << "text_events.message" << "text_events.messageType" << "text_events.messageStatus" << "text_events.readTimestamp";
 
1118
        extraFields << "text_events.message" << "text_events.messageType" << "text_events.messageStatus" << "text_events.readTimestamp" << "chatType" << "text_events.subject" << "text_events.informationType";
768
1119
        break;
769
1120
    case History::EventTypeVoice:
770
1121
        table = "voice_events";
773
1124
    }
774
1125
 
775
1126
    fields << QString("%1.senderId").arg(table)
776
 
           << QString("%1.timestamp").arg(table)
777
1127
           << QString("%1.newEvent").arg(table);
778
1128
    fields << extraFields;
779
1129
 
820
1170
        thread[History::FieldEventId] = query.value(2);
821
1171
        thread[History::FieldCount] = query.value(3);
822
1172
        thread[History::FieldUnreadCount] = query.value(4);
823
 
        QStringList participants = query.value(5).toString().split("|,|");
824
 
        thread[History::FieldParticipants] = History::ContactMatcher::instance()->contactInfo(accountId, participants, true);
 
1173
        QStringList participants = query.value(6).toString().split("|,|", QString::SkipEmptyParts);
 
1174
        QList<int> participantStatus;
 
1175
        QStringList participantStatusString = query.value(7).toString().split("|,|", QString::SkipEmptyParts);
 
1176
        Q_FOREACH(const QString &statusString, participantStatusString) {
 
1177
            participantStatus << statusString.toUInt();
 
1178
        }
 
1179
        QStringList participantRolesString = query.value(8).toString().split("|,|", QString::SkipEmptyParts);
 
1180
        QList<int> participantRoles;
 
1181
        Q_FOREACH(const QString &rolesString, participantRolesString) {
 
1182
            participantRoles << rolesString.toUInt();
 
1183
        }
 
1184
        QVariantList contactList;
 
1185
        QVariantList contactInfo = History::ContactMatcher::instance()->contactInfo(accountId, participants, true);
 
1186
        for (int i = 0; i < contactInfo.count(); ++i) {
 
1187
            QVariantMap map = contactInfo[i].toMap();
 
1188
            map["state"] = participantStatus[i];
 
1189
            map["roles"] = participantRoles[i];
 
1190
            contactList << map;
 
1191
        }
 
1192
        thread[History::FieldParticipants] = contactList;
825
1193
 
826
1194
        // the generic event fields
827
 
        thread[History::FieldSenderId] = query.value(6);
828
 
        thread[History::FieldTimestamp] = toLocalTimeString(query.value(7).toDateTime());
829
 
        thread[History::FieldNewEvent] = query.value(8).toBool();
 
1195
        thread[History::FieldSenderId] = query.value(9);
 
1196
        thread[History::FieldTimestamp] = toLocalTimeString(query.value(5).toDateTime());
 
1197
        thread[History::FieldNewEvent] = query.value(10).toBool();
830
1198
 
831
1199
        // the next step is to get the last event
832
1200
        switch (type) {
857
1225
                thread[History::FieldAttachments] = QVariant::fromValue(attachments);
858
1226
                attachments.clear();
859
1227
            }
860
 
            thread[History::FieldMessage] = query.value(9);
861
 
            thread[History::FieldMessageType] = query.value(10);
862
 
            thread[History::FieldMessageStatus] = query.value(11);
863
 
            thread[History::FieldReadTimestamp] = toLocalTimeString(query.value(12).toDateTime());
 
1228
            thread[History::FieldMessage] = query.value(11);
 
1229
            thread[History::FieldMessageType] = query.value(12);
 
1230
            thread[History::FieldMessageStatus] = query.value(13);
 
1231
            thread[History::FieldReadTimestamp] = toLocalTimeString(query.value(14).toDateTime());
 
1232
            thread[History::FieldChatType] = query.value(15).toUInt();
 
1233
 
 
1234
            if (thread[History::FieldChatType].toInt() == 2) {
 
1235
                QVariantMap chatRoomInfo;
 
1236
                QSqlQuery query1(SQLiteDatabase::instance()->database());
 
1237
 
 
1238
                query1.prepare("SELECT roomName, server, creator, creationTimestamp, anonymous, inviteOnly, participantLimit, moderated, title, description, persistent, private, passwordProtected, password, passwordHint, canUpdateConfiguration, subject, actor, timestamp, joined, selfRoles FROM chat_room_info WHERE accountId=:accountId AND threadId=:threadId AND type=:type LIMIT 1");
 
1239
                query1.bindValue(":accountId", thread[History::FieldAccountId]);
 
1240
                query1.bindValue(":threadId", thread[History::FieldThreadId]);
 
1241
                query1.bindValue(":type", thread[History::FieldType].toInt());
 
1242
 
 
1243
                if (!query1.exec()) {
 
1244
                    qCritical() << "Failed to get chat room info for thread: Error:" << query1.lastError() << query1.lastQuery();
 
1245
                    break;
 
1246
                }
 
1247
                query1.next();
 
1248
 
 
1249
                if (query1.value(0).isValid())
 
1250
                    chatRoomInfo["RoomName"] = query1.value(0);
 
1251
                if (query1.value(1).isValid())
 
1252
                    chatRoomInfo["Server"] = query1.value(1);
 
1253
                if (query1.value(2).isValid())
 
1254
                    chatRoomInfo["Creator"] = query1.value(2);
 
1255
                if (query1.value(3).isValid())
 
1256
                    chatRoomInfo["CreationTimestamp"] = toLocalTimeString(query1.value(3).toDateTime());
 
1257
                if (query1.value(4).isValid())
 
1258
                    chatRoomInfo["Anonymous"] = query1.value(4).toBool();
 
1259
                if (query1.value(5).isValid())
 
1260
                    chatRoomInfo["InviteOnly"] = query1.value(5).toBool();
 
1261
                if (query1.value(6).isValid())
 
1262
                    chatRoomInfo["Limit"] = query1.value(6).toInt();
 
1263
                if (query1.value(7).isValid())
 
1264
                    chatRoomInfo["Moderated"] = query1.value(7).toBool();
 
1265
                if (query1.value(8).isValid())
 
1266
                    chatRoomInfo["Title"] = query1.value(8);
 
1267
                if (query1.value(9).isValid())
 
1268
                    chatRoomInfo["Description"] = query1.value(9);
 
1269
                if (query1.value(10).isValid())
 
1270
                    chatRoomInfo["Persistent"] = query1.value(10).toBool();
 
1271
                if (query1.value(11).isValid())
 
1272
                    chatRoomInfo["Private"] = query1.value(11).toBool();
 
1273
                if (query1.value(12).isValid())
 
1274
                    chatRoomInfo["PasswordProtected"] = query1.value(12).toBool();
 
1275
                if (query1.value(13).isValid())
 
1276
                    chatRoomInfo["Password"] = query1.value(13);
 
1277
                if (query1.value(14).isValid())
 
1278
                    chatRoomInfo["PasswordHint"] = query1.value(14);
 
1279
                if (query1.value(15).isValid())
 
1280
                    chatRoomInfo["CanUpdateConfiguration"] = query1.value(15).toBool();
 
1281
                if (query1.value(16).isValid())
 
1282
                    chatRoomInfo["Subject"] = query1.value(16);
 
1283
                if (query1.value(17).isValid())
 
1284
                    chatRoomInfo["Actor"] = query1.value(17);
 
1285
                if (query1.value(18).isValid())
 
1286
                    chatRoomInfo["Timestamp"] = toLocalTimeString(query1.value(18).toDateTime());
 
1287
                if (query1.value(19).isValid())
 
1288
                    chatRoomInfo["Joined"] = query1.value(19).toBool();
 
1289
                if (query1.value(20).isValid())
 
1290
                    chatRoomInfo["SelfRoles"] = query1.value(20).toInt();
 
1291
 
 
1292
                thread[History::FieldChatRoomInfo] = chatRoomInfo;
 
1293
            }
864
1294
            break;
865
1295
        case History::EventTypeVoice:
866
 
            thread[History::FieldMissed] = query.value(10);
867
 
            thread[History::FieldDuration] = query.value(9);
868
 
            thread[History::FieldRemoteParticipant] = History::ContactMatcher::instance()->contactInfo(accountId, query.value(11).toString(), true);
 
1296
            thread[History::FieldMissed] = query.value(12);
 
1297
            thread[History::FieldDuration] = query.value(11);
 
1298
            thread[History::FieldRemoteParticipant] = History::ContactMatcher::instance()->contactInfo(accountId, query.value(13).toString(), true);
869
1299
            break;
870
1300
        }
871
1301
        threads << thread;
889
1319
    case History::EventTypeText:
890
1320
        participantsField = participantsField.arg("text_events", QString::number(type));
891
1321
        queryText = QString("SELECT accountId, threadId, eventId, senderId, timestamp, newEvent, %1, "
892
 
                            "message, messageType, messageStatus, readTimestamp, subject FROM text_events %2 %3").arg(participantsField, modifiedCondition, order);
 
1322
                            "message, messageType, messageStatus, readTimestamp, subject, informationType FROM text_events %2 %3").arg(participantsField, modifiedCondition, order);
893
1323
        break;
894
1324
    case History::EventTypeVoice:
895
1325
        participantsField = participantsField.arg("voice_events", QString::number(type));
960
1390
            event[History::FieldMessageType] = query.value(8);
961
1391
            event[History::FieldMessageStatus] = query.value(9);
962
1392
            event[History::FieldReadTimestamp] = toLocalTimeString(query.value(10).toDateTime());
 
1393
            if (!query.value(11).toString().isEmpty()) {
 
1394
                event[History::FieldSubject] = query.value(11).toString();
 
1395
            }
 
1396
            event[History::FieldInformationType] = query.value(12).toInt();
963
1397
            break;
964
1398
        case History::EventTypeVoice:
965
1399
            event[History::FieldDuration] = query.value(7).toInt();
975
1409
 
976
1410
QString SQLiteHistoryPlugin::toLocalTimeString(const QDateTime &timestamp)
977
1411
{
978
 
    return QDateTime(timestamp.date(), timestamp.time(), Qt::UTC).toLocalTime().toString("yyyy-MM-ddTHH:mm:ss.zzz");
 
1412
    return QDateTime(timestamp.date(), timestamp.time(), Qt::UTC).toLocalTime().toString(timestampFormat);
979
1413
}
980
1414
 
981
1415
QString SQLiteHistoryPlugin::filterToString(const History::Filter &filter, QVariantMap &bindValues, const QString &propertyPrefix) const