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 }