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 | |
28 | using namespace Attica; |
29 | |
30 | namespace KNSCore |
31 | { |
32 | AtticaProvider::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 | |
48 | AtticaProvider::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 | |
60 | QString AtticaProvider::id() const |
61 | { |
62 | return m_providerId; |
63 | } |
64 | |
65 | void AtticaProvider::onAuthenticationCredentialsMissing(const Attica::Provider &) |
66 | { |
67 | qCDebug(KNEWSTUFFCORE) << "Authentication missing!" ; |
68 | // FIXME Show authentication dialog |
69 | } |
70 | |
71 | bool 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 | |
93 | void AtticaProvider::setCachedEntries(const KNSCore::Entry::List &cachedEntries) |
94 | { |
95 | mCachedEntries = cachedEntries; |
96 | } |
97 | |
98 | void 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 | |
113 | void 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 | |
169 | bool AtticaProvider::isInitialized() const |
170 | { |
171 | return mInitialized; |
172 | } |
173 | |
174 | void 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 | |
225 | void 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 | |
240 | void 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 | |
247 | void 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 | |
270 | void 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 | |
317 | Attica::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 | |
331 | void 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 | |
354 | void AtticaProvider::(const Entry &entry, int , 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 | |
361 | QList<std::shared_ptr<KNSCore::Comment>> (const Attica::Comment::List &, std::shared_ptr<KNSCore::Comment> parent) |
362 | { |
363 | QList<std::shared_ptr<KNSCore::Comment>> ; |
364 | for (const Attica::Comment & : comments) { |
365 | qCDebug(KNEWSTUFFCORE) << "Appending comment with id" << comment.id() << ", which has" << comment.childCount() << "children" ; |
366 | auto = 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 | |
385 | void AtticaProvider::(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 = job->itemList(); |
393 | |
394 | QList<std::shared_ptr<KNSCore::Comment>> = getCommentsList(comments, parent: nullptr); |
395 | Q_EMIT commentsLoaded(comments: receivedComments); |
396 | } |
397 | |
398 | void 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 | |
408 | void 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 | |
428 | void 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 | |
435 | void 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 | |
462 | void 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 | |
500 | void 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 | |
514 | Entry::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 | |
525 | void 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 | |
532 | void 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 | |
540 | void 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 | |
547 | void 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 | |
555 | bool 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 & : 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 | |
612 | Entry 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 | |