KNewStuff

atticaprovider.cpp
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 "entry_p.h"
11#include "question.h"
12#include "tagsfilterchecker.h"
13
14#include <KFormat>
15#include <KLocalizedString>
16#include <QCollator>
17#include <QDomDocument>
18#include <knewstuffcore_debug.h>
19
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>
28
29#include "atticarequester_p.h"
30
31using namespace Attica;
32
33namespace KNSCore
34{
35AtticaProvider::AtticaProvider(const QStringList &categories, const QString &additionalAgentInformation)
36 : mInitialized(false)
37{
38 // init categories map with invalid categories
39 for (const QString &category : categories) {
40 mCategoryMap.insert(category, Attica::Category());
41 }
42
43 connect(&m_providerManager, &ProviderManager::providerAdded, this, [this, additionalAgentInformation](const Attica::Provider &provider) {
44 providerLoaded(provider);
45 m_provider.setAdditionalAgentInformation(additionalAgentInformation);
46 });
47 connect(&m_providerManager, &ProviderManager::authenticationCredentialsMissing, this, &AtticaProvider::onAuthenticationCredentialsMissing);
48}
49
50AtticaProvider::AtticaProvider(const Attica::Provider &provider, const QStringList &categories, const QString &additionalAgentInformation)
51 : mInitialized(false)
52{
53 // init categories map with invalid categories
54 for (const QString &category : categories) {
55 mCategoryMap.insert(category, Attica::Category());
56 }
57 providerLoaded(provider);
58 m_provider.setAdditionalAgentInformation(additionalAgentInformation);
59}
60
61QString AtticaProvider::id() const
62{
63 return m_providerId;
64}
65
66void AtticaProvider::onAuthenticationCredentialsMissing(const Attica::Provider &)
67{
68 qCDebug(KNEWSTUFFCORE) << "Authentication missing!";
69 // FIXME Show authentication dialog
70}
71
72bool AtticaProvider::setProviderXML(const QDomElement &xmldata)
73{
74 if (xmldata.tagName() != QLatin1String("provider")) {
75 return false;
76 }
77
78 // FIXME this is quite ugly, repackaging the xml into a string
79 QDomDocument doc(QStringLiteral("temp"));
80 qCDebug(KNEWSTUFFCORE) << "setting provider xml" << doc.toString();
81
82 doc.appendChild(xmldata.cloneNode(true));
83 m_providerManager.addProviderFromXml(doc.toString());
84
85 if (!m_providerManager.providers().isEmpty()) {
86 qCDebug(KNEWSTUFFCORE) << "base url of attica provider:" << m_providerManager.providers().constLast().baseUrl().toString();
87 } else {
88 qCCritical(KNEWSTUFFCORE) << "Could not load provider.";
89 return false;
90 }
91 return true;
92}
93
94void AtticaProvider::setCachedEntries(const KNSCore::Entry::List &cachedEntries)
95{
96 mCachedEntries = cachedEntries;
97}
98
99void AtticaProvider::providerLoaded(const Attica::Provider &provider)
100{
101 setName(provider.name());
102 setIcon(provider.icon());
103 qCDebug(KNEWSTUFFCORE) << "Added provider: " << provider.name();
104
105 m_provider = provider;
107 m_providerId = provider.baseUrl().host();
108
109 Attica::ListJob<Attica::Category> *job = m_provider.requestCategories();
110 connect(job, &BaseJob::finished, this, &AtticaProvider::listOfCategoriesLoaded);
111 job->start();
112}
113
114void AtticaProvider::listOfCategoriesLoaded(Attica::BaseJob *listJob)
115{
116 if (!jobSuccess(listJob)) {
117 return;
118 }
119
120 qCDebug(KNEWSTUFFCORE) << "loading categories: " << mCategoryMap.keys();
121
122 auto *job = static_cast<Attica::ListJob<Attica::Category> *>(listJob);
123 const Category::List categoryList = job->itemList();
124
125 QList<CategoryMetadata> categoryMetadataList;
126 for (const Category &category : categoryList) {
127 if (mCategoryMap.contains(category.name())) {
128 qCDebug(KNEWSTUFFCORE) << "Adding category: " << category.name() << category.displayName();
129 // If there is only the placeholder category, replace it
130 if (mCategoryMap.contains(category.name()) && !mCategoryMap.value(category.name()).isValid()) {
131 mCategoryMap.replace(category.name(), category);
132 } else {
133 mCategoryMap.insert(category.name(), category);
134 }
135
136 CategoryMetadata categoryMetadata;
137 categoryMetadata.id = category.id();
138 categoryMetadata.name = category.name();
139 categoryMetadata.displayName = category.displayName();
140 categoryMetadataList << categoryMetadata;
141 }
142 }
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);
148
149 return (QCollator().compare(a, b) < 0);
150 });
151
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();
156 } else {
157 correct = true;
158 }
159 }
160
161 if (correct) {
162 mInitialized = true;
163 Q_EMIT providerInitialized(this);
164 Q_EMIT categoriesMetadataLoded(categoryMetadataList);
165 } else {
166 Q_EMIT signalErrorCode(KNSCore::ErrorCode::ConfigFileError, i18n("All categories are missing"), QVariant());
167 }
168}
169
170bool AtticaProvider::isInitialized() const
171{
172 return mInitialized;
173}
174
175void AtticaProvider::loadEntries(const KNSCore::Provider::SearchRequest &request)
176{
177 auto requester = new AtticaRequester(request, this, this);
178 connect(requester, &AtticaRequester::entryDetailsLoaded, this, &AtticaProvider::entryDetailsLoaded);
179 connect(requester, &AtticaRequester::loadingFinished, this, [this, requester](const KNSCore::Entry::List &list) {
180 Q_EMIT loadingFinished(requester->request(), list);
181 });
182 connect(requester, &AtticaRequester::loadingFailed, this, [this, requester] {
183 Q_EMIT loadingFailed(requester->request());
184 });
185 requester->start();
186}
187
188void AtticaProvider::loadEntryDetails(const KNSCore::Entry &entry)
189{
190 ItemJob<Content> *job = m_provider.requestContent(entry.uniqueId());
191 connect(job, &BaseJob::finished, this, [this, entry] {
192 Q_EMIT entryDetailsLoaded(entry);
193 });
194 job->start();
195}
196
197void AtticaProvider::loadPayloadLink(const KNSCore::Entry &entry, int linkId)
198{
199 Attica::Content content = mCachedContent.value(entry.uniqueId());
200 const DownloadDescription desc = content.downloadUrlDescription(linkId);
201
202 if (desc.hasPrice()) {
203 // Ask for balance, then show information...
204 ItemJob<AccountBalance> *job = m_provider.requestAccountBalance();
205 connect(job, &BaseJob::finished, this, &AtticaProvider::accountBalanceLoaded);
206 mDownloadLinkJobs[job] = qMakePair(entry, linkId);
207 job->start();
208
209 qCDebug(KNEWSTUFFCORE) << "get account balance";
210 } else {
211 ItemJob<DownloadItem> *job = m_provider.downloadLink(entry.uniqueId(), QString::number(linkId));
212 connect(job, &BaseJob::finished, this, &AtticaProvider::downloadItemLoaded);
213 mDownloadLinkJobs[job] = qMakePair(entry, linkId);
214 job->start();
215
216 qCDebug(KNEWSTUFFCORE) << " link for " << entry.uniqueId();
217 }
218}
219
220void AtticaProvider::loadComments(const Entry &entry, int commentsPerPage, int page)
221{
222 ListJob<Attica::Comment> *job = m_provider.requestComments(Attica::Comment::ContentComment, entry.uniqueId(), QStringLiteral("0"), page, commentsPerPage);
223 connect(job, &BaseJob::finished, this, &AtticaProvider::loadedComments);
224 job->start();
225}
226
227QList<std::shared_ptr<KNSCore::Comment>> getCommentsList(const Attica::Comment::List &comments, std::shared_ptr<KNSCore::Comment> parent)
228{
230 for (const Attica::Comment &comment : comments) {
231 qCDebug(KNEWSTUFFCORE) << "Appending comment with id" << comment.id() << ", which has" << comment.childCount() << "children";
232 auto knsComment = std::make_shared<KNSCore::Comment>();
233 knsComment->id = comment.id();
234 knsComment->subject = comment.subject();
235 knsComment->text = comment.text();
236 knsComment->childCount = comment.childCount();
237 knsComment->username = comment.user();
238 knsComment->date = comment.date();
239 knsComment->score = comment.score();
240 knsComment->parent = parent;
241 knsComments << knsComment;
242 if (comment.childCount() > 0) {
243 qCDebug(KNEWSTUFFCORE) << "Getting more comments, as this one has children, and we currently have this number of comments:" << knsComments.count();
244 knsComments << getCommentsList(comment.children(), knsComment);
245 qCDebug(KNEWSTUFFCORE) << "After getting the children, we now have the following number of comments:" << knsComments.count();
246 }
247 }
248 return knsComments;
249}
250
251void AtticaProvider::loadedComments(Attica::BaseJob *baseJob)
252{
253 if (!jobSuccess(baseJob)) {
254 return;
255 }
256
257 auto *job = static_cast<ListJob<Attica::Comment> *>(baseJob);
258 Attica::Comment::List comments = job->itemList();
259
260 QList<std::shared_ptr<KNSCore::Comment>> receivedComments = getCommentsList(comments, nullptr);
261 Q_EMIT commentsLoaded(receivedComments);
262}
263
264void AtticaProvider::loadPerson(const QString &username)
265{
266 if (m_provider.hasPersonService()) {
267 ItemJob<Attica::Person> *job = m_provider.requestPerson(username);
268 job->setProperty("username", username);
269 connect(job, &BaseJob::finished, this, &AtticaProvider::loadedPerson);
270 job->start();
271 }
272}
273
274void AtticaProvider::loadedPerson(Attica::BaseJob *baseJob)
275{
276 if (!jobSuccess(baseJob)) {
277 return;
278 }
279
280 auto *job = static_cast<ItemJob<Attica::Person> *>(baseJob);
281 Attica::Person person = job->result();
282
283 auto author = std::make_shared<KNSCore::Author>();
284 // This is a touch hack-like, but it ensures we actually have the data in case it is not returned by the server
285 author->setId(job->property("username").toString());
286 author->setName(QStringLiteral("%1 %2").arg(person.firstName(), person.lastName()).trimmed());
287 author->setHomepage(person.homepage());
288 author->setProfilepage(person.extendedAttribute(QStringLiteral("profilepage")));
289 author->setAvatarUrl(person.avatarUrl());
290 author->setDescription(person.extendedAttribute(QStringLiteral("description")));
291 Q_EMIT personLoaded(author);
292}
293
294void AtticaProvider::loadBasics()
295{
296 Attica::ItemJob<Attica::Config> *configJob = m_provider.requestConfig();
297 connect(configJob, &BaseJob::finished, this, &AtticaProvider::loadedConfig);
298 configJob->start();
299}
300
301void AtticaProvider::loadedConfig(Attica::BaseJob *baseJob)
302{
303 if (jobSuccess(baseJob)) {
304 auto *job = static_cast<ItemJob<Attica::Config> *>(baseJob);
305 Attica::Config config = job->result();
306 setVersion(config.version());
307 setSupportsSsl(config.ssl());
308 setContactEmail(config.contact());
309 QString protocol{QStringLiteral("http")};
310 if (config.ssl()) {
311 protocol = QStringLiteral("https");
312 }
313 // There is usually no protocol in the website and host, but in case
314 // there is, trust what's there
315 if (config.website().contains(QLatin1String("://"))) {
316 setWebsite(QUrl(config.website()));
317 } else {
318 setWebsite(QUrl(QLatin1String("%1://%2").arg(protocol).arg(config.website())));
319 }
320 if (config.host().contains(QLatin1String("://"))) {
321 setHost(QUrl(config.host()));
322 } else {
323 setHost(QUrl(QLatin1String("%1://%2").arg(protocol).arg(config.host())));
324 }
325 }
326}
327
328void AtticaProvider::accountBalanceLoaded(Attica::BaseJob *baseJob)
329{
330 if (!jobSuccess(baseJob)) {
331 return;
332 }
333
334 auto *job = static_cast<ItemJob<AccountBalance> *>(baseJob);
335 AccountBalance item = job->result();
336
337 QPair<Entry, int> pair = mDownloadLinkJobs.take(job);
338 Entry entry(pair.first);
339 Content content = mCachedContent.value(entry.uniqueId());
340 if (content.downloadUrlDescription(pair.second).priceAmount() < item.balance()) {
341 qCDebug(KNEWSTUFFCORE) << "Your balance is greater than the price." << content.downloadUrlDescription(pair.second).priceAmount()
342 << " balance: " << item.balance();
343 Question question;
344 question.setEntry(entry);
345 question.setQuestion(i18nc("the price of a download item, parameter 1 is the currency, 2 is the price",
346 "This item costs %1 %2.\nDo you want to buy it?",
347 item.currency(),
348 content.downloadUrlDescription(pair.second).priceAmount()));
349 if (question.ask() == Question::YesResponse) {
350 ItemJob<DownloadItem> *job = m_provider.downloadLink(entry.uniqueId(), QString::number(pair.second));
351 connect(job, &BaseJob::finished, this, &AtticaProvider::downloadItemLoaded);
352 mDownloadLinkJobs[job] = qMakePair(entry, pair.second);
353 job->start();
354 } else {
355 return;
356 }
357 } else {
358 qCDebug(KNEWSTUFFCORE) << "You don't have enough money on your account!" << content.downloadUrlDescription(0).priceAmount()
359 << " balance: " << item.balance();
360 Q_EMIT signalInformation(i18n("Your account balance is too low:\nYour balance: %1\nPrice: %2", //
361 item.balance(),
362 content.downloadUrlDescription(0).priceAmount()));
363 }
364}
365
366void AtticaProvider::downloadItemLoaded(BaseJob *baseJob)
367{
368 if (!jobSuccess(baseJob)) {
369 return;
370 }
371
372 auto *job = static_cast<ItemJob<DownloadItem> *>(baseJob);
373 DownloadItem item = job->result();
374
375 Entry entry = mDownloadLinkJobs.take(job).first;
376 entry.setPayload(QString(item.url().toString()));
377 Q_EMIT payloadLinkLoaded(entry);
378}
379
380void AtticaProvider::vote(const Entry &entry, uint rating)
381{
382 PostJob *job = m_provider.voteForContent(entry.uniqueId(), rating);
383 connect(job, &BaseJob::finished, this, &AtticaProvider::votingFinished);
384 job->start();
385}
386
387void AtticaProvider::votingFinished(Attica::BaseJob *job)
388{
389 if (!jobSuccess(job)) {
390 return;
391 }
392 Q_EMIT signalInformation(i18nc("voting for an item (good/bad)", "Your vote was recorded."));
393}
394
395void AtticaProvider::becomeFan(const Entry &entry)
396{
397 PostJob *job = m_provider.becomeFan(entry.uniqueId());
398 connect(job, &BaseJob::finished, this, &AtticaProvider::becomeFanFinished);
399 job->start();
400}
401
402void AtticaProvider::becomeFanFinished(Attica::BaseJob *job)
403{
404 if (!jobSuccess(job)) {
405 return;
406 }
407 Q_EMIT signalInformation(i18n("You are now a fan."));
408}
409
410bool AtticaProvider::jobSuccess(Attica::BaseJob *job)
411{
412 if (job->metadata().error() == Attica::Metadata::NoError) {
413 return true;
414 }
415 qCDebug(KNEWSTUFFCORE) << "job error: " << job->metadata().error() << " status code: " << job->metadata().statusCode() << job->metadata().message();
416
417 if (job->metadata().error() == Attica::Metadata::NetworkError) {
418 if (job->metadata().statusCode() == 503) {
419 QDateTime retryAfter;
420 static const QByteArray retryAfterKey{"Retry-After"};
421 for (const QNetworkReply::RawHeaderPair &headerPair : job->metadata().headers()) {
422 if (headerPair.first == retryAfterKey) {
423 // Retry-After is not a known header, so we need to do a bit of running around to make that work
424 // Also, the fromHttpDate function is in the private qnetworkrequest header, so we can't use that
425 // So, simple workaround, just pass it through a dummy request and get a formatted date out (the
426 // cost is sufficiently low here, given we've just done a bunch of i/o heavy things, so...)
427 QNetworkRequest dummyRequest;
428 dummyRequest.setRawHeader(QByteArray{"Last-Modified"}, headerPair.second);
429 retryAfter = dummyRequest.header(QNetworkRequest::LastModifiedHeader).toDateTime();
430 break;
431 }
432 }
433 static const KFormat formatter;
434 Q_EMIT signalErrorCode(KNSCore::ErrorCode::TryAgainLaterError,
435 i18n("The service is currently undergoing maintenance and is expected to be back in %1.",
437 {retryAfter});
438 } else {
439 Q_EMIT signalErrorCode(KNSCore::ErrorCode::NetworkError,
440 i18n("Network error %1: %2", job->metadata().statusCode(), job->metadata().statusString()),
441 job->metadata().statusCode());
442 }
443 }
444 if (job->metadata().error() == Attica::Metadata::OcsError) {
445 if (job->metadata().statusCode() == 200) {
446 Q_EMIT signalErrorCode(KNSCore::ErrorCode::OcsError,
447 i18n("Too many requests to server. Please try again in a few minutes."),
448 job->metadata().statusCode());
449 } else if (job->metadata().statusCode() == 405) {
450 Q_EMIT signalErrorCode(KNSCore::ErrorCode::OcsError,
451 i18n("The Open Collaboration Services instance %1 does not support the attempted function.", name()),
452 job->metadata().statusCode());
453 } else {
454 Q_EMIT signalErrorCode(KNSCore::ErrorCode::OcsError,
455 i18n("Unknown Open Collaboration Service API error. (%1)", job->metadata().statusCode()),
456 job->metadata().statusCode());
457 }
458 }
459
460 if (auto searchRequestVar = job->property("searchRequest"); searchRequestVar.isValid()) {
461 SearchRequest req = searchRequestVar.value<SearchRequest>();
462 Q_EMIT loadingFailed(req);
463 }
464 return false;
465}
466
467} // namespace
468
469#include "moc_atticaprovider_p.cpp"
DownloadDescription downloadUrlDescription(int number) const
QString name() const
QUrl baseUrl() const
void setAdditionalAgentInformation(const QString &additionalInformation)
QUrl icon() const
QString formatSpelloutDuration(quint64 msecs) const
KNewStuff data entry container.
Definition entry.h:48
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
QString name(StandardAction 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
iterator begin()
qsizetype count() const const
iterator end()
QVariant header(KnownHeaders header) const const
void setRawHeader(const QByteArray &headerName, const QByteArray &headerValue)
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
bool isValid() const const
QDateTime toDateTime() const const
QString toString() const const
used to keep track of a search
Definition provider.h:70
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Oct 11 2024 12:12:42 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.