1 /*
2     SPDX-FileCopyrightText: 2014-2015 Eike Hein <hein@kde.org>
3     SPDX-FileCopyrightText: 2016-2017 Ivan Cukic <ivan.cukic@kde.org>
4 
5     SPDX-License-Identifier: GPL-2.0-or-later
6 */
7 
8 #include "kastatsfavoritesmodel.h"
9 #include "actionlist.h"
10 #include "appentry.h"
11 #include "contactentry.h"
12 #include "debug.h"
13 #include "fileentry.h"
14 
15 #include <QFileInfo>
16 #include <QSortFilterProxyModel>
17 #include <QTimer>
18 
19 #include <KConfigGroup>
20 #include <KLocalizedString>
21 #include <KSharedConfig>
22 
23 #include <KActivities/Consumer>
24 #include <KActivities/Stats/Query>
25 #include <KActivities/Stats/ResultSet>
26 #include <KActivities/Stats/ResultWatcher>
27 #include <KActivities/Stats/Terms>
28 
29 namespace KAStats = KActivities::Stats;
30 
31 using namespace KAStats;
32 using namespace KAStats::Terms;
33 
34 #define AGENT_APPLICATIONS QStringLiteral("org.kde.plasma.favorites.applications")
35 #define AGENT_CONTACTS QStringLiteral("org.kde.plasma.favorites.contacts")
36 #define AGENT_DOCUMENTS QStringLiteral("org.kde.plasma.favorites.documents")
37 
agentForUrl(const QString & url)38 QString agentForUrl(const QString &url)
39 {
40     // clang-format off
41     return url.startsWith(QLatin1String("ktp:"))
42                 ? AGENT_CONTACTS
43          : url.startsWith(QLatin1String("preferred:"))
44                 ? AGENT_APPLICATIONS
45          : url.startsWith(QLatin1String("applications:"))
46                 ? AGENT_APPLICATIONS
47          : (url.startsWith(QLatin1Char('/')) && !url.endsWith(QLatin1String(".desktop")))
48                 ? AGENT_DOCUMENTS
49          : (url.startsWith(QLatin1String("file:/")) && !url.endsWith(QLatin1String(".desktop")))
50                 ? AGENT_DOCUMENTS
51          // use applications as the default
52                 : AGENT_APPLICATIONS;
53     // clang-format on
54 }
55 
56 class KAStatsFavoritesModel::Private : public QAbstractListModel
57 {
58 public:
59     class NormalizedId
60     {
61     public:
NormalizedId()62         NormalizedId()
63         {
64         }
65 
NormalizedId(const Private * parent,const QString & id)66         NormalizedId(const Private *parent, const QString &id)
67         {
68             if (id.isEmpty())
69                 return;
70 
71             QSharedPointer<AbstractEntry> entry = nullptr;
72 
73             if (parent->m_itemEntries.contains(id)) {
74                 entry = parent->m_itemEntries[id];
75             } else {
76                 // This entry is not cached - it is temporary,
77                 // so let's clean up when we exit this function
78                 entry = parent->entryForResource(id);
79             }
80 
81             if (!entry || !entry->isValid()) {
82                 qWarning() << "Entry is not valid" << id << entry;
83                 m_id = id;
84                 return;
85             }
86 
87             const auto url = entry->url();
88 
89             qCDebug(KICKER_DEBUG) << "Original id is: " << id << ", and the url is" << url;
90 
91             // Preferred applications need special handling
92             if (entry->id().startsWith(QLatin1String("preferred:"))) {
93                 m_id = entry->id();
94                 return;
95             }
96 
97             // If this is an application, use the applications:-format url
98             auto appEntry = dynamic_cast<AppEntry *>(entry.data());
99             if (appEntry && !appEntry->menuId().isEmpty()) {
100                 m_id = QLatin1String("applications:") + appEntry->menuId();
101                 return;
102             }
103 
104             // We want to resolve symbolic links not to have two paths
105             // refer to the same .desktop file
106             if (url.isLocalFile()) {
107                 QFileInfo file(url.toLocalFile());
108 
109                 if (file.exists()) {
110                     m_id = QUrl::fromLocalFile(file.canonicalFilePath()).toString();
111                     return;
112                 }
113             }
114 
115             // If this is a file, we should have already covered it
116             if (url.scheme() == QLatin1String("file")) {
117                 return;
118             }
119 
120             m_id = url.toString();
121         }
122 
value() const123         const QString &value() const
124         {
125             return m_id;
126         }
127 
operator ==(const NormalizedId & other) const128         bool operator==(const NormalizedId &other) const
129         {
130             return m_id == other.m_id;
131         }
132 
133     private:
134         QString m_id;
135     };
136 
normalizedId(const QString & id) const137     NormalizedId normalizedId(const QString &id) const
138     {
139         return NormalizedId(this, id);
140     }
141 
entryForResource(const QString & resource) const142     QSharedPointer<AbstractEntry> entryForResource(const QString &resource) const
143     {
144         using SP = QSharedPointer<AbstractEntry>;
145 
146         const auto agent = agentForUrl(resource);
147 
148         if (agent == AGENT_CONTACTS) {
149             return SP(new ContactEntry(q, resource));
150 
151         } else if (agent == AGENT_DOCUMENTS) {
152             if (resource.startsWith(QLatin1String("/"))) {
153                 return SP(new FileEntry(q, QUrl::fromLocalFile(resource)));
154             } else {
155                 return SP(new FileEntry(q, QUrl(resource)));
156             }
157 
158         } else if (agent == AGENT_APPLICATIONS) {
159             if (resource.startsWith(QLatin1String("applications:"))) {
160                 return SP(new AppEntry(q, resource.mid(13)));
161             } else {
162                 return SP(new AppEntry(q, resource));
163             }
164 
165         } else {
166             return {};
167         }
168     }
169 
Private(KAStatsFavoritesModel * parent,QString clientId)170     Private(KAStatsFavoritesModel *parent, QString clientId)
171         : q(parent)
172         , m_query(LinkedResources | Agent{AGENT_APPLICATIONS, AGENT_CONTACTS, AGENT_DOCUMENTS} | Type::any() | Activity::current() | Activity::global()
173                   | Limit::all())
174         , m_watcher(m_query)
175         , m_clientId(clientId)
176     {
177         // Connecting the watcher
__anon7bd5a3db0102(const QString &resource) 178         connect(&m_watcher, &ResultWatcher::resultLinked, [this](const QString &resource) {
179             addResult(resource, -1);
180         });
181 
__anon7bd5a3db0202(const QString &resource) 182         connect(&m_watcher, &ResultWatcher::resultUnlinked, [this](const QString &resource) {
183             removeResult(resource);
184         });
185 
186         // Loading the items order
187         const auto cfg = KSharedConfig::openConfig(QStringLiteral("kactivitymanagerd-statsrc"));
188 
189         // We want first to check whether we have an ordering for this activity.
190         // If not, we will try to get a global one for this applet
191 
192         const QString thisGroupName = QStringLiteral("Favorites-") + clientId + QStringLiteral("-") + m_activities.currentActivity();
193         const QString globalGroupName = QStringLiteral("Favorites-") + clientId + QStringLiteral("-global");
194 
195         KConfigGroup thisCfgGroup(cfg, thisGroupName);
196         KConfigGroup globalCfgGroup(cfg, globalGroupName);
197 
198         QStringList ordering = thisCfgGroup.readEntry("ordering", QStringList()) + globalCfgGroup.readEntry("ordering", QStringList());
199 
200         qCDebug(KICKER_DEBUG) << "Loading the ordering " << ordering;
201 
202         // Loading the results without emitting any model signals
203         qCDebug(KICKER_DEBUG) << "Query is" << m_query;
204         ResultSet results(m_query);
205 
206         for (const auto &result : results) {
207             qCDebug(KICKER_DEBUG) << "Got " << result.resource() << " -->";
208             addResult(result.resource(), -1, false);
209         }
210 
211         // Normalizing all the ids
__anon7bd5a3db0302(const QString &item) 212         std::transform(ordering.begin(), ordering.end(), ordering.begin(), [&](const QString &item) {
213             return normalizedId(item).value();
214         });
215 
216         // Sorting the items in the cache
__anon7bd5a3db0402(const NormalizedId &left, const NormalizedId &right) 217         std::sort(m_items.begin(), m_items.end(), [&](const NormalizedId &left, const NormalizedId &right) {
218             auto leftIndex = ordering.indexOf(left.value());
219             auto rightIndex = ordering.indexOf(right.value());
220             // clang-format off
221                     return (leftIndex == -1 && rightIndex == -1) ?
222                                left.value() < right.value() :
223 
224                            (leftIndex == -1) ?
225                                false :
226 
227                            (rightIndex == -1) ?
228                                true :
229 
230                            // otherwise
231                                leftIndex < rightIndex;
232             // clang-format on
233         });
234 
235         // Debugging:
236         QVector<QString> itemStrings(m_items.size());
__anon7bd5a3db0502(const NormalizedId &item) 237         std::transform(m_items.cbegin(), m_items.cend(), itemStrings.begin(), [](const NormalizedId &item) {
238             return item.value();
239         });
240         qCDebug(KICKER_DEBUG) << "After ordering: " << itemStrings;
241     }
242 
addResult(const QString & _resource,int index,bool notifyModel=true)243     void addResult(const QString &_resource, int index, bool notifyModel = true)
244     {
245         // We want even files to have a proper URL
246         const auto resource = _resource.startsWith(QLatin1Char('/')) ? QUrl::fromLocalFile(_resource).toString() : _resource;
247 
248         qCDebug(KICKER_DEBUG) << "Adding result" << resource << "already present?" << m_itemEntries.contains(resource);
249 
250         if (m_itemEntries.contains(resource))
251             return;
252 
253         auto entry = entryForResource(resource);
254 
255         if (!entry || !entry->isValid()) {
256             qCDebug(KICKER_DEBUG) << "Entry is not valid!";
257             return;
258         }
259 
260         if (index == -1) {
261             index = m_items.count();
262         }
263 
264         if (notifyModel) {
265             beginInsertRows(QModelIndex(), index, index);
266         }
267 
268         auto url = entry->url();
269 
270         m_itemEntries[resource] = m_itemEntries[entry->id()] = m_itemEntries[url.toString()] = m_itemEntries[url.toLocalFile()] = entry;
271 
272         auto normalized = normalizedId(resource);
273         m_items.insert(index, normalized);
274         m_itemEntries[normalized.value()] = entry;
275 
276         if (notifyModel) {
277             endInsertRows();
278             saveOrdering();
279         }
280     }
281 
removeResult(const QString & resource)282     void removeResult(const QString &resource)
283     {
284         auto normalized = normalizedId(resource);
285 
286         // If we know this item will not really be removed,
287         // but only that activities it is on have changed,
288         // lets leave it
289         if (m_ignoredItems.contains(normalized.value())) {
290             m_ignoredItems.removeAll(normalized.value());
291             return;
292         }
293 
294         qCDebug(KICKER_DEBUG) << "Removing result" << resource;
295 
296         auto index = m_items.indexOf(normalizedId(resource));
297 
298         if (index == -1)
299             return;
300 
301         beginRemoveRows(QModelIndex(), index, index);
302         auto entry = m_itemEntries[resource];
303         m_items.removeAt(index);
304 
305         // Removing the entry from the cache
306         QMutableHashIterator<QString, QSharedPointer<AbstractEntry>> i(m_itemEntries);
307         while (i.hasNext()) {
308             i.next();
309             if (i.value() == entry) {
310                 i.remove();
311             }
312         }
313 
314         endRemoveRows();
315     }
316 
rowCount(const QModelIndex & parent=QModelIndex ()) const317     int rowCount(const QModelIndex &parent = QModelIndex()) const override
318     {
319         if (parent.isValid())
320             return 0;
321 
322         return m_items.count();
323     }
324 
data(const QModelIndex & item,int role=Qt::DisplayRole) const325     QVariant data(const QModelIndex &item, int role = Qt::DisplayRole) const override
326     {
327         if (item.parent().isValid())
328             return QVariant();
329 
330         const auto index = item.row();
331 
332         const auto entry = m_itemEntries[m_items[index].value()];
333         // clang-format off
334         return entry == nullptr ? QVariant()
335              : role == Qt::DisplayRole ? entry->name()
336              : role == Qt::DecorationRole ? entry->icon()
337              : role == Kicker::DescriptionRole ? entry->description()
338              : role == Kicker::FavoriteIdRole ? entry->id()
339              : role == Kicker::UrlRole ? entry->url()
340              : role == Kicker::HasActionListRole ? entry->hasActions()
341              : role == Kicker::ActionListRole ? entry->actions()
342              : QVariant();
343         // clang-format on
344     }
345 
trigger(int row,const QString & actionId,const QVariant & argument)346     bool trigger(int row, const QString &actionId, const QVariant &argument)
347     {
348         if (row < 0 || row >= rowCount()) {
349             return false;
350         }
351 
352         const QString id = data(index(row, 0), Kicker::UrlRole).toString();
353         if (m_itemEntries.contains(id)) {
354             return m_itemEntries[id]->run(actionId, argument);
355         }
356         // Entries with preferred:// can be changed by the user, BUG: 416161
357         // then the list of entries could be out of sync
358         const auto entry = m_itemEntries[m_items[row].value()];
359         if (QUrl(entry->id()).scheme() == QLatin1String("preferred")) {
360             return entry->run(actionId, argument);
361         }
362         return false;
363     }
364 
move(int from,int to)365     void move(int from, int to)
366     {
367         if (from < 0)
368             return;
369         if (from >= m_items.count())
370             return;
371         if (to < 0)
372             return;
373         if (to >= m_items.count())
374             return;
375 
376         if (from == to)
377             return;
378 
379         const int modelTo = to + (to > from ? 1 : 0);
380 
381         if (q->beginMoveRows(QModelIndex(), from, from, QModelIndex(), modelTo)) {
382             m_items.move(from, to);
383             q->endMoveRows();
384 
385             qCDebug(KICKER_DEBUG) << "Save ordering (from Private::move) -->";
386             saveOrdering();
387         }
388     }
389 
saveOrdering()390     void saveOrdering()
391     {
392         QStringList ids;
393 
394         for (const auto &item : qAsConst(m_items)) {
395             ids << item.value();
396         }
397 
398         qCDebug(KICKER_DEBUG) << "Save ordering (from Private::saveOrdering) -->";
399         saveOrdering(ids, m_clientId, m_activities.currentActivity());
400     }
401 
saveOrdering(const QStringList & ids,const QString & clientId,const QString & currentActivity)402     static void saveOrdering(const QStringList &ids, const QString &clientId, const QString &currentActivity)
403     {
404         const auto cfg = KSharedConfig::openConfig(QStringLiteral("kactivitymanagerd-statsrc"));
405 
406         QStringList activities{currentActivity, QStringLiteral("global")};
407 
408         qCDebug(KICKER_DEBUG) << "Saving ordering for" << currentActivity << "and global" << ids;
409 
410         for (const auto &activity : activities) {
411             const QString groupName = QStringLiteral("Favorites-") + clientId + QStringLiteral("-") + activity;
412 
413             KConfigGroup cfgGroup(cfg, groupName);
414 
415             cfgGroup.writeEntry("ordering", ids);
416         }
417 
418         cfg->sync();
419     }
420 
421     KAStatsFavoritesModel *const q;
422     KActivities::Consumer m_activities;
423     Query m_query;
424     ResultWatcher m_watcher;
425     QString m_clientId;
426 
427     QVector<NormalizedId> m_items;
428     QHash<QString, QSharedPointer<AbstractEntry>> m_itemEntries;
429     QStringList m_ignoredItems;
430 };
431 
KAStatsFavoritesModel(QObject * parent)432 KAStatsFavoritesModel::KAStatsFavoritesModel(QObject *parent)
433     : PlaceholderModel(parent)
434     , d(nullptr) // we have no client id yet
435     , m_enabled(true)
436     , m_maxFavorites(-1)
437     , m_activities(new KActivities::Consumer(this))
438 {
439     connect(m_activities, &KActivities::Consumer::currentActivityChanged, this, [&](const QString &currentActivity) {
440         qCDebug(KICKER_DEBUG) << "Activity just got changed to" << currentActivity;
441         Q_UNUSED(currentActivity);
442         if (d) {
443             auto clientId = d->m_clientId;
444             initForClient(clientId);
445         }
446     });
447 }
448 
~KAStatsFavoritesModel()449 KAStatsFavoritesModel::~KAStatsFavoritesModel()
450 {
451     delete d;
452 }
453 
initForClient(const QString & clientId)454 void KAStatsFavoritesModel::initForClient(const QString &clientId)
455 {
456     qCDebug(KICKER_DEBUG) << "initForClient" << clientId;
457 
458     setSourceModel(nullptr);
459     delete d;
460     d = new Private(this, clientId);
461 
462     setSourceModel(d);
463 }
464 
description() const465 QString KAStatsFavoritesModel::description() const
466 {
467     return i18n("Favorites");
468 }
469 
trigger(int row,const QString & actionId,const QVariant & argument)470 bool KAStatsFavoritesModel::trigger(int row, const QString &actionId, const QVariant &argument)
471 {
472     return d && d->trigger(row, actionId, argument);
473 }
474 
enabled() const475 bool KAStatsFavoritesModel::enabled() const
476 {
477     return m_enabled;
478 }
479 
maxFavorites() const480 int KAStatsFavoritesModel::maxFavorites() const
481 {
482     return m_maxFavorites;
483 }
484 
setMaxFavorites(int max)485 void KAStatsFavoritesModel::setMaxFavorites(int max)
486 {
487     Q_UNUSED(max);
488 }
489 
setEnabled(bool enable)490 void KAStatsFavoritesModel::setEnabled(bool enable)
491 {
492     if (m_enabled != enable) {
493         m_enabled = enable;
494 
495         emit enabledChanged();
496     }
497 }
498 
favorites() const499 QStringList KAStatsFavoritesModel::favorites() const
500 {
501     qWarning() << "KAStatsFavoritesModel::favorites returns nothing, it is here just to keep the API backwards-compatible";
502     return QStringList();
503 }
504 
setFavorites(const QStringList & favorites)505 void KAStatsFavoritesModel::setFavorites(const QStringList &favorites)
506 {
507     Q_UNUSED(favorites);
508     qWarning() << "KAStatsFavoritesModel::setFavorites is ignored";
509 }
510 
isFavorite(const QString & id) const511 bool KAStatsFavoritesModel::isFavorite(const QString &id) const
512 {
513     return d && d->m_itemEntries.contains(id);
514 }
515 
portOldFavorites(const QStringList & ids)516 void KAStatsFavoritesModel::portOldFavorites(const QStringList &ids)
517 {
518     if (!d)
519         return;
520     qCDebug(KICKER_DEBUG) << "portOldFavorites" << ids;
521 
522     const QString activityId = QStringLiteral(":global");
523     std::for_each(ids.begin(), ids.end(), [&](const QString &id) {
524         addFavoriteTo(id, activityId);
525     });
526 
527     // Resetting the model
528     auto clientId = d->m_clientId;
529     setSourceModel(nullptr);
530     delete d;
531     d = nullptr;
532 
533     qCDebug(KICKER_DEBUG) << "Save ordering (from portOldFavorites) -->";
534     Private::saveOrdering(ids, clientId, m_activities->currentActivity());
535 
536     QTimer::singleShot(500, this, std::bind(&KAStatsFavoritesModel::initForClient, this, clientId));
537 }
538 
addFavorite(const QString & id,int index)539 void KAStatsFavoritesModel::addFavorite(const QString &id, int index)
540 {
541     qCDebug(KICKER_DEBUG) << "addFavorite" << id << index << " -->";
542     addFavoriteTo(id, QStringLiteral(":global"), index);
543 }
544 
removeFavorite(const QString & id)545 void KAStatsFavoritesModel::removeFavorite(const QString &id)
546 {
547     qCDebug(KICKER_DEBUG) << "removeFavorite" << id << " -->";
548     removeFavoriteFrom(id, QStringLiteral(":any"));
549 }
550 
addFavoriteTo(const QString & id,const QString & activityId,int index)551 void KAStatsFavoritesModel::addFavoriteTo(const QString &id, const QString &activityId, int index)
552 {
553     qCDebug(KICKER_DEBUG) << "addFavoriteTo" << id << activityId << index << " -->";
554     addFavoriteTo(id, Activity(activityId), index);
555 }
556 
removeFavoriteFrom(const QString & id,const QString & activityId)557 void KAStatsFavoritesModel::removeFavoriteFrom(const QString &id, const QString &activityId)
558 {
559     qCDebug(KICKER_DEBUG) << "removeFavoriteFrom" << id << activityId << " -->";
560     removeFavoriteFrom(id, Activity(activityId));
561 }
562 
addFavoriteTo(const QString & id,const Activity & activity,int index)563 void KAStatsFavoritesModel::addFavoriteTo(const QString &id, const Activity &activity, int index)
564 {
565     if (!d || id.isEmpty())
566         return;
567 
568     Q_ASSERT(!activity.values.isEmpty());
569 
570     setDropPlaceholderIndex(-1);
571 
572     QStringList matchers{d->m_activities.currentActivity(), QStringLiteral(":global"), QStringLiteral(":current")};
573     if (std::find_first_of(activity.values.cbegin(), activity.values.cend(), matchers.cbegin(), matchers.cend()) != activity.values.cend()) {
574         d->addResult(id, index);
575     }
576 
577     const auto url = d->normalizedId(id).value();
578 
579     qCDebug(KICKER_DEBUG) << "addFavoriteTo" << id << activity << index << url << " (actual)";
580 
581     if (url.isEmpty())
582         return;
583 
584     d->m_watcher.linkToActivity(QUrl(url), activity, Agent(agentForUrl(url)));
585 }
586 
removeFavoriteFrom(const QString & id,const Activity & activity)587 void KAStatsFavoritesModel::removeFavoriteFrom(const QString &id, const Activity &activity)
588 {
589     if (!d || id.isEmpty())
590         return;
591 
592     const auto url = d->normalizedId(id).value();
593 
594     Q_ASSERT(!activity.values.isEmpty());
595 
596     qCDebug(KICKER_DEBUG) << "addFavoriteTo" << id << activity << url << " (actual)";
597 
598     if (url.isEmpty())
599         return;
600 
601     d->m_watcher.unlinkFromActivity(QUrl(url), activity, Agent(agentForUrl(url)));
602 }
603 
setFavoriteOn(const QString & id,const QString & activityId)604 void KAStatsFavoritesModel::setFavoriteOn(const QString &id, const QString &activityId)
605 {
606     if (!d || id.isEmpty())
607         return;
608 
609     const auto url = d->normalizedId(id).value();
610 
611     qCDebug(KICKER_DEBUG) << "setFavoriteOn" << id << activityId << url << " (actual)";
612 
613     qCDebug(KICKER_DEBUG) << "%%%%%%%%%%% Activity is" << activityId;
614     if (activityId.isEmpty() || activityId == QLatin1String(":any") || activityId == QLatin1String(":global")
615         || activityId == m_activities->currentActivity()) {
616         d->m_ignoredItems << url;
617     }
618 
619     d->m_watcher.unlinkFromActivity(QUrl(url), Activity::any(), Agent(agentForUrl(url)));
620     d->m_watcher.linkToActivity(QUrl(url), activityId, Agent(agentForUrl(url)));
621 }
622 
moveRow(int from,int to)623 void KAStatsFavoritesModel::moveRow(int from, int to)
624 {
625     if (!d)
626         return;
627 
628     d->move(from, to);
629 }
630 
favoritesModel()631 AbstractModel *KAStatsFavoritesModel::favoritesModel()
632 {
633     return this;
634 }
635 
refresh()636 void KAStatsFavoritesModel::refresh()
637 {
638 }
639 
activities() const640 QObject *KAStatsFavoritesModel::activities() const
641 {
642     return m_activities;
643 }
644 
activityNameForId(const QString & activityId) const645 QString KAStatsFavoritesModel::activityNameForId(const QString &activityId) const
646 {
647     // It is safe to use a short-lived object here,
648     // we are always synced with KAMD in plasma
649     KActivities::Info info(activityId);
650     return info.name();
651 }
652 
linkedActivitiesFor(const QString & id) const653 QStringList KAStatsFavoritesModel::linkedActivitiesFor(const QString &id) const
654 {
655     if (!d) {
656         qCDebug(KICKER_DEBUG) << "Linked for" << id << "is empty, no Private instance";
657         return {};
658     }
659 
660     auto url = d->normalizedId(id).value();
661 
662     if (url.startsWith(QLatin1String("file:"))) {
663         url = QUrl(url).toLocalFile();
664     }
665 
666     if (url.isEmpty()) {
667         qCDebug(KICKER_DEBUG) << "The url for" << id << "is empty";
668         return {};
669     }
670 
671     auto query = LinkedResources | Agent{AGENT_APPLICATIONS, AGENT_CONTACTS, AGENT_DOCUMENTS} | Type::any() | Activity::any() | Url(url) | Limit::all();
672 
673     ResultSet results(query);
674 
675     for (const auto &result : results) {
676         qCDebug(KICKER_DEBUG) << "Returning" << result.linkedActivities() << "for" << id << url;
677         return result.linkedActivities();
678     }
679 
680     qCDebug(KICKER_DEBUG) << "Returning empty list of activities for" << id << url;
681     return {};
682 }
683