File indexing completed on 2023-05-30 11:40:28

0001 /*
0002     Copyright (C) 2014  David Edmundson <kde@davidedmundson.co.uk>
0003     Copyright (C) 2014  Alexandr Akulich <akulichalexander@gmail.com>
0004 
0005     This library is free software; you can redistribute it and/or
0006     modify it under the terms of the GNU Lesser General Public
0007     License as published by the Free Software Foundation; either
0008     version 2.1 of the License, or (at your option) any later version.
0009 
0010     This library is distributed in the hope that it will be useful,
0011     but WITHOUT ANY WARRANTY; without even the implied warranty of
0012     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
0013     Lesser General Public License for more details.
0014 
0015     You should have received a copy of the GNU Lesser General Public
0016     License along with this library; if not, write to the Free Software
0017     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
0018 */
0019 
0020 #include "contact-cache.h"
0021 #include "ktp_kded_debug.h"
0022 
0023 #include <KTp/core.h>
0024 #include <KTp/contact.h>
0025 
0026 #include <TelepathyQt/Account>
0027 #include <TelepathyQt/AccountManager>
0028 #include <TelepathyQt/AvatarData>
0029 #include <TelepathyQt/Connection>
0030 #include <TelepathyQt/ContactManager>
0031 #include <TelepathyQt/PendingOperation>
0032 #include <TelepathyQt/PendingReady>
0033 
0034 #include <QStandardPaths>
0035 #include <QDir>
0036 #include <QSqlQuery>
0037 #include <QSqlDriver>
0038 #include <QSqlField>
0039 
0040 /*
0041  * This class waits for a connection to load then saves the pernament
0042  * data from all contacts into a database that can be loaded by the kpeople plugin
0043  * It will not stay up-to-date, applications should load from the database, then
0044  * fetch volatile and up-to-date data from TpQt
0045  *
0046  * We don't hold a reference to the contact to keep things light
0047  */
0048 
0049 inline QString formatString(const QSqlQuery &query, const QString &str)
0050 {
0051     QSqlField f(QLatin1String(""), QVariant::String);
0052     f.setValue(str);
0053     return query.driver()->formatValue(f);
0054 }
0055 
0056 ContactCache::ContactCache(QObject *parent):
0057     QObject(parent),
0058     m_db(QSqlDatabase::addDatabase(QLatin1String("QSQLITE")))
0059 {
0060     QString path(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/ktp"));
0061     QDir dir(path);
0062     dir.mkpath(path);
0063 
0064     m_db.setDatabaseName(dir.absolutePath() + QStringLiteral("/cache.db"));
0065     if (!m_db.open()) {
0066         qWarning() << "couldn't open database" << m_db.databaseName();
0067     }
0068 
0069     // This is the query that creates the contacts table,
0070     // SQLite will store this within the sqlite_master table
0071     QString createTableQuery = QStringLiteral("CREATE TABLE contacts (accountId VARCHAR NOT NULL, contactId VARCHAR NOT NULL, alias VARCHAR, avatarFileName VARCHAR, isBlocked INT, groupsIds VARCHAR)");
0072 
0073     // Now let's verify that the table we currently have in the database
0074     // is the same one as above - get the stored query and compare them,
0075     // if they are different (for example when the table structure was
0076     // changed), the table will be dropped and recreated
0077     QSqlQuery verifyTableQuery(QStringLiteral("SELECT sql FROM sqlite_master WHERE tbl_name = 'contacts' AND type = 'table';"), m_db);
0078     verifyTableQuery.exec();
0079     verifyTableQuery.first();
0080     bool match = verifyTableQuery.value(QStringLiteral("sql")).toString() == createTableQuery;
0081     verifyTableQuery.finish();
0082 
0083     if (!m_db.tables().contains(QLatin1String("groups")) || !match) {
0084         QSqlQuery preparationsQuery(m_db);
0085         if (m_db.tables().contains(QLatin1String("contacts"))) {
0086             preparationsQuery.exec(QStringLiteral("DROP TABLE 'contacts';"));
0087             // Also drop the groups table
0088             preparationsQuery.exec(QStringLiteral("DROP TABLE 'groups';"));
0089         }
0090 
0091         preparationsQuery.exec(createTableQuery);
0092         preparationsQuery.exec(QLatin1String("CREATE TABLE groups (groupId INTEGER UNIQUE, groupName VARCHAR);"));
0093         preparationsQuery.exec(QLatin1String("CREATE UNIQUE INDEX idIndex ON contacts (accountId, contactId);"));
0094     }
0095 
0096     connect(KTp::accountManager()->becomeReady(), SIGNAL(finished(Tp::PendingOperation*)), SLOT(onAccountManagerReady(Tp::PendingOperation*)));
0097 }
0098 
0099 void ContactCache::onAccountManagerReady(Tp::PendingOperation *op)
0100 {
0101     if (!op || op->isError()) {
0102         qCWarning(KTP_KDED_MODULE) << "ContactCache: Failed to initialize AccountManager:" << op->errorName();
0103         qCWarning(KTP_KDED_MODULE) << op->errorMessage();
0104 
0105         return;
0106     }
0107 
0108     connect(KTp::accountManager().data(), SIGNAL(newAccount(Tp::AccountPtr)), SLOT(onNewAccount(Tp::AccountPtr)));
0109 
0110     QSqlQuery purgeQuery(m_db);
0111     QStringList formattedAccountsIds;
0112 
0113     Q_FOREACH (const Tp::AccountPtr &account, KTp::accountManager()->allAccounts()) {
0114         if (!accountIsInteresting(account)) {
0115             continue;
0116         }
0117 
0118         connectToAccount(account);
0119         if (!account->connection().isNull()) {
0120             onAccountConnectionChanged(account->connection());
0121         }
0122 
0123         formattedAccountsIds.append(formatString(purgeQuery, account->uniqueIdentifier()));
0124     }
0125 
0126     // Cleanup contacts
0127     if (formattedAccountsIds.isEmpty()) {
0128         purgeQuery.prepare(QLatin1String("DELETE * FROM contacts;"));
0129     } else {
0130         purgeQuery.prepare(QString(QLatin1String("DELETE FROM contacts WHERE accountId not in (%1);")).arg(formattedAccountsIds.join(QLatin1String(","))));
0131     }
0132     purgeQuery.exec();
0133 
0134     // Cleanup groups
0135     QStringList usedGroups;
0136 
0137     QSqlQuery usedGroupsQuery(m_db);
0138     usedGroupsQuery.prepare(QLatin1String("SELECT groupsIds FROM contacts;"));
0139     usedGroupsQuery.exec();
0140 
0141     while (usedGroupsQuery.next()) {
0142         usedGroups.append(usedGroupsQuery.value(0).toString().split(QLatin1String(",")));
0143     }
0144     usedGroups.removeDuplicates();
0145 
0146     purgeQuery.prepare(QString(QLatin1String("UPDATE groups SET groupName = '' WHERE groupId not in (%1);")).arg(usedGroups.join(QLatin1String(","))));
0147     purgeQuery.exec();
0148 
0149     // Load groups
0150     QSqlQuery groupsQuery(m_db);
0151     groupsQuery.exec(QLatin1String("SELECT groupName FROM groups ORDER BY groupId;"));
0152 
0153     while (groupsQuery.next()) {
0154         m_groups.append(groupsQuery.value(0).toString());
0155     }
0156 }
0157 
0158 void ContactCache::onNewAccount(const Tp::AccountPtr &account)
0159 {
0160     if (!accountIsInteresting(account)) {
0161         return;
0162     }
0163 
0164     connectToAccount(account);
0165     if (!account->connection().isNull()) {
0166         onAccountConnectionChanged(account->connection());
0167     }
0168 }
0169 
0170 void ContactCache::onAccountRemoved()
0171 {
0172     Tp::Account *account = qobject_cast<Tp::Account*>(sender());
0173 
0174     if (!account) {
0175         return;
0176     }
0177 
0178     QSqlQuery purgeQuery(m_db);
0179     purgeQuery.prepare(QLatin1String("DELETE FROM contacts WHERE accountId = ?;"));
0180     purgeQuery.bindValue(0, account->uniqueIdentifier());
0181     purgeQuery.exec();
0182 }
0183 
0184 void ContactCache::onContactManagerStateChanged()
0185 {
0186     Tp::ContactManagerPtr contactManager(qobject_cast<Tp::ContactManager*>(sender()));
0187     checkContactManagerState(Tp::ContactManagerPtr(contactManager));
0188 }
0189 
0190 void ContactCache::onAccountConnectionChanged(const Tp::ConnectionPtr &connection)
0191 {
0192     if (connection.isNull() || (connection->status() != Tp::ConnectionStatusConnected)) {
0193         return;
0194     }
0195 
0196     //this is needed to make the contact manager roster
0197     //when this finishes the contact manager will change state
0198     connection->becomeReady(Tp::Features() << Tp::Connection::FeatureRoster << Tp::Connection::FeatureRosterGroups);
0199 
0200     if (connect(connection->contactManager().data(), SIGNAL(stateChanged(Tp::ContactListState)), this, SLOT(onContactManagerStateChanged()), Qt::UniqueConnection)) {
0201         /* Check current contactManager state and do sync contact only if it is not performed due to already connected contactManager. */
0202         checkContactManagerState(connection->contactManager());
0203     }
0204 }
0205 
0206 void ContactCache::onAllKnownContactsChanged(const Tp::Contacts &added, const Tp::Contacts &removed)
0207 {
0208     /* Delete both added and removed contacts, because it's faster than accurate comparsion and partial update of exist contacts. */
0209     Tp::Contacts toBeRemoved = added;
0210     toBeRemoved.unite(removed);
0211 
0212     m_db.transaction();
0213     QSqlQuery removeQuery(m_db);
0214     removeQuery.prepare(QLatin1String("DELETE FROM contacts WHERE accountId = ? AND contactId = ?;"));
0215     Q_FOREACH (const Tp::ContactPtr &c, toBeRemoved) {
0216         const KTp::ContactPtr &contact = KTp::ContactPtr::qObjectCast(c);
0217         removeQuery.bindValue(0, contact->accountUniqueIdentifier());
0218         removeQuery.bindValue(1, contact->id());
0219         removeQuery.exec();
0220     }
0221 
0222     QSqlQuery insertQuery(m_db);
0223     insertQuery.prepare(QLatin1String("INSERT INTO contacts (accountId, contactId, alias, avatarFileName, isBlocked, groupsIds) VALUES (?, ?, ?, ?, ?, ?);"));
0224     Q_FOREACH (const Tp::ContactPtr &c, added) {
0225         if (c->manager()->connection()->protocolName() == QLatin1String("local-xmpp")) {
0226             continue;
0227         }
0228 
0229         bindContactToQuery(&insertQuery, c);
0230         insertQuery.exec();
0231     }
0232 
0233     m_db.commit();
0234 }
0235 
0236 void ContactCache::connectToAccount(const Tp::AccountPtr &account)
0237 {
0238     connect(account.data(), SIGNAL(removed()), SLOT(onAccountRemoved()));
0239     connect(account.data(), SIGNAL(connectionChanged(Tp::ConnectionPtr)), SLOT(onAccountConnectionChanged(Tp::ConnectionPtr)));
0240 }
0241 
0242 bool ContactCache::accountIsInteresting(const Tp::AccountPtr &account) const
0243 {
0244     if (account->protocolName() == QLatin1String("local-xmpp")) {// We don't want to cache local-xmpp contacts
0245         return false;
0246     }
0247 
0248     /* There may be more filters. */
0249 
0250     return true;
0251 }
0252 
0253 void ContactCache::syncContactsOfAccount(const Tp::AccountPtr &account)
0254 {
0255     m_db.transaction();
0256     QSqlQuery purgeQuery(m_db);
0257     purgeQuery.prepare(QLatin1String("DELETE FROM contacts WHERE accountId = ?;"));
0258     purgeQuery.bindValue(0, account->uniqueIdentifier());
0259     purgeQuery.exec();
0260 
0261     QSqlQuery insertQuery(m_db);
0262     insertQuery.prepare(QLatin1String("INSERT INTO contacts (accountId, contactId, alias, avatarFileName, isBlocked, groupsIds) VALUES (?, ?, ?, ?, ?, ?);"));
0263     Q_FOREACH (const Tp::ContactPtr &c, account->connection()->contactManager()->allKnownContacts()) {
0264         bindContactToQuery(&insertQuery, c);
0265         insertQuery.exec();
0266     }
0267 
0268     m_db.commit();
0269 
0270     connect(account->connection()->contactManager().data(),
0271             SIGNAL(allKnownContactsChanged(Tp::Contacts,Tp::Contacts,Tp::Channel::GroupMemberChangeDetails)),
0272             SLOT(onAllKnownContactsChanged(Tp::Contacts,Tp::Contacts)), Qt::UniqueConnection);
0273 }
0274 
0275 void ContactCache::checkContactManagerState(const Tp::ContactManagerPtr &contactManager)
0276 {
0277     if (contactManager->state() == Tp::ContactListStateSuccess) {
0278         const QString accountPath = TP_QT_ACCOUNT_OBJECT_PATH_BASE + QLatin1Char('/') + contactManager->connection()->property("accountUID").toString();
0279         Tp::AccountPtr account = KTp::accountManager()->accountForObjectPath(accountPath);
0280         if (!account.isNull()) {
0281             syncContactsOfAccount(account);
0282         } else {
0283             qCWarning(KTP_KDED_MODULE) << "Can't access to account by contactManager";
0284         }
0285     }
0286 }
0287 
0288 int ContactCache::askIdFromGroup(const QString &groupName)
0289 {
0290     int index = m_groups.indexOf(groupName);
0291     if (index >= 0) {
0292         return index;
0293     }
0294 
0295     QSqlQuery updateGroupsQuery(m_db);
0296 
0297     for (index = 0; index < m_groups.count(); ++index) {
0298         if (m_groups.at(index).isEmpty()) {
0299             m_groups[index] = groupName;
0300             updateGroupsQuery.prepare(QLatin1String("UPDATE groups SET groupName = :newGroupName WHERE groupId = :index;"));
0301             break;
0302         }
0303     }
0304 
0305     if (index >= m_groups.count()) {
0306         m_groups.append(groupName);
0307         updateGroupsQuery.prepare(QLatin1String("INSERT INTO groups (groupId, groupName) VALUES (:index, :newGroupName);"));
0308     }
0309 
0310     updateGroupsQuery.bindValue(QLatin1String(":newGroupName"), groupName);
0311     updateGroupsQuery.bindValue(QLatin1String(":index"), index);
0312     updateGroupsQuery.exec();
0313 
0314     return index;
0315 }
0316 
0317 void ContactCache::bindContactToQuery(QSqlQuery *query, const Tp::ContactPtr &contact)
0318 {
0319     const KTp::ContactPtr &ktpContact = KTp::ContactPtr::qObjectCast(contact);
0320     query->bindValue(0, ktpContact->accountUniqueIdentifier());
0321     query->bindValue(1, ktpContact->id());
0322     query->bindValue(2, ktpContact->alias());
0323     query->bindValue(3, ktpContact->avatarData().fileName);
0324     query->bindValue(4, ktpContact->isBlocked());
0325 
0326     QStringList groupsIds;
0327 
0328     Q_FOREACH (const QString &group, ktpContact->groups()) {
0329         groupsIds.append(QString::number(askIdFromGroup(group)));
0330     }
0331 
0332     query->bindValue(5, groupsIds.join(QLatin1String(",")));
0333 }