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

KDE's Doxygen guidelines are available online.