1/*
2 SPDX-FileCopyrightText: 2009-2010 Frederik Gladhorn <gladhorn@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.1-or-later
5*/
6
7#include "atticaprovider_p.h"
8
9#include "commentsmodel.h"
10#include "question.h"
11#include "tagsfilterchecker.h"
12
13#include <KFormat>
14#include <KLocalizedString>
15#include <QCollator>
16#include <QDomDocument>
17#include <knewstuffcore_debug.h>
18
19#include <attica/accountbalance.h>
20#include <attica/config.h>
21#include <attica/content.h>
22#include <attica/downloaditem.h>
23#include <attica/listjob.h>
24#include <attica/person.h>
25#include <attica/provider.h>
26#include <attica/providermanager.h>
27
28using namespace Attica;
29
30namespace KNSCore
31{
32AtticaProvider::AtticaProvider(const QStringList &categories, const QString &additionalAgentInformation)
33 : mEntryJob(nullptr)
34 , mInitialized(false)
35{
36 // init categories map with invalid categories
37 for (const QString &category : categories) {
38 mCategoryMap.insert(key: category, value: Attica::Category());
39 }
40
41 connect(sender: &m_providerManager, signal: &ProviderManager::providerAdded, context: this, slot: [this, additionalAgentInformation](const Attica::Provider &provider) {
42 providerLoaded(provider);
43 m_provider.setAdditionalAgentInformation(additionalAgentInformation);
44 });
45 connect(sender: &m_providerManager, signal: &ProviderManager::authenticationCredentialsMissing, context: this, slot: &AtticaProvider::onAuthenticationCredentialsMissing);
46}
47
48AtticaProvider::AtticaProvider(const Attica::Provider &provider, const QStringList &categories, const QString &additionalAgentInformation)
49 : mEntryJob(nullptr)
50 , mInitialized(false)
51{
52 // init categories map with invalid categories
53 for (const QString &category : categories) {
54 mCategoryMap.insert(key: category, value: Attica::Category());
55 }
56 providerLoaded(provider);
57 m_provider.setAdditionalAgentInformation(additionalAgentInformation);
58}
59
60QString AtticaProvider::id() const
61{
62 return m_providerId;
63}
64
65void AtticaProvider::onAuthenticationCredentialsMissing(const Attica::Provider &)
66{
67 qCDebug(KNEWSTUFFCORE) << "Authentication missing!";
68 // FIXME Show authentication dialog
69}
70
71bool AtticaProvider::setProviderXML(const QDomElement &xmldata)
72{
73 if (xmldata.tagName() != QLatin1String("provider")) {
74 return false;
75 }
76
77 // FIXME this is quite ugly, repackaging the xml into a string
78 QDomDocument doc(QStringLiteral("temp"));
79 qCDebug(KNEWSTUFFCORE) << "setting provider xml" << doc.toString();
80
81 doc.appendChild(newChild: xmldata.cloneNode(deep: true));
82 m_providerManager.addProviderFromXml(providerXml: doc.toString());
83
84 if (!m_providerManager.providers().isEmpty()) {
85 qCDebug(KNEWSTUFFCORE) << "base url of attica provider:" << m_providerManager.providers().constLast().baseUrl().toString();
86 } else {
87 qCCritical(KNEWSTUFFCORE) << "Could not load provider.";
88 return false;
89 }
90 return true;
91}
92
93void AtticaProvider::setCachedEntries(const KNSCore::Entry::List &cachedEntries)
94{
95 mCachedEntries = cachedEntries;
96}
97
98void AtticaProvider::providerLoaded(const Attica::Provider &provider)
99{
100 setName(provider.name());
101 setIcon(provider.icon());
102 qCDebug(KNEWSTUFFCORE) << "Added provider: " << provider.name();
103
104 m_provider = provider;
105 m_provider.setAdditionalAgentInformation(name());
106 m_providerId = provider.baseUrl().host();
107
108 Attica::ListJob<Attica::Category> *job = m_provider.requestCategories();
109 connect(sender: job, signal: &BaseJob::finished, context: this, slot: &AtticaProvider::listOfCategoriesLoaded);
110 job->start();
111}
112
113void AtticaProvider::listOfCategoriesLoaded(Attica::BaseJob *listJob)
114{
115 if (!jobSuccess(job: listJob)) {
116 return;
117 }
118
119 qCDebug(KNEWSTUFFCORE) << "loading categories: " << mCategoryMap.keys();
120
121 auto *job = static_cast<Attica::ListJob<Attica::Category> *>(listJob);
122 const Category::List categoryList = job->itemList();
123
124 QList<CategoryMetadata> categoryMetadataList;
125 for (const Category &category : categoryList) {
126 if (mCategoryMap.contains(key: category.name())) {
127 qCDebug(KNEWSTUFFCORE) << "Adding category: " << category.name() << category.displayName();
128 // If there is only the placeholder category, replace it
129 if (mCategoryMap.contains(key: category.name()) && !mCategoryMap.value(key: category.name()).isValid()) {
130 mCategoryMap.replace(key: category.name(), value: category);
131 } else {
132 mCategoryMap.insert(key: category.name(), value: category);
133 }
134
135 CategoryMetadata categoryMetadata;
136 categoryMetadata.id = category.id();
137 categoryMetadata.name = category.name();
138 categoryMetadata.displayName = category.displayName();
139 categoryMetadataList << categoryMetadata;
140 }
141 }
142 std::sort(first: categoryMetadataList.begin(),
143 last: categoryMetadataList.end(),
144 comp: [](const AtticaProvider::CategoryMetadata &i, const AtticaProvider::CategoryMetadata &j) -> bool {
145 const QString a(i.displayName.isEmpty() ? i.name : i.displayName);
146 const QString b(j.displayName.isEmpty() ? j.name : j.displayName);
147
148 return (QCollator().compare(s1: a, s2: b) < 0);
149 });
150
151 bool correct = false;
152 for (auto it = mCategoryMap.cbegin(), itEnd = mCategoryMap.cend(); it != itEnd; ++it) {
153 if (!it.value().isValid()) {
154 qCWarning(KNEWSTUFFCORE) << "Could not find category" << it.key();
155 } else {
156 correct = true;
157 }
158 }
159
160 if (correct) {
161 mInitialized = true;
162 Q_EMIT providerInitialized(this);
163 Q_EMIT categoriesMetadataLoded(categories: categoryMetadataList);
164 } else {
165 Q_EMIT signalErrorCode(errorCode: KNSCore::ErrorCode::ConfigFileError, i18n("All categories are missing"), metadata: QVariant());
166 }
167}
168
169bool AtticaProvider::isInitialized() const
170{
171 return mInitialized;
172}
173
174void AtticaProvider::loadEntries(const KNSCore::Provider::SearchRequest &request)
175{
176 if (mEntryJob) {
177 mEntryJob->abort();
178 mEntryJob = nullptr;
179 }
180
181 mCurrentRequest = request;
182 switch (request.filter) {
183 case None:
184 break;
185 case ExactEntryId: {
186 ItemJob<Content> *job = m_provider.requestContent(contentId: request.searchTerm);
187 job->setProperty(name: "providedEntryId", value: request.searchTerm);
188 connect(sender: job, signal: &BaseJob::finished, context: this, slot: &AtticaProvider::detailsLoaded);
189 job->start();
190 return;
191 }
192 case Installed:
193 if (request.page == 0) {
194 Q_EMIT loadingFinished(request, installedEntries());
195 } else {
196 Q_EMIT loadingFinished(request, Entry::List());
197 }
198 return;
199 case Updates:
200 checkForUpdates();
201 return;
202 }
203
204 Attica::Provider::SortMode sorting = atticaSortMode(sortMode: request.sortMode);
205 Attica::Category::List categoriesToSearch;
206
207 if (request.categories.isEmpty()) {
208 // search in all categories
209 categoriesToSearch = mCategoryMap.values();
210 } else {
211 categoriesToSearch.reserve(asize: request.categories.size());
212 for (const QString &categoryName : std::as_const(t: request.categories)) {
213 categoriesToSearch.append(other: mCategoryMap.values(key: categoryName));
214 }
215 }
216
217 ListJob<Content> *job = m_provider.searchContents(categories: categoriesToSearch, search: request.searchTerm, mode: sorting, page: request.page, pageSize: request.pageSize);
218 job->setProperty(name: "searchRequest", value: QVariant::fromValue(value: request));
219 connect(sender: job, signal: &BaseJob::finished, context: this, slot: &AtticaProvider::categoryContentsLoaded);
220
221 mEntryJob = job;
222 job->start();
223}
224
225void AtticaProvider::checkForUpdates()
226{
227 if (mCachedEntries.isEmpty()) {
228 Q_EMIT loadingFinished(mCurrentRequest, {});
229 }
230
231 for (const Entry &e : std::as_const(t&: mCachedEntries)) {
232 ItemJob<Content> *job = m_provider.requestContent(contentId: e.uniqueId());
233 connect(sender: job, signal: &BaseJob::finished, context: this, slot: &AtticaProvider::detailsLoaded);
234 m_updateJobs.insert(value: job);
235 job->start();
236 qCDebug(KNEWSTUFFCORE) << "Checking for update: " << e.name();
237 }
238}
239
240void AtticaProvider::loadEntryDetails(const KNSCore::Entry &entry)
241{
242 ItemJob<Content> *job = m_provider.requestContent(contentId: entry.uniqueId());
243 connect(sender: job, signal: &BaseJob::finished, context: this, slot: &AtticaProvider::detailsLoaded);
244 job->start();
245}
246
247void AtticaProvider::detailsLoaded(BaseJob *job)
248{
249 if (jobSuccess(job)) {
250 auto *contentJob = static_cast<ItemJob<Content> *>(job);
251 Content content = contentJob->result();
252 Entry entry = entryFromAtticaContent(content);
253 entry.setEntryRequestedId(job->property(name: "providedEntryId").toString()); // The ResultsStream should still known that this entry was for its query
254 Q_EMIT entryDetailsLoaded(entry);
255 qCDebug(KNEWSTUFFCORE) << "check update finished: " << entry.name();
256 }
257
258 if (m_updateJobs.remove(value: job) && m_updateJobs.isEmpty()) {
259 qCDebug(KNEWSTUFFCORE) << "check update finished.";
260 QList<Entry> updatable;
261 for (const Entry &entry : std::as_const(t&: mCachedEntries)) {
262 if (entry.status() == KNSCore::Entry::Updateable) {
263 updatable.append(t: entry);
264 }
265 }
266 Q_EMIT loadingFinished(mCurrentRequest, updatable);
267 }
268}
269
270void AtticaProvider::categoryContentsLoaded(BaseJob *job)
271{
272 if (!jobSuccess(job)) {
273 return;
274 }
275
276 auto *listJob = static_cast<ListJob<Content> *>(job);
277 const Content::List contents = listJob->itemList();
278
279 Entry::List entries;
280 TagsFilterChecker checker(tagFilter());
281 TagsFilterChecker downloadschecker(downloadTagFilter());
282 for (const Content &content : contents) {
283 if (!content.isValid()) {
284 qCDebug(KNEWSTUFFCORE)
285 << "Filtered out an invalid entry. This suggests something is not right on the originating server. Please contact the administrators of"
286 << name() << "and inform them there is an issue with content in the category or categories" << mCurrentRequest.categories;
287 continue;
288 }
289 if (checker.filterAccepts(tags: content.tags())) {
290 bool filterAcceptsDownloads = true;
291 if (content.downloads() > 0) {
292 filterAcceptsDownloads = false;
293 const QList<Attica::DownloadDescription> descs = content.downloadUrlDescriptions();
294 for (const Attica::DownloadDescription &dli : descs) {
295 if (downloadschecker.filterAccepts(tags: dli.tags())) {
296 filterAcceptsDownloads = true;
297 break;
298 }
299 }
300 }
301 if (filterAcceptsDownloads) {
302 mCachedContent.insert(key: content.id(), value: content);
303 entries.append(t: entryFromAtticaContent(content));
304 } else {
305 qCDebug(KNEWSTUFFCORE) << "Filter has excluded" << content.name() << "on download filter" << downloadTagFilter();
306 }
307 } else {
308 qCDebug(KNEWSTUFFCORE) << "Filter has excluded" << content.name() << "on entry filter" << tagFilter();
309 }
310 }
311
312 qCDebug(KNEWSTUFFCORE) << "loaded: " << mCurrentRequest.hashForRequest() << " count: " << entries.size();
313 Q_EMIT loadingFinished(mCurrentRequest, entries);
314 mEntryJob = nullptr;
315}
316
317Attica::Provider::SortMode AtticaProvider::atticaSortMode(SortMode sortMode)
318{
319 switch (sortMode) {
320 case Newest:
321 return Attica::Provider::Newest;
322 case Alphabetical:
323 return Attica::Provider::Alphabetical;
324 case Downloads:
325 return Attica::Provider::Downloads;
326 default:
327 return Attica::Provider::Rating;
328 }
329}
330
331void AtticaProvider::loadPayloadLink(const KNSCore::Entry &entry, int linkId)
332{
333 Attica::Content content = mCachedContent.value(key: entry.uniqueId());
334 const DownloadDescription desc = content.downloadUrlDescription(number: linkId);
335
336 if (desc.hasPrice()) {
337 // Ask for balance, then show information...
338 ItemJob<AccountBalance> *job = m_provider.requestAccountBalance();
339 connect(sender: job, signal: &BaseJob::finished, context: this, slot: &AtticaProvider::accountBalanceLoaded);
340 mDownloadLinkJobs[job] = qMakePair(value1: entry, value2&: linkId);
341 job->start();
342
343 qCDebug(KNEWSTUFFCORE) << "get account balance";
344 } else {
345 ItemJob<DownloadItem> *job = m_provider.downloadLink(contentId: entry.uniqueId(), itemId: QString::number(linkId));
346 connect(sender: job, signal: &BaseJob::finished, context: this, slot: &AtticaProvider::downloadItemLoaded);
347 mDownloadLinkJobs[job] = qMakePair(value1: entry, value2&: linkId);
348 job->start();
349
350 qCDebug(KNEWSTUFFCORE) << " link for " << entry.uniqueId();
351 }
352}
353
354void AtticaProvider::loadComments(const Entry &entry, int commentsPerPage, int page)
355{
356 ListJob<Attica::Comment> *job = m_provider.requestComments(commentType: Attica::Comment::ContentComment, id: entry.uniqueId(), QStringLiteral("0"), page, pageSize: commentsPerPage);
357 connect(sender: job, signal: &BaseJob::finished, context: this, slot: &AtticaProvider::loadedComments);
358 job->start();
359}
360
361QList<std::shared_ptr<KNSCore::Comment>> getCommentsList(const Attica::Comment::List &comments, std::shared_ptr<KNSCore::Comment> parent)
362{
363 QList<std::shared_ptr<KNSCore::Comment>> knsComments;
364 for (const Attica::Comment &comment : comments) {
365 qCDebug(KNEWSTUFFCORE) << "Appending comment with id" << comment.id() << ", which has" << comment.childCount() << "children";
366 auto knsComment = std::make_shared<KNSCore::Comment>();
367 knsComment->id = comment.id();
368 knsComment->subject = comment.subject();
369 knsComment->text = comment.text();
370 knsComment->childCount = comment.childCount();
371 knsComment->username = comment.user();
372 knsComment->date = comment.date();
373 knsComment->score = comment.score();
374 knsComment->parent = parent;
375 knsComments << knsComment;
376 if (comment.childCount() > 0) {
377 qCDebug(KNEWSTUFFCORE) << "Getting more comments, as this one has children, and we currently have this number of comments:" << knsComments.count();
378 knsComments << getCommentsList(comments: comment.children(), parent: knsComment);
379 qCDebug(KNEWSTUFFCORE) << "After getting the children, we now have the following number of comments:" << knsComments.count();
380 }
381 }
382 return knsComments;
383}
384
385void AtticaProvider::loadedComments(Attica::BaseJob *baseJob)
386{
387 if (!jobSuccess(job: baseJob)) {
388 return;
389 }
390
391 auto *job = static_cast<ListJob<Attica::Comment> *>(baseJob);
392 Attica::Comment::List comments = job->itemList();
393
394 QList<std::shared_ptr<KNSCore::Comment>> receivedComments = getCommentsList(comments, parent: nullptr);
395 Q_EMIT commentsLoaded(comments: receivedComments);
396}
397
398void AtticaProvider::loadPerson(const QString &username)
399{
400 if (m_provider.hasPersonService()) {
401 ItemJob<Attica::Person> *job = m_provider.requestPerson(id: username);
402 job->setProperty(name: "username", value: username);
403 connect(sender: job, signal: &BaseJob::finished, context: this, slot: &AtticaProvider::loadedPerson);
404 job->start();
405 }
406}
407
408void AtticaProvider::loadedPerson(Attica::BaseJob *baseJob)
409{
410 if (!jobSuccess(job: baseJob)) {
411 return;
412 }
413
414 auto *job = static_cast<ItemJob<Attica::Person> *>(baseJob);
415 Attica::Person person = job->result();
416
417 auto author = std::make_shared<KNSCore::Author>();
418 // This is a touch hack-like, but it ensures we actually have the data in case it is not returned by the server
419 author->setId(job->property(name: "username").toString());
420 author->setName(QStringLiteral("%1 %2").arg(args: person.firstName(), args: person.lastName()).trimmed());
421 author->setHomepage(person.homepage());
422 author->setProfilepage(person.extendedAttribute(QStringLiteral("profilepage")));
423 author->setAvatarUrl(person.avatarUrl());
424 author->setDescription(person.extendedAttribute(QStringLiteral("description")));
425 Q_EMIT personLoaded(author);
426}
427
428void AtticaProvider::loadBasics()
429{
430 Attica::ItemJob<Attica::Config> *configJob = m_provider.requestConfig();
431 connect(sender: configJob, signal: &BaseJob::finished, context: this, slot: &AtticaProvider::loadedConfig);
432 configJob->start();
433}
434
435void AtticaProvider::loadedConfig(Attica::BaseJob *baseJob)
436{
437 if (jobSuccess(job: baseJob)) {
438 auto *job = static_cast<ItemJob<Attica::Config> *>(baseJob);
439 Attica::Config config = job->result();
440 setVersion(config.version());
441 setSupportsSsl(config.ssl());
442 setContactEmail(config.contact());
443 QString protocol{QStringLiteral("http")};
444 if (config.ssl()) {
445 protocol = QStringLiteral("https");
446 }
447 // There is usually no protocol in the website and host, but in case
448 // there is, trust what's there
449 if (config.website().contains(s: QLatin1String("://"))) {
450 setWebsite(QUrl(config.website()));
451 } else {
452 setWebsite(QUrl(QLatin1String("%1://%2").arg(args&: protocol).arg(a: config.website())));
453 }
454 if (config.host().contains(s: QLatin1String("://"))) {
455 setHost(QUrl(config.host()));
456 } else {
457 setHost(QUrl(QLatin1String("%1://%2").arg(args&: protocol).arg(a: config.host())));
458 }
459 }
460}
461
462void AtticaProvider::accountBalanceLoaded(Attica::BaseJob *baseJob)
463{
464 if (!jobSuccess(job: baseJob)) {
465 return;
466 }
467
468 auto *job = static_cast<ItemJob<AccountBalance> *>(baseJob);
469 AccountBalance item = job->result();
470
471 QPair<Entry, int> pair = mDownloadLinkJobs.take(key: job);
472 Entry entry(pair.first);
473 Content content = mCachedContent.value(key: entry.uniqueId());
474 if (content.downloadUrlDescription(number: pair.second).priceAmount() < item.balance()) {
475 qCDebug(KNEWSTUFFCORE) << "Your balance is greater than the price." << content.downloadUrlDescription(number: pair.second).priceAmount()
476 << " balance: " << item.balance();
477 Question question;
478 question.setEntry(entry);
479 question.setQuestion(i18nc("the price of a download item, parameter 1 is the currency, 2 is the price",
480 "This item costs %1 %2.\nDo you want to buy it?",
481 item.currency(),
482 content.downloadUrlDescription(pair.second).priceAmount()));
483 if (question.ask() == Question::YesResponse) {
484 ItemJob<DownloadItem> *job = m_provider.downloadLink(contentId: entry.uniqueId(), itemId: QString::number(pair.second));
485 connect(sender: job, signal: &BaseJob::finished, context: this, slot: &AtticaProvider::downloadItemLoaded);
486 mDownloadLinkJobs[job] = qMakePair(value1&: entry, value2&: pair.second);
487 job->start();
488 } else {
489 return;
490 }
491 } else {
492 qCDebug(KNEWSTUFFCORE) << "You don't have enough money on your account!" << content.downloadUrlDescription(number: 0).priceAmount()
493 << " balance: " << item.balance();
494 Q_EMIT signalInformation(i18n("Your account balance is too low:\nYour balance: %1\nPrice: %2", //
495 item.balance(),
496 content.downloadUrlDescription(0).priceAmount()));
497 }
498}
499
500void AtticaProvider::downloadItemLoaded(BaseJob *baseJob)
501{
502 if (!jobSuccess(job: baseJob)) {
503 return;
504 }
505
506 auto *job = static_cast<ItemJob<DownloadItem> *>(baseJob);
507 DownloadItem item = job->result();
508
509 Entry entry = mDownloadLinkJobs.take(key: job).first;
510 entry.setPayload(QString(item.url().toString()));
511 Q_EMIT payloadLinkLoaded(entry);
512}
513
514Entry::List AtticaProvider::installedEntries() const
515{
516 Entry::List entries;
517 for (const Entry &entry : std::as_const(t: mCachedEntries)) {
518 if (entry.status() == KNSCore::Entry::Installed || entry.status() == KNSCore::Entry::Updateable) {
519 entries.append(t: entry);
520 }
521 }
522 return entries;
523}
524
525void AtticaProvider::vote(const Entry &entry, uint rating)
526{
527 PostJob *job = m_provider.voteForContent(contentId: entry.uniqueId(), rating);
528 connect(sender: job, signal: &BaseJob::finished, context: this, slot: &AtticaProvider::votingFinished);
529 job->start();
530}
531
532void AtticaProvider::votingFinished(Attica::BaseJob *job)
533{
534 if (!jobSuccess(job)) {
535 return;
536 }
537 Q_EMIT signalInformation(i18nc("voting for an item (good/bad)", "Your vote was recorded."));
538}
539
540void AtticaProvider::becomeFan(const Entry &entry)
541{
542 PostJob *job = m_provider.becomeFan(contentId: entry.uniqueId());
543 connect(sender: job, signal: &BaseJob::finished, context: this, slot: &AtticaProvider::becomeFanFinished);
544 job->start();
545}
546
547void AtticaProvider::becomeFanFinished(Attica::BaseJob *job)
548{
549 if (!jobSuccess(job)) {
550 return;
551 }
552 Q_EMIT signalInformation(i18n("You are now a fan."));
553}
554
555bool AtticaProvider::jobSuccess(Attica::BaseJob *job)
556{
557 if (job->metadata().error() == Attica::Metadata::NoError) {
558 return true;
559 }
560 qCDebug(KNEWSTUFFCORE) << "job error: " << job->metadata().error() << " status code: " << job->metadata().statusCode() << job->metadata().message();
561
562 if (job->metadata().error() == Attica::Metadata::NetworkError) {
563 if (job->metadata().statusCode() == 503) {
564 QDateTime retryAfter;
565 static const QByteArray retryAfterKey{"Retry-After"};
566 for (const QNetworkReply::RawHeaderPair &headerPair : job->metadata().headers()) {
567 if (headerPair.first == retryAfterKey) {
568 // Retry-After is not a known header, so we need to do a bit of running around to make that work
569 // Also, the fromHttpDate function is in the private qnetworkrequest header, so we can't use that
570 // So, simple workaround, just pass it through a dummy request and get a formatted date out (the
571 // cost is sufficiently low here, given we've just done a bunch of i/o heavy things, so...)
572 QNetworkRequest dummyRequest;
573 dummyRequest.setRawHeader(headerName: QByteArray{"Last-Modified"}, value: headerPair.second);
574 retryAfter = dummyRequest.header(header: QNetworkRequest::LastModifiedHeader).toDateTime();
575 break;
576 }
577 }
578 static const KFormat formatter;
579 Q_EMIT signalErrorCode(errorCode: KNSCore::ErrorCode::TryAgainLaterError,
580 i18n("The service is currently undergoing maintenance and is expected to be back in %1.",
581 formatter.formatSpelloutDuration(retryAfter.toMSecsSinceEpoch() - QDateTime::currentMSecsSinceEpoch())),
582 metadata: {retryAfter});
583 } else {
584 Q_EMIT signalErrorCode(errorCode: KNSCore::ErrorCode::NetworkError,
585 i18n("Network error %1: %2", job->metadata().statusCode(), job->metadata().statusString()),
586 metadata: job->metadata().statusCode());
587 }
588 }
589 if (job->metadata().error() == Attica::Metadata::OcsError) {
590 if (job->metadata().statusCode() == 200) {
591 Q_EMIT signalErrorCode(errorCode: KNSCore::ErrorCode::OcsError,
592 i18n("Too many requests to server. Please try again in a few minutes."),
593 metadata: job->metadata().statusCode());
594 } else if (job->metadata().statusCode() == 405) {
595 Q_EMIT signalErrorCode(errorCode: KNSCore::ErrorCode::OcsError,
596 i18n("The Open Collaboration Services instance %1 does not support the attempted function.", name()),
597 metadata: job->metadata().statusCode());
598 } else {
599 Q_EMIT signalErrorCode(errorCode: KNSCore::ErrorCode::OcsError,
600 i18n("Unknown Open Collaboration Service API error. (%1)", job->metadata().statusCode()),
601 metadata: job->metadata().statusCode());
602 }
603 }
604
605 if (auto searchRequestVar = job->property(name: "searchRequest"); searchRequestVar.isValid()) {
606 SearchRequest req = searchRequestVar.value<SearchRequest>();
607 Q_EMIT loadingFailed(req);
608 }
609 return false;
610}
611
612Entry AtticaProvider::entryFromAtticaContent(const Attica::Content &content)
613{
614 Entry entry;
615
616 entry.setProviderId(id());
617 entry.setUniqueId(content.id());
618 entry.setStatus(KNSCore::Entry::Downloadable);
619 entry.setVersion(content.version());
620 entry.setReleaseDate(content.updated().date());
621 entry.setCategory(content.attribute(QStringLiteral("typeid")));
622
623 int index = mCachedEntries.indexOf(t: entry);
624 if (index >= 0) {
625 Entry &cacheEntry = mCachedEntries[index];
626 // check if updateable
627 if (((cacheEntry.status() == KNSCore::Entry::Installed) || (cacheEntry.status() == KNSCore::Entry::Updateable))
628 && ((cacheEntry.version() != entry.version()) || (cacheEntry.releaseDate() != entry.releaseDate()))) {
629 cacheEntry.setStatus(KNSCore::Entry::Updateable);
630 cacheEntry.setUpdateVersion(entry.version());
631 cacheEntry.setUpdateReleaseDate(entry.releaseDate());
632 }
633 entry = cacheEntry;
634 } else {
635 mCachedEntries.append(t: entry);
636 }
637
638 entry.setName(content.name());
639 entry.setHomepage(content.detailpage());
640 entry.setRating(content.rating());
641 entry.setNumberOfComments(content.numberOfComments());
642 entry.setDownloadCount(content.downloads());
643 entry.setNumberFans(content.attribute(QStringLiteral("fans")).toInt());
644 entry.setDonationLink(content.attribute(QStringLiteral("donationpage")));
645 entry.setKnowledgebaseLink(content.attribute(QStringLiteral("knowledgebasepage")));
646 entry.setNumberKnowledgebaseEntries(content.attribute(QStringLiteral("knowledgebaseentries")).toInt());
647 entry.setHomepage(content.detailpage());
648
649 entry.setPreviewUrl(url: content.smallPreviewPicture(QStringLiteral("1")), type: Entry::PreviewSmall1);
650 entry.setPreviewUrl(url: content.smallPreviewPicture(QStringLiteral("2")), type: Entry::PreviewSmall2);
651 entry.setPreviewUrl(url: content.smallPreviewPicture(QStringLiteral("3")), type: Entry::PreviewSmall3);
652
653 entry.setPreviewUrl(url: content.previewPicture(QStringLiteral("1")), type: Entry::PreviewBig1);
654 entry.setPreviewUrl(url: content.previewPicture(QStringLiteral("2")), type: Entry::PreviewBig2);
655 entry.setPreviewUrl(url: content.previewPicture(QStringLiteral("3")), type: Entry::PreviewBig3);
656
657 entry.setLicense(content.license());
658 Author author;
659 author.setId(content.author());
660 author.setName(content.author());
661 author.setHomepage(content.attribute(QStringLiteral("profilepage")));
662 entry.setAuthor(author);
663
664 entry.setSource(Entry::Online);
665 entry.setSummary(content.description());
666 entry.setShortSummary(content.summary());
667 entry.setChangelog(content.changelog());
668 entry.setTags(content.tags());
669
670 entry.clearDownloadLinkInformation();
671 const QList<Attica::DownloadDescription> descs = content.downloadUrlDescriptions();
672 for (const Attica::DownloadDescription &desc : descs) {
673 Entry::DownloadLinkInformation info;
674 info.name = desc.name();
675 info.priceAmount = desc.priceAmount();
676 info.distributionType = desc.distributionType();
677 info.descriptionLink = desc.link();
678 info.id = desc.id();
679 info.size = desc.size();
680 info.isDownloadtypeLink = desc.type() == Attica::DownloadDescription::LinkDownload;
681 info.tags = desc.tags();
682 entry.appendDownloadLinkInformation(info);
683 }
684
685 return entry;
686}
687
688} // namespace
689
690#include "moc_atticaprovider_p.cpp"
691

source code of knewstuff/src/attica/atticaprovider.cpp