File indexing completed on 2024-04-28 15:28:53

0001 /*
0002     SPDX-FileCopyrightText: 2009-2010 Frederik Gladhorn <gladhorn@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-or-later
0005 */
0006 
0007 #include "atticaprovider_p.h"
0008 
0009 #include "commentsmodel.h"
0010 #include "question.h"
0011 #include "tagsfilterchecker.h"
0012 
0013 #include <KFormat>
0014 #include <KLocalizedString>
0015 #include <QCollator>
0016 #include <knewstuffcore_debug.h>
0017 
0018 #include <attica/accountbalance.h>
0019 #include <attica/config.h>
0020 #include <attica/content.h>
0021 #include <attica/downloaditem.h>
0022 #include <attica/listjob.h>
0023 #include <attica/person.h>
0024 #include <attica/provider.h>
0025 #include <attica/providermanager.h>
0026 
0027 using namespace Attica;
0028 
0029 namespace KNSCore
0030 {
0031 AtticaProvider::AtticaProvider(const QStringList &categories, const QString &additionalAgentInformation)
0032     : mEntryJob(nullptr)
0033     , mInitialized(false)
0034 {
0035     // init categories map with invalid categories
0036     for (const QString &category : categories) {
0037         mCategoryMap.insert(category, Attica::Category());
0038     }
0039 
0040     connect(&m_providerManager, &ProviderManager::providerAdded, this, [=](const Attica::Provider &provider) {
0041         providerLoaded(provider);
0042         m_provider.setAdditionalAgentInformation(additionalAgentInformation);
0043     });
0044     connect(&m_providerManager, &ProviderManager::authenticationCredentialsMissing, this, &AtticaProvider::onAuthenticationCredentialsMissing);
0045     connect(this, &Provider::loadComments, this, &AtticaProvider::loadComments);
0046     connect(this, &Provider::loadPerson, this, &AtticaProvider::loadPerson);
0047     connect(this, &Provider::loadBasics, this, &AtticaProvider::loadBasics);
0048 }
0049 
0050 AtticaProvider::AtticaProvider(const Attica::Provider &provider, const QStringList &categories, const QString &additionalAgentInformation)
0051     : mEntryJob(nullptr)
0052     , mInitialized(false)
0053 {
0054     // init categories map with invalid categories
0055     for (const QString &category : categories) {
0056         mCategoryMap.insert(category, Attica::Category());
0057     }
0058     providerLoaded(provider);
0059     m_provider.setAdditionalAgentInformation(additionalAgentInformation);
0060 }
0061 
0062 QString AtticaProvider::id() const
0063 {
0064     return m_providerId;
0065 }
0066 
0067 void AtticaProvider::onAuthenticationCredentialsMissing(const Attica::Provider &)
0068 {
0069     qCDebug(KNEWSTUFFCORE) << "Authentication missing!";
0070     // FIXME Show authentication dialog
0071 }
0072 
0073 bool AtticaProvider::setProviderXML(const QDomElement &xmldata)
0074 {
0075     if (xmldata.tagName() != QLatin1String("provider")) {
0076         return false;
0077     }
0078 
0079     // FIXME this is quite ugly, repackaging the xml into a string
0080     QDomDocument doc(QStringLiteral("temp"));
0081     qCDebug(KNEWSTUFFCORE) << "setting provider xml" << doc.toString();
0082 
0083     doc.appendChild(xmldata.cloneNode(true));
0084     m_providerManager.addProviderFromXml(doc.toString());
0085 
0086     if (!m_providerManager.providers().isEmpty()) {
0087         qCDebug(KNEWSTUFFCORE) << "base url of attica provider:" << m_providerManager.providers().constLast().baseUrl().toString();
0088     } else {
0089         qCCritical(KNEWSTUFFCORE) << "Could not load provider.";
0090         return false;
0091     }
0092     return true;
0093 }
0094 
0095 void AtticaProvider::setCachedEntries(const KNSCore::EntryInternal::List &cachedEntries)
0096 {
0097     mCachedEntries = cachedEntries;
0098 }
0099 
0100 void AtticaProvider::providerLoaded(const Attica::Provider &provider)
0101 {
0102     mName = provider.name();
0103     mIcon = provider.icon();
0104     qCDebug(KNEWSTUFFCORE) << "Added provider: " << provider.name();
0105 
0106     m_provider = provider;
0107     m_provider.setAdditionalAgentInformation(mName);
0108     m_providerId = provider.baseUrl().toString();
0109 
0110     Attica::ListJob<Attica::Category> *job = m_provider.requestCategories();
0111     connect(job, &BaseJob::finished, this, &AtticaProvider::listOfCategoriesLoaded);
0112     job->start();
0113 }
0114 
0115 void AtticaProvider::listOfCategoriesLoaded(Attica::BaseJob *listJob)
0116 {
0117     if (!jobSuccess(listJob)) {
0118         return;
0119     }
0120 
0121     qCDebug(KNEWSTUFFCORE) << "loading categories: " << mCategoryMap.keys();
0122 
0123     auto *job = static_cast<Attica::ListJob<Attica::Category> *>(listJob);
0124     const Category::List categoryList = job->itemList();
0125 
0126     QList<CategoryMetadata> categoryMetadataList;
0127     for (const Category &category : categoryList) {
0128         if (mCategoryMap.contains(category.name())) {
0129             qCDebug(KNEWSTUFFCORE) << "Adding category: " << category.name() << category.displayName();
0130             // If there is only the placeholder category, replace it
0131             if (mCategoryMap.contains(category.name()) && !mCategoryMap.value(category.name()).isValid()) {
0132                 mCategoryMap.replace(category.name(), category);
0133             } else {
0134                 mCategoryMap.insert(category.name(), category);
0135             }
0136 
0137             CategoryMetadata categoryMetadata;
0138             categoryMetadata.id = category.id();
0139             categoryMetadata.name = category.name();
0140             categoryMetadata.displayName = category.displayName();
0141             categoryMetadataList << categoryMetadata;
0142         }
0143     }
0144     std::sort(categoryMetadataList.begin(),
0145               categoryMetadataList.end(),
0146               [](const AtticaProvider::CategoryMetadata &i, const AtticaProvider::CategoryMetadata &j) -> bool {
0147                   const QString a(i.displayName.isEmpty() ? i.name : i.displayName);
0148                   const QString b(j.displayName.isEmpty() ? j.name : j.displayName);
0149 
0150                   return (QCollator().compare(a, b) < 0);
0151               });
0152 
0153     bool correct = false;
0154     for (auto it = mCategoryMap.cbegin(), itEnd = mCategoryMap.cend(); it != itEnd; ++it) {
0155         if (!it.value().isValid()) {
0156             qCWarning(KNEWSTUFFCORE) << "Could not find category" << it.key();
0157         } else {
0158             correct = true;
0159         }
0160     }
0161 
0162     if (correct) {
0163         mInitialized = true;
0164         Q_EMIT providerInitialized(this);
0165         Q_EMIT categoriesMetadataLoded(categoryMetadataList);
0166     } else {
0167         Q_EMIT signalErrorCode(KNSCore::ConfigFileError, i18n("All categories are missing"), QVariant());
0168     }
0169 }
0170 
0171 bool AtticaProvider::isInitialized() const
0172 {
0173     return mInitialized;
0174 }
0175 
0176 void AtticaProvider::loadEntries(const KNSCore::Provider::SearchRequest &request)
0177 {
0178     if (mEntryJob) {
0179         mEntryJob->abort();
0180         mEntryJob = nullptr;
0181     }
0182 
0183     mCurrentRequest = request;
0184     switch (request.filter) {
0185     case None:
0186         break;
0187     case ExactEntryId: {
0188         ItemJob<Content> *job = m_provider.requestContent(request.searchTerm);
0189         connect(job, &BaseJob::finished, this, &AtticaProvider::detailsLoaded);
0190         job->start();
0191         return;
0192     }
0193     case Installed:
0194         if (request.page == 0) {
0195             Q_EMIT loadingFinished(request, installedEntries());
0196         } else {
0197             Q_EMIT loadingFinished(request, EntryInternal::List());
0198         }
0199         return;
0200     case Updates:
0201         checkForUpdates();
0202         return;
0203     }
0204 
0205     Attica::Provider::SortMode sorting = atticaSortMode(request.sortMode);
0206     Attica::Category::List categoriesToSearch;
0207 
0208     if (request.categories.isEmpty()) {
0209         // search in all categories
0210         categoriesToSearch = mCategoryMap.values();
0211     } else {
0212         categoriesToSearch.reserve(request.categories.size());
0213         for (const QString &categoryName : std::as_const(request.categories)) {
0214             categoriesToSearch.append(mCategoryMap.values(categoryName));
0215         }
0216     }
0217 
0218     ListJob<Content> *job = m_provider.searchContents(categoriesToSearch, request.searchTerm, sorting, request.page, request.pageSize);
0219     connect(job, &BaseJob::finished, this, &AtticaProvider::categoryContentsLoaded);
0220 
0221     mEntryJob = job;
0222     job->start();
0223 }
0224 
0225 void AtticaProvider::checkForUpdates()
0226 {
0227     if (mCachedEntries.isEmpty()) {
0228         Q_EMIT loadingFinished(mCurrentRequest, {});
0229     }
0230 
0231     for (const EntryInternal &e : std::as_const(mCachedEntries)) {
0232         ItemJob<Content> *job = m_provider.requestContent(e.uniqueId());
0233         connect(job, &BaseJob::finished, this, &AtticaProvider::detailsLoaded);
0234         m_updateJobs.insert(job);
0235         job->start();
0236         qCDebug(KNEWSTUFFCORE) << "Checking for update: " << e.name();
0237     }
0238 }
0239 
0240 void AtticaProvider::loadEntryDetails(const KNSCore::EntryInternal &entry)
0241 {
0242     ItemJob<Content> *job = m_provider.requestContent(entry.uniqueId());
0243     connect(job, &BaseJob::finished, this, &AtticaProvider::detailsLoaded);
0244     job->start();
0245 }
0246 
0247 void AtticaProvider::detailsLoaded(BaseJob *job)
0248 {
0249     if (jobSuccess(job)) {
0250         auto *contentJob = static_cast<ItemJob<Content> *>(job);
0251         Content content = contentJob->result();
0252         EntryInternal entry = entryFromAtticaContent(content);
0253         Q_EMIT entryDetailsLoaded(entry);
0254         qCDebug(KNEWSTUFFCORE) << "check update finished: " << entry.name();
0255     }
0256 
0257     if (m_updateJobs.remove(job) && m_updateJobs.isEmpty()) {
0258         qCDebug(KNEWSTUFFCORE) << "check update finished.";
0259         QList<EntryInternal> updatable;
0260         for (const EntryInternal &entry : std::as_const(mCachedEntries)) {
0261             if (entry.status() == KNS3::Entry::Updateable) {
0262                 updatable.append(entry);
0263             }
0264         }
0265         Q_EMIT loadingFinished(mCurrentRequest, updatable);
0266     }
0267 }
0268 
0269 void AtticaProvider::categoryContentsLoaded(BaseJob *job)
0270 {
0271     if (!jobSuccess(job)) {
0272         return;
0273     }
0274 
0275     auto *listJob = static_cast<ListJob<Content> *>(job);
0276     const Content::List contents = listJob->itemList();
0277 
0278     EntryInternal::List entries;
0279     TagsFilterChecker checker(tagFilter());
0280     TagsFilterChecker downloadschecker(downloadTagFilter());
0281     for (const Content &content : contents) {
0282         if (!content.isValid()) {
0283             qCDebug(KNEWSTUFFCORE)
0284                 << "Filtered out an invalid entry. This suggests something is not right on the originating server. Please contact the administrators of"
0285                 << name() << "and inform them there is an issue with content in the category or categories" << mCurrentRequest.categories;
0286             continue;
0287         }
0288         if (checker.filterAccepts(content.tags())) {
0289             bool filterAcceptsDownloads = true;
0290             if (content.downloads() > 0) {
0291                 filterAcceptsDownloads = false;
0292                 const QList<Attica::DownloadDescription> descs = content.downloadUrlDescriptions();
0293                 for (const Attica::DownloadDescription &dli : descs) {
0294                     if (downloadschecker.filterAccepts(dli.tags())) {
0295                         filterAcceptsDownloads = true;
0296                         break;
0297                     }
0298                 }
0299             }
0300             if (filterAcceptsDownloads) {
0301                 mCachedContent.insert(content.id(), content);
0302                 entries.append(entryFromAtticaContent(content));
0303             } else {
0304                 qCDebug(KNEWSTUFFCORE) << "Filter has excluded" << content.name() << "on download filter" << downloadTagFilter();
0305             }
0306         } else {
0307             qCDebug(KNEWSTUFFCORE) << "Filter has excluded" << content.name() << "on entry filter" << tagFilter();
0308         }
0309     }
0310 
0311     qCDebug(KNEWSTUFFCORE) << "loaded: " << mCurrentRequest.hashForRequest() << " count: " << entries.size();
0312     Q_EMIT loadingFinished(mCurrentRequest, entries);
0313     mEntryJob = nullptr;
0314 }
0315 
0316 Attica::Provider::SortMode AtticaProvider::atticaSortMode(const SortMode &sortMode)
0317 {
0318     switch (sortMode) {
0319     case Newest:
0320         return Attica::Provider::Newest;
0321     case Alphabetical:
0322         return Attica::Provider::Alphabetical;
0323     case Downloads:
0324         return Attica::Provider::Downloads;
0325     default:
0326         return Attica::Provider::Rating;
0327     }
0328 }
0329 
0330 void AtticaProvider::loadPayloadLink(const KNSCore::EntryInternal &entry, int linkId)
0331 {
0332     Attica::Content content = mCachedContent.value(entry.uniqueId());
0333     const DownloadDescription desc = content.downloadUrlDescription(linkId);
0334 
0335     if (desc.hasPrice()) {
0336         // Ask for balance, then show information...
0337         ItemJob<AccountBalance> *job = m_provider.requestAccountBalance();
0338         connect(job, &BaseJob::finished, this, &AtticaProvider::accountBalanceLoaded);
0339         mDownloadLinkJobs[job] = qMakePair(entry, linkId);
0340         job->start();
0341 
0342         qCDebug(KNEWSTUFFCORE) << "get account balance";
0343     } else {
0344         ItemJob<DownloadItem> *job = m_provider.downloadLink(entry.uniqueId(), QString::number(linkId));
0345         connect(job, &BaseJob::finished, this, &AtticaProvider::downloadItemLoaded);
0346         mDownloadLinkJobs[job] = qMakePair(entry, linkId);
0347         job->start();
0348 
0349         qCDebug(KNEWSTUFFCORE) << " link for " << entry.uniqueId();
0350     }
0351 }
0352 
0353 void AtticaProvider::loadComments(const EntryInternal &entry, int commentsPerPage, int page)
0354 {
0355     ListJob<Attica::Comment> *job = m_provider.requestComments(Attica::Comment::ContentComment, entry.uniqueId(), QStringLiteral("0"), page, commentsPerPage);
0356     connect(job, &BaseJob::finished, this, &AtticaProvider::loadedComments);
0357     job->start();
0358 }
0359 
0360 /// TODO KF6 QList is discouraged, and we'll probably want to switch this (and the rest of the KNS library) to QVector instead
0361 QList<std::shared_ptr<KNSCore::Comment>> getCommentsList(const Attica::Comment::List &comments, std::shared_ptr<KNSCore::Comment> parent)
0362 {
0363     QList<std::shared_ptr<KNSCore::Comment>> knsComments;
0364     for (const Attica::Comment &comment : comments) {
0365         qCDebug(KNEWSTUFFCORE) << "Appending comment with id" << comment.id() << ", which has" << comment.childCount() << "children";
0366         auto knsComment = std::make_shared<KNSCore::Comment>();
0367         knsComment->id = comment.id();
0368         knsComment->subject = comment.subject();
0369         knsComment->text = comment.text();
0370         knsComment->childCount = comment.childCount();
0371         knsComment->username = comment.user();
0372         knsComment->date = comment.date();
0373         knsComment->score = comment.score();
0374         knsComment->parent = parent;
0375         knsComments << knsComment;
0376         if (comment.childCount() > 0) {
0377             qCDebug(KNEWSTUFFCORE) << "Getting more comments, as this one has children, and we currently have this number of comments:" << knsComments.count();
0378             knsComments << getCommentsList(comment.children(), knsComment);
0379             qCDebug(KNEWSTUFFCORE) << "After getting the children, we now have the following number of comments:" << knsComments.count();
0380         }
0381     }
0382     return knsComments;
0383 }
0384 
0385 void AtticaProvider::loadedComments(Attica::BaseJob *baseJob)
0386 {
0387     if (!jobSuccess(baseJob)) {
0388         return;
0389     }
0390 
0391     auto *job = static_cast<ListJob<Attica::Comment> *>(baseJob);
0392     Attica::Comment::List comments = job->itemList();
0393 
0394     QList<std::shared_ptr<KNSCore::Comment>> receivedComments = getCommentsList(comments, nullptr);
0395     Q_EMIT commentsLoaded(receivedComments);
0396 }
0397 
0398 void AtticaProvider::loadPerson(const QString &username)
0399 {
0400     if (m_provider.hasPersonService()) {
0401         ItemJob<Attica::Person> *job = m_provider.requestPerson(username);
0402         job->setProperty("username", username);
0403         connect(job, &BaseJob::finished, this, &AtticaProvider::loadedPerson);
0404         job->start();
0405     }
0406 }
0407 
0408 void AtticaProvider::loadedPerson(Attica::BaseJob *baseJob)
0409 {
0410     if (!jobSuccess(baseJob)) {
0411         return;
0412     }
0413 
0414     auto *job = static_cast<ItemJob<Attica::Person> *>(baseJob);
0415     Attica::Person person = job->result();
0416 
0417     auto author = std::make_shared<KNSCore::Author>();
0418     // This is a touch hack-like, but it ensures we actually have the data in case it is not returned by the server
0419     author->setId(job->property("username").toString());
0420     author->setName(QStringLiteral("%1 %2").arg(person.firstName(), person.lastName()).trimmed());
0421     author->setHomepage(person.homepage());
0422     author->setProfilepage(person.extendedAttribute(QStringLiteral("profilepage")));
0423     author->setAvatarUrl(person.avatarUrl());
0424     author->setDescription(person.extendedAttribute(QStringLiteral("description")));
0425     Q_EMIT personLoaded(author);
0426 }
0427 
0428 void AtticaProvider::loadBasics()
0429 {
0430     Attica::ItemJob<Attica::Config> *configJob = m_provider.requestConfig();
0431     connect(configJob, &BaseJob::finished, this, &AtticaProvider::loadedConfig);
0432     configJob->start();
0433 }
0434 
0435 void AtticaProvider::loadedConfig(Attica::BaseJob *baseJob)
0436 {
0437     if (jobSuccess(baseJob)) {
0438         auto *job = static_cast<ItemJob<Attica::Config> *>(baseJob);
0439         Attica::Config config = job->result();
0440         setVersion(config.version());
0441         setSupportsSsl(config.ssl());
0442         setContactEmail(config.contact());
0443         QString protocol{QStringLiteral("http")};
0444         if (config.ssl()) {
0445             protocol = QStringLiteral("https");
0446         }
0447         // There is usually no protocol in the website and host, but in case
0448         // there is, trust what's there
0449         if (config.website().contains(QLatin1String("://"))) {
0450             setWebsite(QUrl(config.website()));
0451         } else {
0452             setWebsite(QUrl(QLatin1String("%1://%2").arg(protocol).arg(config.website())));
0453         }
0454         if (config.host().contains(QLatin1String("://"))) {
0455             setHost(QUrl(config.host()));
0456         } else {
0457             setHost(QUrl(QLatin1String("%1://%2").arg(protocol).arg(config.host())));
0458         }
0459     }
0460 }
0461 
0462 void AtticaProvider::accountBalanceLoaded(Attica::BaseJob *baseJob)
0463 {
0464     if (!jobSuccess(baseJob)) {
0465         return;
0466     }
0467 
0468     auto *job = static_cast<ItemJob<AccountBalance> *>(baseJob);
0469     AccountBalance item = job->result();
0470 
0471     QPair<EntryInternal, int> pair = mDownloadLinkJobs.take(job);
0472     EntryInternal entry(pair.first);
0473     Content content = mCachedContent.value(entry.uniqueId());
0474     if (content.downloadUrlDescription(pair.second).priceAmount() < item.balance()) {
0475         qCDebug(KNEWSTUFFCORE) << "Your balance is greater than the price." << content.downloadUrlDescription(pair.second).priceAmount()
0476                                << " balance: " << item.balance();
0477         Question question;
0478         question.setEntry(entry);
0479         question.setQuestion(i18nc("the price of a download item, parameter 1 is the currency, 2 is the price",
0480                                    "This item costs %1 %2.\nDo you want to buy it?",
0481                                    item.currency(),
0482                                    content.downloadUrlDescription(pair.second).priceAmount()));
0483         if (question.ask() == Question::YesResponse) {
0484             ItemJob<DownloadItem> *job = m_provider.downloadLink(entry.uniqueId(), QString::number(pair.second));
0485             connect(job, &BaseJob::finished, this, &AtticaProvider::downloadItemLoaded);
0486             mDownloadLinkJobs[job] = qMakePair(entry, pair.second);
0487             job->start();
0488         } else {
0489             return;
0490         }
0491     } else {
0492         qCDebug(KNEWSTUFFCORE) << "You don't have enough money on your account!" << content.downloadUrlDescription(0).priceAmount()
0493                                << " balance: " << item.balance();
0494         Q_EMIT signalInformation(i18n("Your account balance is too low:\nYour balance: %1\nPrice: %2", //
0495                                       item.balance(),
0496                                       content.downloadUrlDescription(0).priceAmount()));
0497     }
0498 }
0499 
0500 void AtticaProvider::downloadItemLoaded(BaseJob *baseJob)
0501 {
0502     if (!jobSuccess(baseJob)) {
0503         return;
0504     }
0505 
0506     auto *job = static_cast<ItemJob<DownloadItem> *>(baseJob);
0507     DownloadItem item = job->result();
0508 
0509     EntryInternal entry = mDownloadLinkJobs.take(job).first;
0510     entry.setPayload(QString(item.url().toString()));
0511     Q_EMIT payloadLinkLoaded(entry);
0512 }
0513 
0514 EntryInternal::List AtticaProvider::installedEntries() const
0515 {
0516     EntryInternal::List entries;
0517     for (const EntryInternal &entry : std::as_const(mCachedEntries)) {
0518         if (entry.status() == KNS3::Entry::Installed || entry.status() == KNS3::Entry::Updateable) {
0519             entries.append(entry);
0520         }
0521     }
0522     return entries;
0523 }
0524 
0525 void AtticaProvider::vote(const EntryInternal &entry, uint rating)
0526 {
0527     PostJob *job = m_provider.voteForContent(entry.uniqueId(), rating);
0528     connect(job, &BaseJob::finished, this, &AtticaProvider::votingFinished);
0529     job->start();
0530 }
0531 
0532 void AtticaProvider::votingFinished(Attica::BaseJob *job)
0533 {
0534     if (!jobSuccess(job)) {
0535         return;
0536     }
0537     Q_EMIT signalInformation(i18nc("voting for an item (good/bad)", "Your vote was recorded."));
0538 }
0539 
0540 void AtticaProvider::becomeFan(const EntryInternal &entry)
0541 {
0542     PostJob *job = m_provider.becomeFan(entry.uniqueId());
0543     connect(job, &BaseJob::finished, this, &AtticaProvider::becomeFanFinished);
0544     job->start();
0545 }
0546 
0547 void AtticaProvider::becomeFanFinished(Attica::BaseJob *job)
0548 {
0549     if (!jobSuccess(job)) {
0550         return;
0551     }
0552     Q_EMIT signalInformation(i18n("You are now a fan."));
0553 }
0554 
0555 bool AtticaProvider::jobSuccess(Attica::BaseJob *job) const
0556 {
0557     if (job->metadata().error() == Attica::Metadata::NoError) {
0558         return true;
0559     }
0560     qCDebug(KNEWSTUFFCORE) << "job error: " << job->metadata().error() << " status code: " << job->metadata().statusCode() << job->metadata().message();
0561 
0562     if (job->metadata().error() == Attica::Metadata::NetworkError) {
0563         if (job->metadata().statusCode() == 503) {
0564             QDateTime retryAfter;
0565             static const QByteArray retryAfterKey{"Retry-After"};
0566             for (const QNetworkReply::RawHeaderPair &headerPair : job->metadata().headers()) {
0567                 if (headerPair.first == retryAfterKey) {
0568                     // Retry-After is not a known header, so we need to do a bit of running around to make that work
0569                     // Also, the fromHttpDate function is in the private qnetworkrequest header, so we can't use that
0570                     // So, simple workaround, just pass it through a dummy request and get a formatted date out (the
0571                     // cost is sufficiently low here, given we've just done a bunch of i/o heavy things, so...)
0572                     QNetworkRequest dummyRequest;
0573                     dummyRequest.setRawHeader(QByteArray{"Last-Modified"}, headerPair.second);
0574                     retryAfter = dummyRequest.header(QNetworkRequest::LastModifiedHeader).toDateTime();
0575                     break;
0576                 }
0577             }
0578             static const KFormat formatter;
0579             Q_EMIT signalErrorCode(KNSCore::TryAgainLaterError,
0580                                    i18n("The service is currently undergoing maintenance and is expected to be back in %1.",
0581                                         formatter.formatSpelloutDuration(retryAfter.toMSecsSinceEpoch() - QDateTime::currentMSecsSinceEpoch())),
0582                                    {retryAfter});
0583         } else {
0584             Q_EMIT signalErrorCode(KNSCore::NetworkError,
0585                                    i18n("Network error %1: %2", job->metadata().statusCode(), job->metadata().statusString()),
0586                                    job->metadata().statusCode());
0587         }
0588     }
0589     if (job->metadata().error() == Attica::Metadata::OcsError) {
0590         if (job->metadata().statusCode() == 200) {
0591             Q_EMIT signalErrorCode(KNSCore::OcsError, i18n("Too many requests to server. Please try again in a few minutes."), job->metadata().statusCode());
0592         } else if (job->metadata().statusCode() == 405) {
0593             Q_EMIT signalErrorCode(KNSCore::OcsError,
0594                                    i18n("The Open Collaboration Services instance %1 does not support the attempted function.", name()),
0595                                    job->metadata().statusCode());
0596         } else {
0597             Q_EMIT signalErrorCode(KNSCore::OcsError,
0598                                    i18n("Unknown Open Collaboration Service API error. (%1)", job->metadata().statusCode()),
0599                                    job->metadata().statusCode());
0600         }
0601     }
0602     return false;
0603 }
0604 
0605 EntryInternal AtticaProvider::entryFromAtticaContent(const Attica::Content &content)
0606 {
0607     EntryInternal entry;
0608 
0609     entry.setProviderId(id());
0610     entry.setUniqueId(content.id());
0611     entry.setStatus(KNS3::Entry::Downloadable);
0612     entry.setVersion(content.version());
0613     entry.setReleaseDate(content.updated().date());
0614     entry.setCategory(content.attribute(QStringLiteral("typeid")));
0615 
0616     int index = mCachedEntries.indexOf(entry);
0617     if (index >= 0) {
0618         EntryInternal &cacheEntry = mCachedEntries[index];
0619         // check if updateable
0620         if (((cacheEntry.status() == KNS3::Entry::Installed) || (cacheEntry.status() == KNS3::Entry::Updateable))
0621             && ((cacheEntry.version() != entry.version()) || (cacheEntry.releaseDate() != entry.releaseDate()))) {
0622             cacheEntry.setStatus(KNS3::Entry::Updateable);
0623             cacheEntry.setUpdateVersion(entry.version());
0624             cacheEntry.setUpdateReleaseDate(entry.releaseDate());
0625         }
0626         entry = cacheEntry;
0627     } else {
0628         mCachedEntries.append(entry);
0629     }
0630 
0631     entry.setName(content.name());
0632     entry.setHomepage(content.detailpage());
0633     entry.setRating(content.rating());
0634     entry.setNumberOfComments(content.numberOfComments());
0635     entry.setDownloadCount(content.downloads());
0636     entry.setNumberFans(content.attribute(QStringLiteral("fans")).toInt());
0637     entry.setDonationLink(content.attribute(QStringLiteral("donationpage")));
0638     entry.setKnowledgebaseLink(content.attribute(QStringLiteral("knowledgebasepage")));
0639     entry.setNumberKnowledgebaseEntries(content.attribute(QStringLiteral("knowledgebaseentries")).toInt());
0640     entry.setHomepage(content.detailpage());
0641 
0642     entry.setPreviewUrl(content.smallPreviewPicture(QStringLiteral("1")), EntryInternal::PreviewSmall1);
0643     entry.setPreviewUrl(content.smallPreviewPicture(QStringLiteral("2")), EntryInternal::PreviewSmall2);
0644     entry.setPreviewUrl(content.smallPreviewPicture(QStringLiteral("3")), EntryInternal::PreviewSmall3);
0645 
0646     entry.setPreviewUrl(content.previewPicture(QStringLiteral("1")), EntryInternal::PreviewBig1);
0647     entry.setPreviewUrl(content.previewPicture(QStringLiteral("2")), EntryInternal::PreviewBig2);
0648     entry.setPreviewUrl(content.previewPicture(QStringLiteral("3")), EntryInternal::PreviewBig3);
0649 
0650     entry.setLicense(content.license());
0651     Author author;
0652     author.setId(content.author());
0653     author.setName(content.author());
0654     author.setHomepage(content.attribute(QStringLiteral("profilepage")));
0655     entry.setAuthor(author);
0656 
0657     entry.setSource(EntryInternal::Online);
0658     entry.setSummary(content.description());
0659     entry.setShortSummary(content.summary());
0660     entry.setChangelog(content.changelog());
0661     entry.setTags(content.tags());
0662 
0663     entry.clearDownloadLinkInformation();
0664     const QList<Attica::DownloadDescription> descs = content.downloadUrlDescriptions();
0665     for (const Attica::DownloadDescription &desc : descs) {
0666         EntryInternal::DownloadLinkInformation info;
0667         info.name = desc.name();
0668         info.priceAmount = desc.priceAmount();
0669         info.distributionType = desc.distributionType();
0670         info.descriptionLink = desc.link();
0671         info.id = desc.id();
0672         info.size = desc.size();
0673         info.isDownloadtypeLink = desc.type() == Attica::DownloadDescription::LinkDownload;
0674         info.tags = desc.tags();
0675         entry.appendDownloadLinkInformation(info);
0676     }
0677 
0678     return entry;
0679 }
0680 
0681 } // namespace
0682 
0683 #include "moc_atticaprovider_p.cpp"