7#include "atticaprovider_p.h"
9#include "commentsmodel.h"
12#include "tagsfilterchecker.h"
15#include <KLocalizedString>
17#include <QDomDocument>
18#include <knewstuffcore_debug.h>
20#include <attica/accountbalance.h>
21#include <attica/config.h>
22#include <attica/content.h>
23#include <attica/downloaditem.h>
24#include <attica/listjob.h>
25#include <attica/person.h>
26#include <attica/provider.h>
27#include <attica/providermanager.h>
33AtticaProvider::AtticaProvider(
const QStringList &categories,
const QString &additionalAgentInformation)
38 for (
const QString &category : categories) {
42 connect(&m_providerManager, &ProviderManager::providerAdded,
this, [
this, additionalAgentInformation](
const Attica::Provider &provider) {
43 providerLoaded(provider);
44 m_provider.setAdditionalAgentInformation(additionalAgentInformation);
46 connect(&m_providerManager, &ProviderManager::authenticationCredentialsMissing,
this, &AtticaProvider::onAuthenticationCredentialsMissing);
54 for (
const QString &category : categories) {
57 providerLoaded(provider);
58 m_provider.setAdditionalAgentInformation(additionalAgentInformation);
61QString AtticaProvider::id()
const
66void AtticaProvider::onAuthenticationCredentialsMissing(
const Attica::Provider &)
68 qCDebug(KNEWSTUFFCORE) <<
"Authentication missing!";
72bool AtticaProvider::setProviderXML(
const QDomElement &xmldata)
80 qCDebug(KNEWSTUFFCORE) <<
"setting provider xml" << doc.toString();
83 m_providerManager.addProviderFromXml(doc.toString());
85 if (!m_providerManager.providers().isEmpty()) {
86 qCDebug(KNEWSTUFFCORE) <<
"base url of attica provider:" << m_providerManager.providers().constLast().baseUrl().toString();
88 qCCritical(KNEWSTUFFCORE) <<
"Could not load provider.";
96 mCachedEntries = cachedEntries;
101 setName(provider.
name());
102 setIcon(provider.
icon());
103 qCDebug(KNEWSTUFFCORE) <<
"Added provider: " << provider.
name();
105 m_provider = provider;
110 connect(job, &BaseJob::finished,
this, &AtticaProvider::listOfCategoriesLoaded);
116 if (!jobSuccess(listJob)) {
120 qCDebug(KNEWSTUFFCORE) <<
"loading categories: " << mCategoryMap.keys();
123 const Category::List categoryList = job->itemList();
126 for (
const Category &category : categoryList) {
127 if (mCategoryMap.contains(
category.name())) {
128 qCDebug(KNEWSTUFFCORE) <<
"Adding category: " <<
category.name() <<
category.displayName();
130 if (mCategoryMap.contains(
category.name()) && !mCategoryMap.value(
category.name()).isValid()) {
131 mCategoryMap.replace(
category.name(), category);
133 mCategoryMap.insert(
category.name(), category);
136 CategoryMetadata categoryMetadata;
137 categoryMetadata.id =
category.id();
138 categoryMetadata.name =
category.name();
139 categoryMetadata.displayName =
category.displayName();
140 categoryMetadataList << categoryMetadata;
143 std::sort(categoryMetadataList.
begin(),
144 categoryMetadataList.
end(),
145 [](
const AtticaProvider::CategoryMetadata &i,
const AtticaProvider::CategoryMetadata &j) ->
bool {
146 const QString a(i.displayName.isEmpty() ? i.name : i.displayName);
147 const QString b(j.displayName.isEmpty() ? j.name : j.displayName);
149 return (QCollator().compare(a, b) < 0);
152 bool correct =
false;
153 for (
auto it = mCategoryMap.cbegin(), itEnd = mCategoryMap.cend(); it != itEnd; ++it) {
154 if (!it.value().isValid()) {
155 qCWarning(KNEWSTUFFCORE) <<
"Could not find category" << it.key();
163 Q_EMIT providerInitialized(
this);
164 Q_EMIT categoriesMetadataLoded(categoryMetadataList);
166 Q_EMIT signalErrorCode(KNSCore::ErrorCode::ConfigFileError,
i18n(
"All categories are missing"),
QVariant());
170bool AtticaProvider::isInitialized()
const
182 mCurrentRequest = request;
183 switch (request.filter) {
188 job->
setProperty(
"providedEntryId", request.searchTerm);
189 connect(job, &BaseJob::finished,
this, &AtticaProvider::detailsLoaded);
194 if (request.page == 0) {
195 Q_EMIT loadingFinished(request, installedEntries());
197 Q_EMIT loadingFinished(request, Entry::List());
205 Attica::Provider::SortMode sorting = atticaSortMode(request.sortMode);
206 Attica::Category::List categoriesToSearch;
208 if (request.categories.
isEmpty()) {
210 categoriesToSearch = mCategoryMap.values();
212 categoriesToSearch.reserve(request.categories.
size());
213 for (
const QString &categoryName : std::as_const(request.categories)) {
214 categoriesToSearch.append(mCategoryMap.values(categoryName));
218 ListJob<Content> *job = m_provider.searchContents(categoriesToSearch, request.searchTerm, sorting, request.page, request.pageSize);
220 connect(job, &BaseJob::finished,
this, &AtticaProvider::categoryContentsLoaded);
226void AtticaProvider::checkForUpdates()
228 if (mCachedEntries.isEmpty()) {
229 Q_EMIT loadingFinished(mCurrentRequest, {});
232 for (
const Entry &e : std::as_const(mCachedEntries)) {
234 connect(job, &BaseJob::finished,
this, &AtticaProvider::detailsLoaded);
235 m_updateJobs.insert(job);
237 qCDebug(KNEWSTUFFCORE) <<
"Checking for update: " << e.name();
244 connect(job, &BaseJob::finished,
this, &AtticaProvider::detailsLoaded);
248void AtticaProvider::detailsLoaded(
BaseJob *job)
250 if (jobSuccess(job)) {
252 Content content = contentJob->result();
253 Entry entry = entryFromAtticaContent(content);
255 Q_EMIT entryDetailsLoaded(entry);
256 qCDebug(KNEWSTUFFCORE) <<
"check update finished: " << entry.name();
259 if (m_updateJobs.remove(job) && m_updateJobs.isEmpty()) {
260 qCDebug(KNEWSTUFFCORE) <<
"check update finished.";
262 for (
const Entry &entry : std::as_const(mCachedEntries)) {
263 if (entry.status() == KNSCore::Entry::Updateable) {
267 Q_EMIT loadingFinished(mCurrentRequest, updatable);
271void AtticaProvider::categoryContentsLoaded(
BaseJob *job)
273 if (!jobSuccess(job)) {
278 const Content::List contents = listJob->itemList();
281 TagsFilterChecker checker(tagFilter());
282 TagsFilterChecker downloadschecker(downloadTagFilter());
283 for (
const Content &content : contents) {
284 if (!content.isValid()) {
285 qCDebug(KNEWSTUFFCORE)
286 <<
"Filtered out an invalid entry. This suggests something is not right on the originating server. Please contact the administrators of"
287 <<
name() <<
"and inform them there is an issue with content in the category or categories" << mCurrentRequest.categories;
290 if (checker.filterAccepts(content.tags())) {
291 bool filterAcceptsDownloads =
true;
292 if (content.downloads() > 0) {
293 filterAcceptsDownloads =
false;
296 if (downloadschecker.filterAccepts(dli.tags())) {
297 filterAcceptsDownloads =
true;
302 if (filterAcceptsDownloads) {
303 mCachedContent.insert(content.id(), content);
304 entries.append(entryFromAtticaContent(content));
306 qCDebug(KNEWSTUFFCORE) <<
"Filter has excluded" << content.name() <<
"on download filter" << downloadTagFilter();
309 qCDebug(KNEWSTUFFCORE) <<
"Filter has excluded" << content.name() <<
"on entry filter" << tagFilter();
313 qCDebug(KNEWSTUFFCORE) <<
"loaded: " << mCurrentRequest.hashForRequest() <<
" count: " << entries.size();
314 Q_EMIT loadingFinished(mCurrentRequest, entries);
318Attica::Provider::SortMode AtticaProvider::atticaSortMode(SortMode sortMode)
322 return Attica::Provider::Newest;
324 return Attica::Provider::Alphabetical;
326 return Attica::Provider::Downloads;
328 return Attica::Provider::Rating;
332void AtticaProvider::loadPayloadLink(
const KNSCore::Entry &entry,
int linkId)
337 if (desc.hasPrice()) {
340 connect(job, &BaseJob::finished,
this, &AtticaProvider::accountBalanceLoaded);
341 mDownloadLinkJobs[job] = qMakePair(entry, linkId);
344 qCDebug(KNEWSTUFFCORE) <<
"get account balance";
347 connect(job, &BaseJob::finished,
this, &AtticaProvider::downloadItemLoaded);
348 mDownloadLinkJobs[job] = qMakePair(entry, linkId);
351 qCDebug(KNEWSTUFFCORE) <<
" link for " << entry.uniqueId();
355void AtticaProvider::loadComments(
const Entry &entry,
int commentsPerPage,
int page)
357 ListJob<Attica::Comment> *job = m_provider.requestComments(Attica::Comment::ContentComment, entry.uniqueId(), QStringLiteral(
"0"), page, commentsPerPage);
358 connect(job, &BaseJob::finished,
this, &AtticaProvider::loadedComments);
366 qCDebug(KNEWSTUFFCORE) <<
"Appending comment with id" << comment.id() <<
", which has" << comment.childCount() <<
"children";
367 auto knsComment = std::make_shared<KNSCore::Comment>();
368 knsComment->id = comment.id();
369 knsComment->subject = comment.subject();
370 knsComment->text = comment.text();
371 knsComment->childCount = comment.childCount();
372 knsComment->username = comment.user();
373 knsComment->date = comment.date();
374 knsComment->score = comment.score();
375 knsComment->parent = parent;
376 knsComments << knsComment;
377 if (comment.childCount() > 0) {
378 qCDebug(KNEWSTUFFCORE) <<
"Getting more comments, as this one has children, and we currently have this number of comments:" << knsComments.
count();
379 knsComments << getCommentsList(comment.children(), knsComment);
380 qCDebug(KNEWSTUFFCORE) <<
"After getting the children, we now have the following number of comments:" << knsComments.
count();
388 if (!jobSuccess(baseJob)) {
393 Attica::Comment::List
comments = job->itemList();
396 Q_EMIT commentsLoaded(receivedComments);
399void AtticaProvider::loadPerson(
const QString &username)
401 if (m_provider.hasPersonService()) {
404 connect(job, &BaseJob::finished,
this, &AtticaProvider::loadedPerson);
411 if (!jobSuccess(baseJob)) {
418 auto author = std::make_shared<KNSCore::Author>();
421 author->setName(QStringLiteral(
"%1 %2").arg(person.firstName(), person.lastName()).trimmed());
422 author->setHomepage(person.homepage());
423 author->setProfilepage(person.extendedAttribute(QStringLiteral(
"profilepage")));
424 author->setAvatarUrl(person.avatarUrl());
425 author->setDescription(person.extendedAttribute(QStringLiteral(
"description")));
426 Q_EMIT personLoaded(author);
429void AtticaProvider::loadBasics()
432 connect(configJob, &BaseJob::finished,
this, &AtticaProvider::loadedConfig);
438 if (jobSuccess(baseJob)) {
441 setVersion(config.version());
442 setSupportsSsl(config.ssl());
443 setContactEmail(config.contact());
444 QString protocol{QStringLiteral(
"http")};
446 protocol = QStringLiteral(
"https");
451 setWebsite(
QUrl(config.website()));
456 setHost(
QUrl(config.host()));
465 if (!jobSuccess(baseJob)) {
472 QPair<Entry, int> pair = mDownloadLinkJobs.take(job);
473 Entry entry(pair.first);
474 Content content = mCachedContent.value(entry.uniqueId());
476 qCDebug(KNEWSTUFFCORE) <<
"Your balance is greater than the price." << content.
downloadUrlDescription(pair.second).priceAmount()
477 <<
" balance: " << item.balance();
479 question.setEntry(entry);
480 question.setQuestion(
i18nc(
"the price of a download item, parameter 1 is the currency, 2 is the price",
481 "This item costs %1 %2.\nDo you want to buy it?",
484 if (question.ask() == Question::YesResponse) {
486 connect(job, &BaseJob::finished,
this, &AtticaProvider::downloadItemLoaded);
487 mDownloadLinkJobs[job] = qMakePair(entry, pair.second);
493 qCDebug(KNEWSTUFFCORE) <<
"You don't have enough money on your account!" << content.
downloadUrlDescription(0).priceAmount()
494 <<
" balance: " << item.balance();
495 Q_EMIT signalInformation(
i18n(
"Your account balance is too low:\nYour balance: %1\nPrice: %2",
501void AtticaProvider::downloadItemLoaded(
BaseJob *baseJob)
503 if (!jobSuccess(baseJob)) {
510 Entry entry = mDownloadLinkJobs.take(job).first;
511 entry.setPayload(
QString(item.url().toString()));
512 Q_EMIT payloadLinkLoaded(entry);
515Entry::List AtticaProvider::installedEntries()
const
518 for (
const Entry &entry : std::as_const(mCachedEntries)) {
519 if (entry.status() == KNSCore::Entry::Installed || entry.status() == KNSCore::Entry::Updateable) {
520 entries.append(entry);
526void AtticaProvider::vote(
const Entry &entry, uint rating)
528 PostJob *job = m_provider.voteForContent(entry.uniqueId(), rating);
529 connect(job, &BaseJob::finished,
this, &AtticaProvider::votingFinished);
535 if (!jobSuccess(job)) {
538 Q_EMIT signalInformation(
i18nc(
"voting for an item (good/bad)",
"Your vote was recorded."));
541void AtticaProvider::becomeFan(
const Entry &entry)
543 PostJob *job = m_provider.becomeFan(entry.uniqueId());
544 connect(job, &BaseJob::finished,
this, &AtticaProvider::becomeFanFinished);
550 if (!jobSuccess(job)) {
553 Q_EMIT signalInformation(
i18n(
"You are now a fan."));
558 if (job->metadata().error() == Attica::Metadata::NoError) {
561 qCDebug(KNEWSTUFFCORE) <<
"job error: " << job->metadata().error() <<
" status code: " << job->metadata().statusCode() << job->metadata().message();
563 if (job->metadata().error() == Attica::Metadata::NetworkError) {
564 if (job->metadata().statusCode() == 503) {
566 static const QByteArray retryAfterKey{
"Retry-After"};
568 if (headerPair.first == retryAfterKey) {
579 static const KFormat formatter;
580 Q_EMIT signalErrorCode(KNSCore::ErrorCode::TryAgainLaterError,
581 i18n(
"The service is currently undergoing maintenance and is expected to be back in %1.",
585 Q_EMIT signalErrorCode(KNSCore::ErrorCode::NetworkError,
586 i18n(
"Network error %1: %2", job->metadata().statusCode(), job->metadata().statusString()),
587 job->metadata().statusCode());
590 if (job->metadata().error() == Attica::Metadata::OcsError) {
591 if (job->metadata().statusCode() == 200) {
592 Q_EMIT signalErrorCode(KNSCore::ErrorCode::OcsError,
593 i18n(
"Too many requests to server. Please try again in a few minutes."),
594 job->metadata().statusCode());
595 }
else if (job->metadata().statusCode() == 405) {
596 Q_EMIT signalErrorCode(KNSCore::ErrorCode::OcsError,
597 i18n(
"The Open Collaboration Services instance %1 does not support the attempted function.",
name()),
598 job->metadata().statusCode());
600 Q_EMIT signalErrorCode(KNSCore::ErrorCode::OcsError,
601 i18n(
"Unknown Open Collaboration Service API error. (%1)", job->metadata().statusCode()),
602 job->metadata().statusCode());
606 if (
auto searchRequestVar = job->
property(
"searchRequest"); searchRequestVar.isValid()) {
607 SearchRequest req = searchRequestVar.value<SearchRequest>();
608 Q_EMIT loadingFailed(req);
613Entry AtticaProvider::entryFromAtticaContent(
const Attica::Content &content)
617 entry.setProviderId(
id());
618 entry.setUniqueId(content.
id());
619 entry.setStatus(KNSCore::Entry::Downloadable);
620 entry.setVersion(content.version());
622 entry.setCategory(content.
attribute(QStringLiteral(
"typeid")));
624 int index = mCachedEntries.indexOf(entry);
626 Entry &cacheEntry = mCachedEntries[index];
628 if (((cacheEntry.status() == KNSCore::Entry::Installed) || (cacheEntry.status() == KNSCore::Entry::Updateable))
629 && ((cacheEntry.version() != entry.version()) || (cacheEntry.releaseDate() != entry.releaseDate()))) {
630 cacheEntry.setStatus(KNSCore::Entry::Updateable);
631 cacheEntry.setUpdateVersion(entry.version());
632 cacheEntry.setUpdateReleaseDate(entry.releaseDate());
636 mCachedEntries.append(entry);
639 entry.setName(content.
name());
641 entry.setRating(content.
rating());
643 entry.setDownloadCount(content.
downloads());
644 entry.setNumberFans(content.
attribute(QStringLiteral(
"fans")).toInt());
645 entry.setDonationLink(content.
attribute(QStringLiteral(
"donationpage")));
646 entry.setKnowledgebaseLink(content.
attribute(QStringLiteral(
"knowledgebasepage")));
647 entry.setNumberKnowledgebaseEntries(content.
attribute(QStringLiteral(
"knowledgebaseentries")).toInt());
650 entry.setPreviewUrl(content.smallPreviewPicture(QStringLiteral(
"1")), Entry::PreviewSmall1);
651 entry.setPreviewUrl(content.smallPreviewPicture(QStringLiteral(
"2")), Entry::PreviewSmall2);
652 entry.setPreviewUrl(content.smallPreviewPicture(QStringLiteral(
"3")), Entry::PreviewSmall3);
654 entry.setPreviewUrl(content.previewPicture(QStringLiteral(
"1")), Entry::PreviewBig1);
655 entry.setPreviewUrl(content.previewPicture(QStringLiteral(
"2")), Entry::PreviewBig2);
656 entry.setPreviewUrl(content.previewPicture(QStringLiteral(
"3")), Entry::PreviewBig3);
658 entry.setLicense(content.license());
660 author.setId(content.author());
661 author.setName(content.author());
662 author.setHomepage(content.
attribute(QStringLiteral(
"profilepage")));
663 entry.setAuthor(author);
665 entry.setSource(Entry::Online);
667 entry.setShortSummary(content.
summary());
668 entry.setChangelog(content.changelog());
669 entry.setTags(content.
tags());
672 entry.d->mDownloadLinkInformationList.clear();
673 entry.d->mDownloadLinkInformationList.reserve(descs.
size());
675 entry.d->mDownloadLinkInformationList.append({.name = desc.name(),
676 .priceAmount = desc.priceAmount(),
677 .distributionType = desc.distributionType(),
678 .descriptionLink = desc.link(),
680 .isDownloadtypeLink = desc.type() == Attica::DownloadDescription::LinkDownload,
691#include "moc_atticaprovider_p.cpp"
QString description() const
QList< DownloadDescription > downloadUrlDescriptions() const
QString attribute(const QString &key) const
int numberOfComments() const
QDateTime updated() const
DownloadDescription downloadUrlDescription(int number) const
void setAdditionalAgentInformation(const QString &additionalInformation)
KNewStuff data entry container.
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
QString name(GameStandardAction id)
Category category(StandardShortcut id)
KEDUVOCDOCUMENT_EXPORT QStringList comments(const QString &language=QString())
qint64 currentMSecsSinceEpoch()
qint64 toMSecsSinceEpoch() const const
QString tagName() const const
QDomNode cloneNode(bool deep) const const
void append(QList< T > &&value)
qsizetype count() const const
bool isEmpty() const const
qsizetype size() const const
QVariant property(const char *name) const const
bool setProperty(const char *name, QVariant &&value)
QString number(double n, char format, int precision)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QString host(ComponentFormattingOptions options) const const
QVariant fromValue(T &&value)
QDateTime toDateTime() const const
QString toString() const const
used to keep track of a search