birthdaysresource.cpp 13.9 KB
Newer Older
1
/*
2
3
    SPDX-FileCopyrightText: 2003 Cornelius Schumacher <schumacher@kde.org>
    SPDX-FileCopyrightText: 2009 Volker Krause <vkrause@kde.org>
4

5
    SPDX-License-Identifier: LGPL-2.0-or-later
6
7
8
9
10
11
*/

#include "birthdaysresource.h"
#include "settings.h"
#include "settingsadaptor.h"

12
#include <Akonadi/CollectionFetchJob>
13
#include <Akonadi/EntityDisplayAttribute>
14
#include <Akonadi/ItemFetchJob>
15
#include <Akonadi/ItemFetchScope>
16
#include <Akonadi/MimeTypeChecker>
17
18
#include <Akonadi/Monitor>
#include <Akonadi/VectorHelper>
19

20
#include <KContacts/Addressee>
21

22
#include <KEmailAddress>
23

Laurent Montel's avatar
Laurent Montel committed
24
#include "birthdays_debug.h"
Laurent Montel's avatar
Laurent Montel committed
25
#include <KLocalizedString>
26

27
#include <Akonadi/TagCreateJob>
28

29
using namespace Akonadi;
Aleix Pol Gonzalez's avatar
Aleix Pol Gonzalez committed
30
using namespace KContacts;
31
using namespace KCalendarCore;
32

Laurent Montel's avatar
Laurent Montel committed
33
34
BirthdaysResource::BirthdaysResource(const QString &id)
    : ResourceBase(id)
35
{
Laurent Montel's avatar
Laurent Montel committed
36
    Settings::instance(KSharedConfig::openConfig());
Laurent Montel's avatar
Laurent Montel committed
37
    new SettingsAdaptor(Settings::self());
Laurent Montel's avatar
Laurent Montel committed
38
    QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"), Settings::self(), QDBusConnection::ExportAdaptors);
39

Laurent Montel's avatar
Laurent Montel committed
40
    setName(i18n("Birthdays & Anniversaries"));
Laurent Montel's avatar
Laurent Montel committed
41
    auto monitor = new Monitor(this);
Laurent Montel's avatar
Laurent Montel committed
42
43
44
45
46
    monitor->setMimeTypeMonitored(Addressee::mimeType());
    monitor->itemFetchScope().fetchFullPayload();
    connect(monitor, &Monitor::itemAdded, this, &BirthdaysResource::contactChanged);
    connect(monitor, &Monitor::itemChanged, this, &BirthdaysResource::contactChanged);
    connect(monitor, &Monitor::itemRemoved, this, &BirthdaysResource::contactRemoved);
Volker Krause's avatar
Volker Krause committed
47

Laurent Montel's avatar
Laurent Montel committed
48
    connect(this, &BirthdaysResource::reloadConfiguration, this, &BirthdaysResource::slotReloadConfig);
49
50
51
52
53
}

BirthdaysResource::~BirthdaysResource()
{
}
Laurent Montel's avatar
Laurent Montel committed
54
55

void BirthdaysResource::slotReloadConfig()
56
{
Laurent Montel's avatar
Laurent Montel committed
57
58
    doFullSearch();
    synchronizeCollectionTree();
59
60
61
62
}

void BirthdaysResource::retrieveCollections()
{
Laurent Montel's avatar
Laurent Montel committed
63
64
    Collection c;
    c.setParentCollection(Collection::root());
Laurent Montel's avatar
Laurent Montel committed
65
    c.setRemoteId(QStringLiteral("akonadi_birthdays_resource"));
Laurent Montel's avatar
Laurent Montel committed
66
    c.setName(name());
Laurent Montel's avatar
Laurent Montel committed
67
    c.setContentMimeTypes(QStringList() << QStringLiteral("application/x-vnd.akonadi.calendar.event"));
Laurent Montel's avatar
Laurent Montel committed
68
69
    c.setRights(Collection::ReadOnly);

Laurent Montel's avatar
Laurent Montel committed
70
    auto attribute = c.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
Laurent Montel's avatar
Laurent Montel committed
71
    attribute->setIconName(QStringLiteral("view-calendar-birthday"));
Laurent Montel's avatar
Laurent Montel committed
72
73
74
75

    Collection::List list;
    list << c;
    collectionsRetrieved(list);
76
77
}

Laurent Montel's avatar
Laurent Montel committed
78
void BirthdaysResource::retrieveItems(const Akonadi::Collection &collection)
79
{
Laurent Montel's avatar
Laurent Montel committed
80
    Q_UNUSED(collection)
81
    itemsRetrievedIncremental(Akonadi::valuesToVector(mPendingItems), Akonadi::valuesToVector(mDeletedItems));
Laurent Montel's avatar
Laurent Montel committed
82
83
    mPendingItems.clear();
    mDeletedItems.clear();
84
85
}

86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
bool BirthdaysResource::retrieveItems(const Akonadi::Item::List &items, const QSet<QByteArray> &parts)
{
    Q_UNUSED(parts)

    // collect contacts, same contact might be used multiple times, for birthday & anniversary
    QSet<Akonadi::Item::Id> contactIds;
    for (auto &item : items) {
        const Akonadi::Item::Id contactId = item.remoteId().mid(1).toLongLong();
        contactIds << contactId;
    }

    // query contacts
    Akonadi::Item::List contactItems;
    contactItems.reserve(contactIds.size());
    for (Akonadi::Item::Id contactId : std::as_const(contactIds)) {
        contactItems.append(Item(contactId));
    }
    auto job = new ItemFetchJob(contactItems, this);
    job->fetchScope().fetchFullPayload();
    connect(job, &ItemFetchJob::result, this, &BirthdaysResource::contactsRetrieved);

    return true;
}

Laurent Montel's avatar
Laurent Montel committed
110
bool BirthdaysResource::retrieveItem(const Akonadi::Item &item, const QSet<QByteArray> &parts)
111
{
Laurent Montel's avatar
Laurent Montel committed
112
    Q_UNUSED(parts)
Laurent Montel's avatar
Laurent Montel committed
113
    qint64 contactId = item.remoteId().mid(1).toLongLong();
Laurent Montel's avatar
Laurent Montel committed
114
    auto job = new ItemFetchJob(Item(contactId), this);
Laurent Montel's avatar
Laurent Montel committed
115
116
117
    job->fetchScope().fetchFullPayload();
    connect(job, &ItemFetchJob::result, this, &BirthdaysResource::contactRetrieved);
    return true;
118
119
}

Laurent Montel's avatar
Laurent Montel committed
120
void BirthdaysResource::contactRetrieved(KJob *job)
121
{
Laurent Montel's avatar
Laurent Montel committed
122
    auto fj = static_cast<ItemFetchJob *>(job);
Laurent Montel's avatar
Laurent Montel committed
123
    if (job->error()) {
Laurent Montel's avatar
Laurent Montel committed
124
        Q_EMIT error(job->errorText());
Laurent Montel's avatar
Laurent Montel committed
125
126
127
        cancelTask();
    } else if (fj->items().count() != 1) {
        cancelTask();
128
    } else {
129
130
131
132
133
134
        const auto contactItem = fj->items().at(0);
        if (!contactItem.hasPayload<KContacts::Addressee>()) {
            cancelTask();
            return;
        }
        auto contact = contactItem.payload<KContacts::Addressee>();
135
        KCalendarCore::Incidence::Ptr ev;
Laurent Montel's avatar
Laurent Montel committed
136
        if (currentItems().at(0).remoteId().startsWith(QLatin1Char('b'))) {
137
            ev = createBirthday(contact, contactItem.id());
Laurent Montel's avatar
Laurent Montel committed
138
        } else if (currentItems().at(0).remoteId().startsWith(QLatin1Char('a'))) {
139
            ev = createAnniversary(contact, contactItem.id());
Laurent Montel's avatar
Laurent Montel committed
140
141
142
143
        }
        if (!ev) {
            cancelTask();
        } else {
Laurent Montel's avatar
Laurent Montel committed
144
            Item i(currentItems().at(0));
Laurent Montel's avatar
Laurent Montel committed
145
146
147
            i.setPayload<Incidence::Ptr>(ev);
            itemRetrieved(i);
        }
148
149
150
    }
}

151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
void BirthdaysResource::contactsRetrieved(KJob *job)
{
    if (job->error()) {
        Q_EMIT error(job->errorText());
        cancelTask();
        return;
    }

    // prepare contacts look-up table
    auto fetchJob = static_cast<ItemFetchJob *>(job);
    const auto contactItems = fetchJob->items();

    QHash<Akonadi::Item::Id, KContacts::Addressee> contacts;
    contacts.reserve(contactItems.size());
    for (auto &contactItem : contactItems) {
        if (!contactItem.hasPayload<KContacts::Addressee>()) {
            cancelTask();
            return;
        }
        auto contact = contactItem.payload<KContacts::Addressee>();
        contacts.insert(contactItem.id(), contact);
    }

    // for all queried items now generate payload from the available contacts
    const Akonadi::Item::List queriedItems = currentItems();

    Akonadi::Item::List resultItems;
    resultItems.reserve(queriedItems.size());

    for (auto &item : queriedItems) {
        const QString remoteId = item.remoteId();
        const Akonadi::Item::Id contactId = remoteId.mid(1).toLongLong();
        auto it = contacts.constFind(contactId);
        if (it == contacts.constEnd()) {
            cancelTask();
            return;
        }
        auto &contact = *it;

        KCalendarCore::Incidence::Ptr ev;
        if (remoteId.startsWith(QLatin1Char('b'))) {
            ev = createBirthday(contact, contactId);
        } else if (remoteId.startsWith(QLatin1Char('a'))) {
            ev = createAnniversary(contact, contactId);
        }
        if (!ev) {
            cancelTask();
            return;
        }

        Item i(item);
        i.setPayload<Incidence::Ptr>(ev);
        resultItems.append(i);
    }

    itemsRetrieved(resultItems);
}

Laurent Montel's avatar
Laurent Montel committed
209
void BirthdaysResource::contactChanged(const Akonadi::Item &item)
210
{
Laurent Montel's avatar
Laurent Montel committed
211
212
    if (!item.hasPayload<KContacts::Addressee>()) {
        return;
213
214
    }

Laurent Montel's avatar
Laurent Montel committed
215
    auto contact = item.payload<KContacts::Addressee>();
Laurent Montel's avatar
Laurent Montel committed
216
217
218
219

    if (Settings::self()->filterOnCategories()) {
        bool hasCategory = false;
        const QStringList categories = contact.categories();
Laurent Montel's avatar
Laurent Montel committed
220
221
        const QStringList lst = Settings::self()->filterCategories();
        for (const QString &cat : lst) {
Laurent Montel's avatar
Laurent Montel committed
222
223
224
225
226
227
228
229
230
231
            if (categories.contains(cat)) {
                hasCategory = true;
                break;
            }
        }

        if (!hasCategory) {
            return;
        }
    }
232

233
234
    const Akonadi::Item::Id itemId = item.id();
    Event::Ptr event = createBirthday(contact, itemId);
Laurent Montel's avatar
Laurent Montel committed
235
    if (event) {
236
        addPendingEvent(event, QStringLiteral("b%1").arg(itemId));
237
    } else {
238
        Item i(KCalendarCore::Event::eventMimeType());
239
        i.setRemoteId(QStringLiteral("b%1").arg(itemId));
Laurent Montel's avatar
Laurent Montel committed
240
        mDeletedItems[i.remoteId()] = i;
Laurent Montel's avatar
Laurent Montel committed
241
    }
242

243
    event = createAnniversary(contact, itemId);
Laurent Montel's avatar
Laurent Montel committed
244
    if (event) {
245
        addPendingEvent(event, QStringLiteral("a%1").arg(itemId));
246
    } else {
247
        Item i(KCalendarCore::Event::eventMimeType());
248
        i.setRemoteId(QStringLiteral("a%1").arg(itemId));
Laurent Montel's avatar
Laurent Montel committed
249
        mDeletedItems[i.remoteId()] = i;
Laurent Montel's avatar
Laurent Montel committed
250
    }
251
    synchronize();
252
253
}

254
void BirthdaysResource::addPendingEvent(const KCalendarCore::Event::Ptr &event, const QString &remoteId)
255
{
256
257
    KCalendarCore::Incidence::Ptr evptr(event);
    Item i(KCalendarCore::Event::eventMimeType());
Laurent Montel's avatar
Laurent Montel committed
258
259
    i.setRemoteId(remoteId);
    i.setPayload(evptr);
Laurent Montel's avatar
Laurent Montel committed
260
    mPendingItems[remoteId] = i;
261
262
}

Laurent Montel's avatar
Laurent Montel committed
263
void BirthdaysResource::contactRemoved(const Akonadi::Item &item)
264
{
265
    Item i(KCalendarCore::Event::eventMimeType());
Laurent Montel's avatar
Laurent Montel committed
266
    i.setRemoteId(QStringLiteral("b%1").arg(item.id()));
Laurent Montel's avatar
Laurent Montel committed
267
    mDeletedItems[i.remoteId()] = i;
Laurent Montel's avatar
Laurent Montel committed
268
    i.setRemoteId(QStringLiteral("a%1").arg(item.id()));
Laurent Montel's avatar
Laurent Montel committed
269
    mDeletedItems[i.remoteId()] = i;
Laurent Montel's avatar
Laurent Montel committed
270
    synchronize();
271
272
273
274
}

void BirthdaysResource::doFullSearch()
{
Laurent Montel's avatar
Laurent Montel committed
275
    auto job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, this);
Laurent Montel's avatar
Laurent Montel committed
276
    connect(job, &CollectionFetchJob::collectionsReceived, this, &BirthdaysResource::listContacts);
277
278
279
280
}

void BirthdaysResource::listContacts(const Akonadi::Collection::List &cols)
{
Laurent Montel's avatar
Laurent Montel committed
281
282
    MimeTypeChecker contactFilter;
    contactFilter.addWantedMimeType(Addressee::mimeType());
Laurent Montel's avatar
Laurent Montel committed
283
    for (const Collection &col : cols) {
Laurent Montel's avatar
Laurent Montel committed
284
285
286
        if (!contactFilter.isWantedCollection(col)) {
            continue;
        }
Laurent Montel's avatar
Laurent Montel committed
287
        auto job = new ItemFetchJob(col, this);
Laurent Montel's avatar
Laurent Montel committed
288
289
290
        job->fetchScope().fetchFullPayload();
        connect(job, &ItemFetchJob::itemsReceived, this, &BirthdaysResource::createEvents);
    }
291
292
293
294
}

void BirthdaysResource::createEvents(const Akonadi::Item::List &items)
{
Laurent Montel's avatar
Laurent Montel committed
295
    for (const Item &item : items) {
Laurent Montel's avatar
Laurent Montel committed
296
297
        contactChanged(item);
    }
298
299
}

300
KCalendarCore::Event::Ptr BirthdaysResource::createBirthday(const KContacts::Addressee &contact, Akonadi::Item::Id itemId)
301
{
Laurent Montel's avatar
Laurent Montel committed
302
303
    const QString name = contact.realName().isEmpty() ? contact.nickName() : contact.realName();
    if (name.isEmpty()) {
304
        qCDebug(BIRTHDAYS_LOG) << "contact " << contact.uid() << itemId << " has no name, skipping.";
305
        return KCalendarCore::Event::Ptr();
Laurent Montel's avatar
Laurent Montel committed
306
    }
307

Laurent Montel's avatar
Laurent Montel committed
308
309
310
    const QDate birthdate = contact.birthday().date();
    if (birthdate.isValid()) {
        const QString summary = i18n("%1's birthday", name);
311

Laurent Montel's avatar
Laurent Montel committed
312
        Event::Ptr ev = createEvent(birthdate);
Laurent Montel's avatar
Laurent Montel committed
313
        ev->setUid(contact.uid() + QStringLiteral("_KABC_Birthday"));
314

Laurent Montel's avatar
Laurent Montel committed
315
        ev->setCustomProperty("KABC", "BIRTHDAY", QStringLiteral("YES"));
Laurent Montel's avatar
Laurent Montel committed
316
317
        ev->setCustomProperty("KABC", "UID-1", contact.uid());
        ev->setCustomProperty("KABC", "NAME-1", name);
318
        ev->setCustomProperty("KABC", "EMAIL-1", contact.preferredEmail());
Laurent Montel's avatar
Laurent Montel committed
319
        ev->setSummary(summary);
320

Laurent Montel's avatar
Laurent Montel committed
321
        checkForUnknownCategories(i18n("Birthday"), ev);
Laurent Montel's avatar
Laurent Montel committed
322
323
        return ev;
    }
324
    return KCalendarCore::Event::Ptr();
325
326
}

327
KCalendarCore::Event::Ptr BirthdaysResource::createAnniversary(const KContacts::Addressee &contact, Akonadi::Item::Id itemId)
328
{
Laurent Montel's avatar
Laurent Montel committed
329
330
    const QString name = contact.realName().isEmpty() ? contact.nickName() : contact.realName();
    if (name.isEmpty()) {
331
        qCDebug(BIRTHDAYS_LOG) << "contact " << contact.uid() << itemId << " has no name, skipping.";
332
        return KCalendarCore::Event::Ptr();
Laurent Montel's avatar
Laurent Montel committed
333
    }
334

Laurent Montel's avatar
Laurent Montel committed
335
    const QString anniversary_string = contact.custom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-Anniversary"));
Laurent Montel's avatar
Laurent Montel committed
336
    if (anniversary_string.isEmpty()) {
337
        return KCalendarCore::Event::Ptr();
Laurent Montel's avatar
Laurent Montel committed
338
339
340
    }
    const QDate anniversary = QDate::fromString(anniversary_string, Qt::ISODate);
    if (anniversary.isValid()) {
Laurent Montel's avatar
Laurent Montel committed
341
        const QString spouseName = contact.custom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-SpousesName"));
Laurent Montel's avatar
Laurent Montel committed
342
343
344
345
346
347
348
349

        QString summary;
        if (!spouseName.isEmpty()) {
            QString tname, temail;
            KEmailAddress::extractEmailAddressAndName(spouseName, temail, tname);
            tname = KEmailAddress::quoteNameIfNecessary(tname);
            if ((tname[0] == QLatin1Char('"')) && (tname[tname.length() - 1] == QLatin1Char('"'))) {
                tname.remove(0, 1);
350
                tname.chop(1);
Laurent Montel's avatar
Laurent Montel committed
351
            }
Laurent Montel's avatar
Laurent Montel committed
352
            tname.remove(QLatin1Char('\\')); // remove escape chars
Laurent Montel's avatar
Laurent Montel committed
353
354
355
356
357
358
            KContacts::Addressee spouse;
            spouse.setNameFromString(tname);
            QString name_2 = spouse.nickName();
            if (name_2.isEmpty()) {
                name_2 = spouse.realName();
            }
Laurent Montel's avatar
Laurent Montel committed
359
            summary = i18nc("insert names of both spouses", "%1's & %2's anniversary", name, name_2);
Laurent Montel's avatar
Laurent Montel committed
360
        } else {
Laurent Montel's avatar
Laurent Montel committed
361
            summary = i18nc("only one spouse in addressbook, insert the name", "%1's anniversary", name);
Laurent Montel's avatar
Laurent Montel committed
362
363
364
        }

        Event::Ptr event = createEvent(anniversary);
Laurent Montel's avatar
Laurent Montel committed
365
        event->setUid(contact.uid() + QStringLiteral("_KABC_Anniversary"));
Laurent Montel's avatar
Laurent Montel committed
366
367
368
369
370
        event->setSummary(summary);

        event->setCustomProperty("KABC", "UID-1", contact.uid());
        event->setCustomProperty("KABC", "NAME-1", name);
        event->setCustomProperty("KABC", "EMAIL-1", contact.fullEmail());
Laurent Montel's avatar
Laurent Montel committed
371
        event->setCustomProperty("KABC", "ANNIVERSARY", QStringLiteral("YES"));
Laurent Montel's avatar
Laurent Montel committed
372
        // insert category
Laurent Montel's avatar
Laurent Montel committed
373
        checkForUnknownCategories(i18n("Anniversary"), event);
Laurent Montel's avatar
Laurent Montel committed
374
375
        return event;
    }
376
    return KCalendarCore::Event::Ptr();
Laurent Montel's avatar
Laurent Montel committed
377
378
}

Laurent Montel's avatar
Laurent Montel committed
379
KCalendarCore::Event::Ptr BirthdaysResource::createEvent(QDate date)
Laurent Montel's avatar
Laurent Montel committed
380
381
{
    Event::Ptr event(new Event());
Daniel Vrátil's avatar
Daniel Vrátil committed
382
383
    event->setDtStart(QDateTime(date, {}));
    event->setDtEnd(QDateTime(date, {}));
Laurent Montel's avatar
Laurent Montel committed
384
385
386
387
388
    event->setAllDay(true);
    event->setTransparency(Event::Transparent);

    // Set the recurrence
    Recurrence *recurrence = event->recurrence();
389
    recurrence->setStartDateTime(QDateTime(date, {}), true);
Laurent Montel's avatar
Laurent Montel committed
390
391
392
    recurrence->setYearly(1);
    if (date.month() == 2 && date.day() == 29) {
        recurrence->addYearlyDay(60);
393
394
    }

Laurent Montel's avatar
Laurent Montel committed
395
396
397
398
399
400
    // Set the alarm
    event->clearAlarms();
    if (Settings::self()->enableAlarm()) {
        Alarm::Ptr alarm = event->newAlarm();
        alarm->setType(Alarm::Display);
        alarm->setText(event->summary());
401
        alarm->setTime(QDateTime(date, {}));
Laurent Montel's avatar
Laurent Montel committed
402
403
404
405
        // N days before
        alarm->setStartOffset(Duration(-Settings::self()->alarmDays(), Duration::Days));
        alarm->setEnabled(true);
    }
406
407
408
409

    return event;
}

410
411
void BirthdaysResource::checkForUnknownCategories(const QString &categoryToCheck, Event::Ptr &event)
{
Laurent Montel's avatar
Laurent Montel committed
412
    auto tagCreateJob = new Akonadi::TagCreateJob(Akonadi::Tag(categoryToCheck), this);
413
414
415
    tagCreateJob->setMergeIfExisting(true);
    event->setCategories(categoryToCheck);
}
416

Laurent Montel's avatar
Laurent Montel committed
417
AKONADI_RESOURCE_MAIN(BirthdaysResource)