Merge lp:~mardy/account-polld/external-plugins into lp:account-polld

Proposed by Alberto Mardegan
Status: Needs review
Proposed branch: lp:~mardy/account-polld/external-plugins
Merge into: lp:account-polld
Diff against target: 13432 lines (+3027/-9737)
125 files modified
.bzrignore (+12/-0)
.qmake.conf (+2/-0)
account-polld.pro (+11/-0)
account-polld/account-polld.pro (+40/-0)
account-polld/account_manager.cpp (+282/-0)
account-polld/account_manager.h (+78/-0)
account-polld/app_manager.cpp (+118/-0)
account-polld/app_manager.h (+60/-0)
account-polld/debug.cpp (+29/-0)
account-polld/debug.h (+47/-0)
account-polld/main.cpp (+71/-0)
account-polld/plugin.cpp (+155/-0)
account-polld/plugin.h (+55/-0)
account-polld/poll_service.cpp (+190/-0)
account-polld/poll_service.h (+65/-0)
account-polld/push_client.cpp (+112/-0)
account-polld/push_client.h (+48/-0)
accounts/account-watcher.c (+0/-302)
accounts/account-watcher.h (+0/-47)
accounts/accounts.c (+0/-26)
accounts/accounts.go (+0/-130)
click-hook/account-polld.hook.in (+4/-0)
click-hook/click-hook (+108/-0)
click-hook/click-hook.pro (+20/-0)
cmd/account-polld/account_service.go (+0/-186)
cmd/account-polld/main.go (+0/-260)
cmd/account-watcher-test/main.go (+0/-17)
cmd/qtcontact-test/main.go (+0/-36)
common-installs-config.pri (+43/-0)
common-pkgconfig.pri (+12/-0)
common-project-config.pri (+36/-0)
common-vars.pri (+18/-0)
coverage.pri (+48/-0)
debian/control (+12/-17)
debian/rules (+2/-33)
gettext/LICENSE (+0/-20)
gettext/README.md (+0/-94)
gettext/gettext.go (+0/-207)
plugins/caldav/api.go (+0/-54)
plugins/caldav/caldav.go (+0/-201)
plugins/dekko/api.go (+0/-127)
plugins/dekko/dekko.go (+0/-346)
plugins/gcalendar/api.go (+0/-54)
plugins/gcalendar/gcalendar.go (+0/-189)
plugins/gmail/api.go (+0/-127)
plugins/gmail/gmail.go (+0/-346)
plugins/plugins.go (+0/-239)
plugins/twitter/oauth/README.markdown (+0/-22)
plugins/twitter/oauth/examples_test.go (+0/-55)
plugins/twitter/oauth/oauth.go (+0/-456)
plugins/twitter/oauth/oauth_test.go (+0/-172)
plugins/twitter/twitter.go (+0/-304)
plugins/twitter/twitter_test.go (+0/-500)
po/aa.po (+0/-90)
po/account-polld.pot (+0/-88)
po/am.po (+0/-100)
po/ast.po (+0/-100)
po/az.po (+0/-90)
po/br.po (+0/-90)
po/bs.po (+0/-100)
po/ca.po (+0/-100)
po/ca@valencia.po (+0/-100)
po/cs.po (+0/-90)
po/da.po (+0/-90)
po/de.po (+0/-100)
po/el.po (+0/-100)
po/en_AU.po (+0/-100)
po/en_GB.po (+0/-100)
po/es.po (+0/-100)
po/eu.po (+0/-100)
po/fa.po (+0/-100)
po/fi.po (+0/-100)
po/fr.po (+0/-100)
po/gd.po (+0/-101)
po/gl.po (+0/-100)
po/he.po (+0/-100)
po/hr.po (+0/-100)
po/hu.po (+0/-100)
po/is.po (+0/-100)
po/it.po (+0/-100)
po/ja.po (+0/-100)
po/km.po (+0/-100)
po/ko.po (+0/-100)
po/lt.po (+0/-90)
po/lv.po (+0/-90)
po/nb.po (+0/-100)
po/ne.po (+0/-90)
po/nl.po (+0/-100)
po/pl.po (+0/-93)
po/pt.po (+0/-100)
po/pt_BR.po (+0/-100)
po/ro.po (+0/-100)
po/ru.po (+0/-100)
po/sl.po (+0/-100)
po/sr.po (+0/-100)
po/sv.po (+0/-100)
po/ug.po (+0/-100)
po/uk.po (+0/-100)
po/vi.po (+0/-88)
po/zh_CN.po (+0/-90)
po/zh_TW.po (+0/-100)
pollbus/bus.go (+0/-93)
qtcontact/contacts.go (+0/-56)
qtcontact/qtcontacts.cpp (+0/-70)
qtcontact/qtcontacts.h (+0/-31)
qtcontact/qtcontacts.hpp (+0/-32)
qtcontact/qtcontacts.moc (+0/-88)
syncmonitor/syncmonitor.go (+0/-93)
tests/account-polld/account-polld.pro (+3/-0)
tests/account-polld/data/com.ubuntu.tests_application.application (+13/-0)
tests/account-polld/data/com.ubuntu.tests_coolshare.service (+7/-0)
tests/account-polld/data/cool.provider (+6/-0)
tests/account-polld/data/coolmail.service (+21/-0)
tests/account-polld/data/mailer.application (+12/-0)
tests/account-polld/fake_push_client.h (+60/-0)
tests/account-polld/fake_signond.h (+56/-0)
tests/account-polld/push_client.py (+39/-0)
tests/account-polld/signond.py (+101/-0)
tests/account-polld/test_plugin.py (+43/-0)
tests/account-polld/tst_account_polld.cpp (+600/-0)
tests/account-polld/tst_account_polld.pro (+36/-0)
tests/click-hook/click-hook.pro (+21/-0)
tests/click-hook/tst_click_hook.cpp (+327/-0)
tests/tests.pro (+4/-0)
update_translations.sh (+0/-27)
To merge this branch: bzr merge lp:~mardy/account-polld/external-plugins
Reviewer Review Type Date Requested Status
Ubuntu Push Hackers Pending
Review via email: mp+306083@code.launchpad.net

Commit message

Support out-of-process plugins

Existing Go plugins have been moved into the account-polld-plugins-go project.

Description of the change

Support out-of-process plugins

Existing Go plugins have been moved into the account-polld-plugins-go project.

To post a comment you must log in.
216. By Alberto Mardegan

Avoid cyclical deps

217. By Alberto Mardegan

Handle sigterm signal

This allows us to collect coverage results

218. By Alberto Mardegan

Fix reauthentication logic, add a test for that

Unmerged revisions

218. By Alberto Mardegan

Fix reauthentication logic, add a test for that

217. By Alberto Mardegan

Handle sigterm signal

This allows us to collect coverage results

216. By Alberto Mardegan

Avoid cyclical deps

215. By Alberto Mardegan

Update dependencies and description

214. By Alberto Mardegan

Update VCS link

213. By Alberto Mardegan

Tests: wait till method returns

212. By Alberto Mardegan

more of the same

211. By Alberto Mardegan

Tests: increase plugin timeout

210. By Alberto Mardegan

Fix push object path

209. By Alberto Mardegan

Make tests more robust

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file '.bzrignore'
2--- .bzrignore 1970-01-01 00:00:00 +0000
3+++ .bzrignore 2016-09-21 07:16:22 +0000
4@@ -0,0 +1,12 @@
5+*.moc
6+Makefile*
7+account-polld/account-polld
8+click-hook/account-polld.hook
9+debian/*.log
10+debian/*.substvars
11+debian/*-stamp
12+debian/account-polld/
13+debian/files
14+moc_*
15+tests/account-polld/tst_account_polld
16+tests/click-hook/tst_click_hook
17
18=== added file '.qmake.conf'
19--- .qmake.conf 1970-01-01 00:00:00 +0000
20+++ .qmake.conf 2016-09-21 07:16:22 +0000
21@@ -0,0 +1,2 @@
22+TOP_SRC_DIR = $$PWD
23+TOP_BUILD_DIR = $$shadowed($$PWD)
24
25=== added directory 'account-polld'
26=== added file 'account-polld.pro'
27--- account-polld.pro 1970-01-01 00:00:00 +0000
28+++ account-polld.pro 2016-09-21 07:16:22 +0000
29@@ -0,0 +1,11 @@
30+include(common-vars.pri)
31+include(common-project-config.pri)
32+
33+TEMPLATE = subdirs
34+SUBDIRS = \
35+ account-polld \
36+ click-hook \
37+ tests
38+CONFIG += ordered
39+
40+include(common-installs-config.pri)
41
42=== added file 'account-polld/account-polld.pro'
43--- account-polld/account-polld.pro 1970-01-01 00:00:00 +0000
44+++ account-polld/account-polld.pro 2016-09-21 07:16:22 +0000
45@@ -0,0 +1,40 @@
46+include(../common-project-config.pri)
47+include($${TOP_SRC_DIR}/common-vars.pri)
48+
49+TEMPLATE = app
50+TARGET = account-polld
51+
52+CONFIG += \
53+ link_pkgconfig \
54+ no_keywords \
55+ qt
56+
57+QT += \
58+ dbus
59+
60+PKGCONFIG += \
61+ accounts-qt5 \
62+ libsignon-qt5
63+
64+DEFINES += \
65+ DEBUG_ENABLED \
66+ PLUGIN_DATA_FILE=\\\"$${PLUGIN_DATA_FILE}\\\"
67+
68+SOURCES += \
69+ account_manager.cpp \
70+ app_manager.cpp \
71+ debug.cpp \
72+ main.cpp \
73+ plugin.cpp \
74+ poll_service.cpp \
75+ push_client.cpp
76+
77+HEADERS += \
78+ account_manager.h \
79+ app_manager.h \
80+ debug.h \
81+ plugin.h \
82+ poll_service.h \
83+ push_client.h
84+
85+include($${TOP_SRC_DIR}/common-installs-config.pri)
86
87=== added file 'account-polld/account_manager.cpp'
88--- account-polld/account_manager.cpp 1970-01-01 00:00:00 +0000
89+++ account-polld/account_manager.cpp 2016-09-21 07:16:22 +0000
90@@ -0,0 +1,282 @@
91+/*
92+ * Copyright (C) 2016 Canonical Ltd.
93+ *
94+ * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
95+ *
96+ * This file is part of account-polld
97+ *
98+ * This program is free software: you can redistribute it and/or modify it
99+ * under the terms of the GNU General Public License version 3, as published
100+ * by the Free Software Foundation.
101+ *
102+ * This program is distributed in the hope that it will be useful, but
103+ * WITHOUT ANY WARRANTY; without even the implied warranties of
104+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
105+ * PURPOSE. See the GNU General Public License for more details.
106+ *
107+ * You should have received a copy of the GNU General Public License along
108+ * with this program. If not, see <http://www.gnu.org/licenses/>.
109+ */
110+
111+#include "account_manager.h"
112+
113+#include "app_manager.h"
114+#include "debug.h"
115+
116+#include <Accounts/Account>
117+#include <Accounts/AccountService>
118+#include <Accounts/Application>
119+#include <Accounts/Manager>
120+#include <Accounts/Service>
121+#include <QMetaObject>
122+#include <SignOn/AuthSession>
123+#include <SignOn/Identity>
124+#include <SignOn/SessionData>
125+
126+using namespace AccountPolld;
127+
128+namespace AccountPolld {
129+
130+class AccountManagerPrivate: public QObject
131+{
132+ Q_OBJECT
133+ Q_DECLARE_PUBLIC(AccountManager)
134+
135+ struct AuthState {
136+ QVariantMap lastAuthReply;
137+ bool needNewToken;
138+ };
139+
140+public:
141+ AccountManagerPrivate(AccountManager *q, AppManager *appManager);
142+ ~AccountManagerPrivate() {};
143+
144+ void loadApplications();
145+ void activateAccount(Accounts::AccountService *as,
146+ const QString &appKey);
147+ void accountReady(Accounts::AccountService *as, const QString &appKey,
148+ const QVariantMap &auth = QVariantMap());
149+ static QString accountServiceKey(Accounts::AccountService *as);
150+ static QString accountServiceKey(uint accountId, const QString &serviceId);
151+
152+ void markAuthFailure(const AccountData &data);
153+ QVariantMap formatAuthReply(const Accounts::AuthData &authData,
154+ const QVariantMap &reply) const;
155+
156+public Q_SLOTS:
157+ void operationFinished();
158+
159+private:
160+ Accounts::Manager m_manager;
161+ AppManager *m_appManager;
162+ Applications m_apps;
163+ QHash<QString,Accounts::Application> m_accountApps;
164+ QHash<QString,AuthState> m_authStates;
165+ int m_pendingOperations;
166+ AccountManager *q_ptr;
167+};
168+
169+uint qHash(const AccountData &data) {
170+ return ::qHash(data.pluginId) + ::qHash(data.accountId) +
171+ ::qHash(data.serviceId);
172+}
173+
174+} // namespace
175+
176+AccountManagerPrivate::AccountManagerPrivate(AccountManager *q,
177+ AppManager *appManager):
178+ QObject(q),
179+ m_appManager(appManager),
180+ m_pendingOperations(0),
181+ q_ptr(q)
182+{
183+ qRegisterMetaType<AccountData>("AccountData");
184+}
185+
186+QString AccountManagerPrivate::accountServiceKey(Accounts::AccountService *as)
187+{
188+ return accountServiceKey(as->account()->id(), as->service().name());
189+}
190+
191+QString AccountManagerPrivate::accountServiceKey(uint accountId, const QString &serviceId)
192+{
193+ return QString("%1-%2").arg(accountId).arg(serviceId);
194+}
195+
196+void AccountManagerPrivate::loadApplications()
197+{
198+ m_accountApps.clear();
199+
200+ m_apps = m_appManager->applications();
201+ for (auto i = m_apps.constBegin(); i != m_apps.constEnd(); i++) {
202+ Accounts::Application app = m_manager.application(i.value().appId);
203+ if (app.isValid()) {
204+ m_accountApps.insert(i.key(), app);
205+ } else {
206+ DEBUG() << "Application not found:" << i.value().appId;
207+ }
208+ }
209+}
210+
211+QVariantMap AccountManagerPrivate::formatAuthReply(const Accounts::AuthData &authData,
212+ const QVariantMap &reply) const
213+{
214+ QVariantMap formattedReply(reply);
215+
216+ QString mechanism = authData.mechanism();
217+ const QVariantMap &parameters = authData.parameters();
218+ if (mechanism == "HMAC-SHA1" || mechanism == "PLAINTEXT") {
219+ /* For OAuth 1.0, let's return also the Consumer key and secret along
220+ * with the reply. */
221+ formattedReply["ClientId"] = parameters.value("ConsumerKey");
222+ formattedReply["ClientSecret"] = parameters.value("ConsumerSecret");
223+ } else if (mechanism == "web_server" || mechanism == "user_agent") {
224+ formattedReply["ClientId"] = parameters.value("ClientId");
225+ formattedReply["ClientSecret"] = parameters.value("ClientId");
226+ }
227+
228+ return formattedReply;
229+}
230+
231+void AccountManagerPrivate::accountReady(Accounts::AccountService *as,
232+ const QString &appKey,
233+ const QVariantMap &auth)
234+{
235+ Q_Q(AccountManager);
236+ AccountData accountData;
237+ accountData.pluginId = appKey;
238+ accountData.accountId = as->account()->id();
239+ accountData.serviceId = as->service().name();
240+ accountData.auth = auth;
241+ QMetaObject::invokeMethod(q, "accountReady", Qt::QueuedConnection,
242+ Q_ARG(AccountData, accountData));
243+}
244+
245+void AccountManagerPrivate::activateAccount(Accounts::AccountService *as,
246+ const QString &appKey)
247+{
248+ const AppData &data = m_apps[appKey];
249+ if (data.needsAuthData) {
250+ Accounts::AuthData authData = as->authData();
251+ QString key = accountServiceKey(as);
252+
253+ auto identity =
254+ SignOn::Identity::existingIdentity(authData.credentialsId(), as);
255+ auto authSession = identity->createSession(authData.method());
256+ QObject::connect(authSession, &SignOn::AuthSession::response,
257+ [this,as,appKey](const SignOn::SessionData &reply) {
258+ as->deleteLater();
259+
260+ QVariantMap authReply = formatAuthReply(as->authData(), reply.toMap());
261+ AuthState &authState = m_authStates[accountServiceKey(as)];
262+ if (authState.needNewToken && authReply == authState.lastAuthReply) {
263+ /* This account won't work, don't even check it */
264+ operationFinished();
265+ return;
266+ }
267+
268+ authState.needNewToken = false;
269+ accountReady(as, appKey, authReply);
270+ operationFinished();
271+ });
272+ QObject::connect(authSession, &SignOn::AuthSession::error,
273+ [this,as](const SignOn::Error &error) {
274+ as->deleteLater();
275+ operationFinished();
276+ DEBUG() << "authentication error:" << error.message();
277+ });
278+
279+ AuthState &authState = m_authStates[key];
280+
281+ QVariantMap sessionData = authData.parameters();
282+ sessionData["UiPolicy"] = SignOn::NoUserInteractionPolicy;
283+ if (authState.needNewToken) {
284+ sessionData["ForceTokenRefresh"] = true;
285+ }
286+ m_pendingOperations++;
287+ authSession->process(sessionData, authData.mechanism());
288+ } else {
289+ accountReady(as, appKey);
290+ }
291+}
292+
293+void AccountManagerPrivate::markAuthFailure(const AccountData &data)
294+{
295+ QString key = accountServiceKey(data.accountId, data.serviceId);
296+ AuthState &authState = m_authStates[key];
297+ authState.lastAuthReply = data.auth;
298+ authState.needNewToken = true;
299+}
300+
301+void AccountManagerPrivate::operationFinished()
302+{
303+ Q_Q(AccountManager);
304+ m_pendingOperations--;
305+ if (m_pendingOperations == 0) {
306+ /* since the accountReady signal is sent in a queued connection, this
307+ * signal must also be sent in that way, in order to be delivered after
308+ * all the accountReady signals. */
309+ QMetaObject::invokeMethod(q, "finished", Qt::QueuedConnection);
310+ }
311+}
312+
313+AccountManager::AccountManager(AppManager *appManager, QObject *parent):
314+ QObject(parent),
315+ d_ptr(new AccountManagerPrivate(this, appManager))
316+{
317+}
318+
319+AccountManager::~AccountManager()
320+{
321+ delete d_ptr;
322+}
323+
324+void AccountManager::listAccounts()
325+{
326+ Q_D(AccountManager);
327+
328+ d->loadApplications();
329+
330+ d->m_pendingOperations++;
331+
332+ Accounts::AccountIdList accountIds = d->m_manager.accountListEnabled();
333+ for (Accounts::AccountId accountId: accountIds) {
334+ Accounts::Account *account = d->m_manager.account(accountId);
335+ if (Q_UNLIKELY(!account)) continue;
336+
337+ Accounts::ServiceList services = account->enabledServices();
338+
339+ /* check if we have some plugins registered for this service */
340+ for (auto i = d->m_accountApps.constBegin();
341+ i != d->m_accountApps.constEnd(); i++) {
342+ for (Accounts::Service &service: services) {
343+ /* Check if the application can use this service */
344+ if (i.value().serviceUsage(service).isEmpty()) {
345+ continue;
346+ }
347+
348+ /* Check if the plugin manifest allows using this service */
349+ const AppData &appData = d->m_apps[i.key()];
350+ if (!appData.services.isEmpty() &&
351+ !appData.services.contains(service.name())) {
352+ DEBUG() << "Skipping service" << service.name() <<
353+ "for plugin" << i.key();
354+ continue;
355+ }
356+
357+ auto *as = new Accounts::AccountService(account, service);
358+ d->activateAccount(as, i.key());
359+ }
360+ }
361+ }
362+
363+ d->operationFinished();
364+}
365+
366+void AccountManager::markAuthFailure(const AccountData &data)
367+{
368+ Q_D(AccountManager);
369+ d->markAuthFailure(data);
370+}
371+
372+#include "account_manager.moc"
373
374=== added file 'account-polld/account_manager.h'
375--- account-polld/account_manager.h 1970-01-01 00:00:00 +0000
376+++ account-polld/account_manager.h 2016-09-21 07:16:22 +0000
377@@ -0,0 +1,78 @@
378+/*
379+ * Copyright (C) 2016 Canonical Ltd.
380+ *
381+ * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
382+ *
383+ * This file is part of account-polld
384+ *
385+ * This program is free software: you can redistribute it and/or modify it
386+ * under the terms of the GNU General Public License version 3, as published
387+ * by the Free Software Foundation.
388+ *
389+ * This program is distributed in the hope that it will be useful, but
390+ * WITHOUT ANY WARRANTY; without even the implied warranties of
391+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
392+ * PURPOSE. See the GNU General Public License for more details.
393+ *
394+ * You should have received a copy of the GNU General Public License along
395+ * with this program. If not, see <http://www.gnu.org/licenses/>.
396+ */
397+
398+#ifndef AP_ACCOUNT_MANAGER_H
399+#define AP_ACCOUNT_MANAGER_H
400+
401+#include <QObject>
402+#include <QVariantMap>
403+
404+namespace AccountPolld {
405+
406+struct AccountData {
407+ QString pluginId;
408+ uint accountId;
409+ QString serviceId;
410+ QVariantMap auth;
411+
412+ /* This is needed for using the struct as a QHash key; the "auth" map is
413+ * intentionally omitted from the comparison as we don't want to use that
414+ * as a key, too. */
415+ bool operator==(const AccountData &other) const {
416+ return pluginId == other.pluginId && accountId == other.accountId &&
417+ serviceId == other.serviceId;
418+ }
419+};
420+
421+uint qHash(const AccountData &data);
422+
423+class AppManager;
424+
425+class AccountManagerPrivate;
426+class AccountManager: public QObject
427+{
428+ Q_OBJECT
429+
430+public:
431+ explicit AccountManager(AppManager *appManager, QObject *parent = 0);
432+ ~AccountManager();
433+
434+ /* Scan for accounts; for each valid account, the accountReady() signal
435+ * will be emitted. A finished() signal will be emitted last. */
436+ void listAccounts();
437+
438+ /* Call when the authentication data for an account is refused by the
439+ * server because of token expiration */
440+ void markAuthFailure(const AccountData &data);
441+
442+Q_SIGNALS:
443+ void accountReady(const AccountData &data);
444+ void finished();
445+
446+private:
447+ AccountManagerPrivate *d_ptr;
448+ Q_DECLARE_PRIVATE(AccountManager)
449+};
450+
451+} // namespace
452+
453+Q_DECLARE_METATYPE(AccountPolld::AccountData)
454+
455+#endif // AP_ACCOUNT_MANAGER_H
456
457=== added file 'account-polld/app_manager.cpp'
458--- account-polld/app_manager.cpp 1970-01-01 00:00:00 +0000
459+++ account-polld/app_manager.cpp 2016-09-21 07:16:22 +0000
460@@ -0,0 +1,118 @@
461+/*
462+ * Copyright (C) 2016 Canonical Ltd.
463+ *
464+ * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
465+ *
466+ * This file is part of account-polld
467+ *
468+ * This program is free software: you can redistribute it and/or modify it
469+ * under the terms of the GNU General Public License version 3, as published
470+ * by the Free Software Foundation.
471+ *
472+ * This program is distributed in the hope that it will be useful, but
473+ * WITHOUT ANY WARRANTY; without even the implied warranties of
474+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
475+ * PURPOSE. See the GNU General Public License for more details.
476+ *
477+ * You should have received a copy of the GNU General Public License along
478+ * with this program. If not, see <http://www.gnu.org/licenses/>.
479+ */
480+
481+#include "debug.h"
482+#include "app_manager.h"
483+
484+#include <QFile>
485+#include <QJsonArray>
486+#include <QJsonDocument>
487+#include <QJsonObject>
488+#include <QStandardPaths>
489+
490+using namespace AccountPolld;
491+
492+namespace AccountPolld {
493+
494+class AppManagerPrivate: public QObject
495+{
496+ Q_OBJECT
497+ Q_DECLARE_PUBLIC(AppManager)
498+
499+public:
500+ AppManagerPrivate(AppManager *q);
501+ ~AppManagerPrivate() {};
502+
503+ Applications readPluginData() const;
504+
505+private:
506+ QString m_dataFilePath;
507+ AppManager *q_ptr;
508+};
509+
510+} // namespace
511+
512+AppManagerPrivate::AppManagerPrivate(AppManager *q):
513+ QObject(q),
514+ q_ptr(q)
515+{
516+ const QString localShare =
517+ QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
518+ m_dataFilePath = localShare + "/" PLUGIN_DATA_FILE;
519+}
520+
521+Applications AppManagerPrivate::readPluginData() const
522+{
523+ Applications apps;
524+
525+ QFile file(m_dataFilePath);
526+ if (!file.open(QIODevice::ReadOnly)) return apps;
527+
528+ QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
529+ file.close();
530+
531+ QJsonObject mainObject = doc.object();
532+ for (auto i = mainObject.begin(); i != mainObject.end(); i++) {
533+ QJsonObject appObject = i.value().toObject();
534+
535+ AppData data;
536+ data.profile = appObject.value("profile").toString();
537+ data.execLine = appObject.value("exec").toString();
538+ data.appId = appObject.value("appId").toString();
539+ QJsonArray services = appObject.value("services").toArray();
540+ for (const QJsonValue &v: services) {
541+ data.services.append(v.toString());
542+ }
543+ data.interval = appObject.value("interval").toInt();
544+ data.needsAuthData = appObject.value("needsAuthData").toBool();
545+
546+ if (data.profile.isEmpty() ||
547+ data.execLine.isEmpty() ||
548+ data.appId.isEmpty()) {
549+ qWarning() << "Incomplete plugin data:" <<
550+ QJsonDocument(appObject).toJson(QJsonDocument::Compact);
551+ continue;
552+ }
553+
554+ apps.insert(i.key(), data);
555+ }
556+
557+ return apps;
558+}
559+
560+AppManager::AppManager(QObject *parent):
561+ QObject(parent),
562+ d_ptr(new AppManagerPrivate(this))
563+{
564+}
565+
566+AppManager::~AppManager()
567+{
568+ delete d_ptr;
569+}
570+
571+Applications AppManager::applications() const
572+{
573+ Q_D(const AppManager);
574+
575+ return d->readPluginData();
576+}
577+
578+#include "app_manager.moc"
579
580=== added file 'account-polld/app_manager.h'
581--- account-polld/app_manager.h 1970-01-01 00:00:00 +0000
582+++ account-polld/app_manager.h 2016-09-21 07:16:22 +0000
583@@ -0,0 +1,60 @@
584+/*
585+ * Copyright (C) 2016 Canonical Ltd.
586+ *
587+ * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
588+ *
589+ * This file is part of account-polld
590+ *
591+ * This program is free software: you can redistribute it and/or modify it
592+ * under the terms of the GNU General Public License version 3, as published
593+ * by the Free Software Foundation.
594+ *
595+ * This program is distributed in the hope that it will be useful, but
596+ * WITHOUT ANY WARRANTY; without even the implied warranties of
597+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
598+ * PURPOSE. See the GNU General Public License for more details.
599+ *
600+ * You should have received a copy of the GNU General Public License along
601+ * with this program. If not, see <http://www.gnu.org/licenses/>.
602+ */
603+
604+#ifndef AP_APP_MANAGER_H
605+#define AP_APP_MANAGER_H
606+
607+#include <QObject>
608+#include <QStringList>
609+#include <QHash>
610+
611+namespace AccountPolld {
612+
613+struct AppData {
614+ QString profile; // apparmor label for the plugin process
615+ QString execLine;
616+ QString appId; // appId, for matching with OA
617+ QStringList services;
618+ int interval;
619+ bool needsAuthData;
620+};
621+
622+typedef QHash<QString,AppData> Applications;
623+
624+class AppManagerPrivate;
625+
626+class AppManager: public QObject
627+{
628+ Q_OBJECT
629+
630+public:
631+ explicit AppManager(QObject *parent = 0);
632+ ~AppManager();
633+
634+ Applications applications() const;
635+
636+private:
637+ AppManagerPrivate *d_ptr;
638+ Q_DECLARE_PRIVATE(AppManager)
639+};
640+
641+} // namespace
642+
643+#endif // AP_APP_MANAGER_H
644
645=== added file 'account-polld/debug.cpp'
646--- account-polld/debug.cpp 1970-01-01 00:00:00 +0000
647+++ account-polld/debug.cpp 2016-09-21 07:16:22 +0000
648@@ -0,0 +1,29 @@
649+/*
650+ * Copyright (C) 2016 Canonical Ltd.
651+ *
652+ * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
653+ *
654+ * This file is part of account-polld
655+ *
656+ * This program is free software: you can redistribute it and/or modify it
657+ * under the terms of the GNU General Public License version 3, as published
658+ * by the Free Software Foundation.
659+ *
660+ * This program is distributed in the hope that it will be useful, but
661+ * WITHOUT ANY WARRANTY; without even the implied warranties of
662+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
663+ * PURPOSE. See the GNU General Public License for more details.
664+ *
665+ * You should have received a copy of the GNU General Public License along
666+ * with this program. If not, see <http://www.gnu.org/licenses/>.
667+ */
668+
669+#include "debug.h"
670+
671+int appLoggingLevel = 1; // criticals
672+
673+void setLoggingLevel(int level)
674+{
675+ appLoggingLevel = level;
676+}
677+
678
679=== added file 'account-polld/debug.h'
680--- account-polld/debug.h 1970-01-01 00:00:00 +0000
681+++ account-polld/debug.h 2016-09-21 07:16:22 +0000
682@@ -0,0 +1,47 @@
683+/*
684+ * Copyright (C) 2016 Canonical Ltd.
685+ *
686+ * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
687+ *
688+ * This file is part of account-polld
689+ *
690+ * This program is free software: you can redistribute it and/or modify it
691+ * under the terms of the GNU General Public License version 3, as published
692+ * by the Free Software Foundation.
693+ *
694+ * This program is distributed in the hope that it will be useful, but
695+ * WITHOUT ANY WARRANTY; without even the implied warranties of
696+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
697+ * PURPOSE. See the GNU General Public License for more details.
698+ *
699+ * You should have received a copy of the GNU General Public License along
700+ * with this program. If not, see <http://www.gnu.org/licenses/>.
701+ */
702+#ifndef AP_DEBUG_H
703+#define AP_DEBUG_H
704+
705+#include <QDebug>
706+
707+/* 0 - fatal, 1 - critical(default), 2 - info/debug */
708+extern int appLoggingLevel;
709+
710+static inline bool debugEnabled()
711+{
712+ return appLoggingLevel >= 2;
713+}
714+
715+static inline int loggingLevel()
716+{
717+ return appLoggingLevel;
718+}
719+
720+void setLoggingLevel(int level);
721+
722+#ifdef DEBUG_ENABLED
723+ #define DEBUG() \
724+ if (debugEnabled()) qDebug() << __FILE__ << __LINE__ << __func__
725+#else
726+ #define DEBUG() while (0) qDebug()
727+#endif
728+
729+#endif // AP_DEBUG_H
730
731=== added file 'account-polld/main.cpp'
732--- account-polld/main.cpp 1970-01-01 00:00:00 +0000
733+++ account-polld/main.cpp 2016-09-21 07:16:22 +0000
734@@ -0,0 +1,71 @@
735+/*
736+ * Copyright (C) 2016 Canonical Ltd.
737+ *
738+ * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
739+ *
740+ * This file is part of account-polld
741+ *
742+ * This program is free software: you can redistribute it and/or modify it
743+ * under the terms of the GNU General Public License version 3, as published
744+ * by the Free Software Foundation.
745+ *
746+ * This program is distributed in the hope that it will be useful, but
747+ * WITHOUT ANY WARRANTY; without even the implied warranties of
748+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
749+ * PURPOSE. See the GNU General Public License for more details.
750+ *
751+ * You should have received a copy of the GNU General Public License along
752+ * with this program. If not, see <http://www.gnu.org/licenses/>.
753+ */
754+
755+#include <QCoreApplication>
756+#include <QDBusConnection>
757+#include <QProcessEnvironment>
758+#include <QSettings>
759+#include <signal.h>
760+
761+#include "debug.h"
762+#include "poll_service.h"
763+
764+
765+static void signalHandler(int)
766+{
767+ QCoreApplication::quit();
768+}
769+
770+int main(int argc, char **argv)
771+{
772+ QCoreApplication app(argc, argv);
773+
774+ signal(SIGTERM, signalHandler);
775+
776+ QSettings settings("account-polld");
777+
778+ /* read environment variables */
779+ QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
780+ if (environment.contains(QLatin1String("AP_LOGGING_LEVEL"))) {
781+ bool isOk;
782+ int value = environment.value(
783+ QLatin1String("AP_LOGGING_LEVEL")).toInt(&isOk);
784+ if (isOk)
785+ setLoggingLevel(value);
786+ } else {
787+ setLoggingLevel(settings.value("LoggingLevel", 1).toInt());
788+ }
789+
790+ QDBusConnection connection = QDBusConnection::sessionBus();
791+
792+ auto service = new AccountPolld::PollService();
793+ connection.registerObject(ACCOUNT_POLLD_OBJECT_PATH, service,
794+ QDBusConnection::ExportAllContents);
795+ connection.registerService(ACCOUNT_POLLD_SERVICE_NAME);
796+
797+ int ret = app.exec();
798+
799+ connection.unregisterService(ACCOUNT_POLLD_SERVICE_NAME);
800+ connection.unregisterObject(ACCOUNT_POLLD_OBJECT_PATH);
801+ delete service;
802+
803+ return ret;
804+}
805+
806
807=== added file 'account-polld/plugin.cpp'
808--- account-polld/plugin.cpp 1970-01-01 00:00:00 +0000
809+++ account-polld/plugin.cpp 2016-09-21 07:16:22 +0000
810@@ -0,0 +1,155 @@
811+/*
812+ * Copyright (C) 2016 Canonical Ltd.
813+ *
814+ * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
815+ *
816+ * This file is part of account-polld
817+ *
818+ * This program is free software: you can redistribute it and/or modify it
819+ * under the terms of the GNU General Public License version 3, as published
820+ * by the Free Software Foundation.
821+ *
822+ * This program is distributed in the hope that it will be useful, but
823+ * WITHOUT ANY WARRANTY; without even the implied warranties of
824+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
825+ * PURPOSE. See the GNU General Public License for more details.
826+ *
827+ * You should have received a copy of the GNU General Public License along
828+ * with this program. If not, see <http://www.gnu.org/licenses/>.
829+ */
830+
831+#include "plugin.h"
832+
833+#include "debug.h"
834+
835+#include <QByteArray>
836+#include <QJsonDocument>
837+#include <QJsonObject>
838+#include <QJsonParseError>
839+#include <QProcess>
840+#include <QTimer>
841+#include <signal.h>
842+#include <sys/types.h>
843+
844+using namespace AccountPolld;
845+
846+namespace AccountPolld {
847+
848+class PluginPrivate: public QProcess
849+{
850+ Q_OBJECT
851+ Q_DECLARE_PUBLIC(Plugin)
852+
853+public:
854+ PluginPrivate(Plugin *q, const QString &execLine, const QString &profile);
855+ ~PluginPrivate() {};
856+
857+public Q_SLOTS:
858+ void onReadyRead();
859+ void killPlugin();
860+
861+private:
862+ QString m_execLine;
863+ QString m_profile;
864+ QTimer m_timer;
865+ QByteArray m_inputBuffer;
866+ bool m_sigtermSent;
867+ Plugin *q_ptr;
868+};
869+
870+} // namespace
871+
872+PluginPrivate::PluginPrivate(Plugin *q,
873+ const QString &execLine,
874+ const QString &profile):
875+ QProcess(q),
876+ m_execLine(execLine),
877+ m_profile(profile),
878+ m_sigtermSent(false),
879+ q_ptr(q)
880+{
881+ int killTime = 10;
882+ QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
883+ if (environment.contains("AP_PLUGIN_TIMEOUT")) {
884+ killTime = environment.value("AP_PLUGIN_TIMEOUT").toInt();
885+ }
886+ m_timer.setInterval(killTime * 1000);
887+ m_timer.setSingleShot(true);
888+
889+ setProcessChannelMode(QProcess::ForwardedErrorChannel);
890+ QObject::connect(this, SIGNAL(started()), &m_timer, SLOT(start()));
891+ QObject::connect(this, SIGNAL(started()), q, SIGNAL(ready()));
892+ QObject::connect(this, SIGNAL(finished(int,QProcess::ExitStatus)),
893+ q, SIGNAL(finished()));
894+ QObject::connect(this, SIGNAL(readyReadStandardOutput()),
895+ this, SLOT(onReadyRead()));
896+ QObject::connect(&m_timer, SIGNAL(timeout()), this, SLOT(killPlugin()));
897+}
898+
899+void PluginPrivate::onReadyRead()
900+{
901+ Q_Q(Plugin);
902+
903+ m_inputBuffer.append(readAllStandardOutput());
904+ QJsonParseError error;
905+ auto doc = QJsonDocument::fromJson(m_inputBuffer, &error);
906+ if (error.error == QJsonParseError::NoError) {
907+ m_inputBuffer.clear();
908+ Q_EMIT q->response(doc.object());
909+ }
910+
911+ /* otherwise continue reasing, the object is probably uncomplete */
912+}
913+
914+void PluginPrivate::killPlugin()
915+{
916+ pid_t pid = processId();
917+ DEBUG() << "killing plugin" << pid;
918+ if (!m_sigtermSent) {
919+ ::kill(pid, SIGTERM);
920+ m_sigtermSent = true;
921+ m_timer.setInterval(1 * 1000);
922+ m_timer.start();
923+ } else {
924+ ::kill(pid, SIGKILL);
925+ }
926+}
927+
928+Plugin::Plugin(const QString &execLine, const QString &profile,
929+ QObject *parent):
930+ QObject(parent),
931+ d_ptr(new PluginPrivate(this, execLine, profile))
932+{
933+}
934+
935+Plugin::~Plugin()
936+{
937+ delete d_ptr;
938+}
939+
940+void Plugin::run()
941+{
942+ Q_D(Plugin);
943+
944+ QString command;
945+
946+ if (d->m_profile != "unconfined") {
947+ command = QString("aa-exec-click -p %1 -- ").arg(d->m_profile);
948+ }
949+
950+ command.append(d->m_execLine);
951+
952+ DEBUG() << "Starting" << command;
953+ d->start(command);
954+}
955+
956+void Plugin::poll(const QJsonObject &pollData)
957+{
958+ Q_D(Plugin);
959+
960+ DEBUG() << "Plugin input:" << pollData;
961+ d->write(QJsonDocument(pollData).toJson(QJsonDocument::Compact));
962+ d->write("\n");
963+}
964+
965+#include "plugin.moc"
966
967=== added file 'account-polld/plugin.h'
968--- account-polld/plugin.h 1970-01-01 00:00:00 +0000
969+++ account-polld/plugin.h 2016-09-21 07:16:22 +0000
970@@ -0,0 +1,55 @@
971+/*
972+ * Copyright (C) 2016 Canonical Ltd.
973+ *
974+ * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
975+ *
976+ * This file is part of account-polld
977+ *
978+ * This program is free software: you can redistribute it and/or modify it
979+ * under the terms of the GNU General Public License version 3, as published
980+ * by the Free Software Foundation.
981+ *
982+ * This program is distributed in the hope that it will be useful, but
983+ * WITHOUT ANY WARRANTY; without even the implied warranties of
984+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
985+ * PURPOSE. See the GNU General Public License for more details.
986+ *
987+ * You should have received a copy of the GNU General Public License along
988+ * with this program. If not, see <http://www.gnu.org/licenses/>.
989+ */
990+
991+#ifndef AP_PLUGIN_H
992+#define AP_PLUGIN_H
993+
994+#include <QObject>
995+
996+class QJsonObject;
997+
998+namespace AccountPolld {
999+
1000+class PluginPrivate;
1001+class Plugin: public QObject
1002+{
1003+ Q_OBJECT
1004+
1005+public:
1006+ explicit Plugin(const QString &execLine, const QString &profile,
1007+ QObject *parent = 0);
1008+ ~Plugin();
1009+
1010+ void run();
1011+ void poll(const QJsonObject &pollData);
1012+
1013+Q_SIGNALS:
1014+ void ready();
1015+ void response(const QJsonObject &resp);
1016+ void finished();
1017+
1018+private:
1019+ PluginPrivate *d_ptr;
1020+ Q_DECLARE_PRIVATE(Plugin)
1021+};
1022+
1023+} // namespace
1024+
1025+#endif // AP_PLUGIN_H
1026
1027=== added file 'account-polld/poll_service.cpp'
1028--- account-polld/poll_service.cpp 1970-01-01 00:00:00 +0000
1029+++ account-polld/poll_service.cpp 2016-09-21 07:16:22 +0000
1030@@ -0,0 +1,190 @@
1031+/*
1032+ * Copyright (C) 2016 Canonical Ltd.
1033+ *
1034+ * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
1035+ *
1036+ * This file is part of account-polld
1037+ *
1038+ * This program is free software: you can redistribute it and/or modify it
1039+ * under the terms of the GNU General Public License version 3, as published
1040+ * by the Free Software Foundation.
1041+ *
1042+ * This program is distributed in the hope that it will be useful, but
1043+ * WITHOUT ANY WARRANTY; without even the implied warranties of
1044+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1045+ * PURPOSE. See the GNU General Public License for more details.
1046+ *
1047+ * You should have received a copy of the GNU General Public License along
1048+ * with this program. If not, see <http://www.gnu.org/licenses/>.
1049+ */
1050+
1051+#include "debug.h"
1052+#include "account_manager.h"
1053+#include "app_manager.h"
1054+#include "poll_service.h"
1055+#include "plugin.h"
1056+#include "push_client.h"
1057+
1058+#include <QDateTime>
1059+#include <QDBusArgument>
1060+#include <QDBusConnection>
1061+#include <QJsonArray>
1062+#include <QJsonObject>
1063+#include <QVariantMap>
1064+
1065+using namespace AccountPolld;
1066+
1067+namespace AccountPolld {
1068+
1069+class PollServicePrivate: public QObject
1070+{
1071+ Q_OBJECT
1072+ Q_DECLARE_PUBLIC(PollService)
1073+
1074+ struct PollData {
1075+ QDateTime lastPolled;
1076+ };
1077+
1078+public:
1079+ PollServicePrivate(PollService *q);
1080+ ~PollServicePrivate() {};
1081+
1082+ QJsonObject preparePluginInput(const AccountData &accountData,
1083+ const AppData &appData);
1084+ void handleResponse(const QJsonObject &response, const QString &appId,
1085+ const AccountData &accountData);
1086+
1087+private Q_SLOTS:
1088+ void poll();
1089+ void onAccountReady(const AccountData &data);
1090+ void operationFinished();
1091+
1092+private:
1093+ AppManager m_appManager;
1094+ AccountManager m_accountManager;
1095+ PushClient m_pushClient;
1096+ QHash<AccountData,PollData> m_polls;
1097+ int m_pendingOperations;
1098+ PollService *q_ptr;
1099+};
1100+
1101+} // namespace
1102+
1103+PollServicePrivate::PollServicePrivate(PollService *q):
1104+ QObject(q),
1105+ m_accountManager(&m_appManager),
1106+ m_pendingOperations(0),
1107+ q_ptr(q)
1108+{
1109+ QObject::connect(&m_accountManager,
1110+ SIGNAL(accountReady(const AccountData&)),
1111+ this,
1112+ SLOT(onAccountReady(const AccountData&)));
1113+ QObject::connect(&m_accountManager, SIGNAL(finished()),
1114+ this, SLOT(operationFinished()));
1115+}
1116+
1117+void PollServicePrivate::operationFinished()
1118+{
1119+ Q_Q(PollService);
1120+ m_pendingOperations--;
1121+ if (m_pendingOperations == 0) {
1122+ Q_EMIT q->Done();
1123+ }
1124+}
1125+
1126+QJsonObject
1127+PollServicePrivate::preparePluginInput(const AccountData &accountData,
1128+ const AppData &appData)
1129+{
1130+ QJsonObject object;
1131+ object["helperId"] = accountData.pluginId;
1132+ object["appId"] = appData.appId;
1133+ object["accountId"] = int(accountData.accountId);
1134+ if (appData.needsAuthData) {
1135+ object["auth"] = QJsonObject::fromVariantMap(accountData.auth);
1136+ }
1137+ return object;
1138+}
1139+
1140+void PollServicePrivate::handleResponse(const QJsonObject &response,
1141+ const QString &appId,
1142+ const AccountData &accountData)
1143+{
1144+ DEBUG() << "Plugin response:" << response;
1145+ QJsonObject error = response["error"].toObject();
1146+ if (error["code"].toString() == "ERR_INVALID_AUTH") {
1147+ m_accountManager.markAuthFailure(accountData);
1148+ return;
1149+ }
1150+
1151+ QJsonArray notifications = response["notifications"].toArray();
1152+ for (const QJsonValue &v: notifications) {
1153+ m_pushClient.post(appId, v.toObject());
1154+ }
1155+}
1156+
1157+void PollServicePrivate::poll()
1158+{
1159+ m_pendingOperations++;
1160+ m_accountManager.listAccounts();
1161+}
1162+
1163+void PollServicePrivate::onAccountReady(const AccountData &accountData)
1164+{
1165+ Applications apps = m_appManager.applications();
1166+ const auto i = apps.find(accountData.pluginId);
1167+ if (i == apps.end()) {
1168+ qWarning() << "Got account for plugin, but no app linked:" << accountData.pluginId;
1169+ return;
1170+ }
1171+
1172+ const AppData &appData = i.value();
1173+
1174+ /* Check that we are not polling more often than what the application
1175+ * wishes to */
1176+ PollData &pollData = m_polls[accountData];
1177+ QDateTime now = QDateTime::currentDateTime();
1178+ if (pollData.lastPolled.isValid() &&
1179+ pollData.lastPolled.secsTo(now) < appData.interval) {
1180+ DEBUG() << "Skipping poll, interval not yet expired:" << accountData.pluginId;
1181+ return;
1182+ }
1183+ pollData.lastPolled = now;
1184+
1185+ QJsonObject pluginInput = preparePluginInput(accountData, appData);
1186+
1187+ Plugin *plugin = new Plugin(appData.execLine, appData.profile, this);
1188+ QObject::connect(plugin, SIGNAL(finished()), plugin, SLOT(deleteLater()));
1189+ QObject::connect(plugin, SIGNAL(finished()), this, SLOT(operationFinished()));
1190+ QObject::connect(plugin, &Plugin::ready,
1191+ [plugin, pluginInput]() { plugin->poll(pluginInput); });
1192+ QObject::connect(plugin, &Plugin::response,
1193+ [this, accountData, appData](const QJsonObject &resp) {
1194+ handleResponse(resp, appData.appId, accountData);
1195+ });
1196+
1197+ m_pendingOperations++;
1198+ plugin->run();
1199+}
1200+
1201+PollService::PollService(QObject *parent):
1202+ QObject(parent),
1203+ d_ptr(new PollServicePrivate(this))
1204+{
1205+}
1206+
1207+PollService::~PollService()
1208+{
1209+ delete d_ptr;
1210+}
1211+
1212+void PollService::Poll()
1213+{
1214+ Q_D(PollService);
1215+
1216+ DEBUG() << "Got Poll";
1217+ QMetaObject::invokeMethod(d, "poll", Qt::QueuedConnection);
1218+}
1219+
1220+#include "poll_service.moc"
1221
1222=== added file 'account-polld/poll_service.h'
1223--- account-polld/poll_service.h 1970-01-01 00:00:00 +0000
1224+++ account-polld/poll_service.h 2016-09-21 07:16:22 +0000
1225@@ -0,0 +1,65 @@
1226+/*
1227+ * Copyright (C) 2016 Canonical Ltd.
1228+ *
1229+ * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
1230+ *
1231+ * This file is part of account-polld
1232+ *
1233+ * This program is free software: you can redistribute it and/or modify it
1234+ * under the terms of the GNU General Public License version 3, as published
1235+ * by the Free Software Foundation.
1236+ *
1237+ * This program is distributed in the hope that it will be useful, but
1238+ * WITHOUT ANY WARRANTY; without even the implied warranties of
1239+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1240+ * PURPOSE. See the GNU General Public License for more details.
1241+ *
1242+ * You should have received a copy of the GNU General Public License along
1243+ * with this program. If not, see <http://www.gnu.org/licenses/>.
1244+ */
1245+
1246+#ifndef AP_POLL_SERVICE_H
1247+#define AP_POLL_SERVICE_H
1248+
1249+#include <QDBusContext>
1250+#include <QDBusMessage>
1251+#include <QObject>
1252+
1253+namespace AccountPolld {
1254+
1255+#define ACCOUNT_POLLD_OBJECT_PATH \
1256+ QStringLiteral("/com/ubuntu/AccountPolld")
1257+#define ACCOUNT_POLLD_SERVICE_NAME \
1258+ QStringLiteral("com.ubuntu.AccountPolld")
1259+
1260+class PollServicePrivate;
1261+
1262+class PollService: public QObject, protected QDBusContext
1263+{
1264+ Q_OBJECT
1265+ Q_CLASSINFO("D-Bus Interface", "com.ubuntu.AccountPolld")
1266+ Q_CLASSINFO("D-Bus Introspection", ""
1267+" <interface name=\"com.ubuntu.AccountPolld\">\n"
1268+" <method name=\"Poll\" />\n"
1269+" <signal name=\"Done\" />\n"
1270+" </interface>\n"
1271+ "")
1272+
1273+public:
1274+ explicit PollService(QObject *parent = 0);
1275+ ~PollService();
1276+
1277+public Q_SLOTS:
1278+ void Poll();
1279+
1280+Q_SIGNALS:
1281+ void Done();
1282+
1283+private:
1284+ PollServicePrivate *d_ptr;
1285+ Q_DECLARE_PRIVATE(PollService)
1286+};
1287+
1288+} // namespace
1289+
1290+#endif // AP_POLL_SERVICE_H
1291
1292=== added file 'account-polld/push_client.cpp'
1293--- account-polld/push_client.cpp 1970-01-01 00:00:00 +0000
1294+++ account-polld/push_client.cpp 2016-09-21 07:16:22 +0000
1295@@ -0,0 +1,112 @@
1296+/*
1297+ * Copyright (C) 2016 Canonical Ltd.
1298+ *
1299+ * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
1300+ *
1301+ * This file is part of account-polld
1302+ *
1303+ * This program is free software: you can redistribute it and/or modify it
1304+ * under the terms of the GNU General Public License version 3, as published
1305+ * by the Free Software Foundation.
1306+ *
1307+ * This program is distributed in the hope that it will be useful, but
1308+ * WITHOUT ANY WARRANTY; without even the implied warranties of
1309+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1310+ * PURPOSE. See the GNU General Public License for more details.
1311+ *
1312+ * You should have received a copy of the GNU General Public License along
1313+ * with this program. If not, see <http://www.gnu.org/licenses/>.
1314+ */
1315+
1316+#include "push_client.h"
1317+
1318+#include "debug.h"
1319+
1320+#include <QByteArray>
1321+#include <QDBusConnection>
1322+#include <QDBusMessage>
1323+#include <QJsonDocument>
1324+#include <QJsonObject>
1325+
1326+using namespace AccountPolld;
1327+
1328+namespace AccountPolld {
1329+
1330+class PushClientPrivate: public QObject
1331+{
1332+ Q_OBJECT
1333+ Q_DECLARE_PUBLIC(PushClient)
1334+
1335+public:
1336+ PushClientPrivate(PushClient *q);
1337+ ~PushClientPrivate() {};
1338+
1339+ static QByteArray makeObjectPath(const QString &appId);
1340+
1341+private:
1342+ QDBusConnection m_conn;
1343+ PushClient *q_ptr;
1344+};
1345+
1346+} // namespace
1347+
1348+PushClientPrivate::PushClientPrivate(PushClient *q):
1349+ QObject(q),
1350+ m_conn(QDBusConnection::sessionBus()),
1351+ q_ptr(q)
1352+{
1353+}
1354+
1355+QByteArray PushClientPrivate::makeObjectPath(const QString &appId)
1356+{
1357+ QByteArray path(QByteArrayLiteral("/com/ubuntu/Postal/"));
1358+
1359+ QByteArray pkg = appId.split('_').first().toUtf8();
1360+ for (int i = 0; i < pkg.count(); i++) {
1361+ char buffer[10];
1362+ char c = pkg[i];
1363+ switch (c) {
1364+ case '+':
1365+ case '.':
1366+ case '-':
1367+ case ':':
1368+ case '~':
1369+ case '_':
1370+ sprintf(buffer, "_%.2x", c);
1371+ path += buffer;
1372+ break;
1373+ default:
1374+ path += c;
1375+ }
1376+ }
1377+ return path;
1378+}
1379+
1380+PushClient::PushClient(QObject *parent):
1381+ QObject(parent),
1382+ d_ptr(new PushClientPrivate(this))
1383+{
1384+}
1385+
1386+PushClient::~PushClient()
1387+{
1388+ delete d_ptr;
1389+}
1390+
1391+void PushClient::post(const QString &appId, const QJsonObject &message)
1392+{
1393+ Q_D(PushClient);
1394+
1395+ QByteArray objectPath = d->makeObjectPath(appId);
1396+ QDBusMessage msg = QDBusMessage::createMethodCall("com.ubuntu.Postal",
1397+ objectPath,
1398+ "com.ubuntu.Postal",
1399+ "Post");
1400+ msg << appId;
1401+ QByteArray data = QJsonDocument(message).toJson(QJsonDocument::Compact);
1402+ msg << QString::fromUtf8(data);
1403+
1404+ d->m_conn.send(msg);
1405+}
1406+
1407+#include "push_client.moc"
1408
1409=== added file 'account-polld/push_client.h'
1410--- account-polld/push_client.h 1970-01-01 00:00:00 +0000
1411+++ account-polld/push_client.h 2016-09-21 07:16:22 +0000
1412@@ -0,0 +1,48 @@
1413+/*
1414+ * Copyright (C) 2016 Canonical Ltd.
1415+ *
1416+ * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
1417+ *
1418+ * This file is part of account-polld
1419+ *
1420+ * This program is free software: you can redistribute it and/or modify it
1421+ * under the terms of the GNU General Public License version 3, as published
1422+ * by the Free Software Foundation.
1423+ *
1424+ * This program is distributed in the hope that it will be useful, but
1425+ * WITHOUT ANY WARRANTY; without even the implied warranties of
1426+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1427+ * PURPOSE. See the GNU General Public License for more details.
1428+ *
1429+ * You should have received a copy of the GNU General Public License along
1430+ * with this program. If not, see <http://www.gnu.org/licenses/>.
1431+ */
1432+
1433+#ifndef AP_PUSH_CLIENT_H
1434+#define AP_PUSH_CLIENT_H
1435+
1436+#include <QObject>
1437+
1438+class QJsonObject;
1439+
1440+namespace AccountPolld {
1441+
1442+class PushClientPrivate;
1443+class PushClient: public QObject
1444+{
1445+ Q_OBJECT
1446+
1447+public:
1448+ explicit PushClient(QObject *parent = 0);
1449+ ~PushClient();
1450+
1451+ void post(const QString &appId, const QJsonObject &message);
1452+
1453+private:
1454+ PushClientPrivate *d_ptr;
1455+ Q_DECLARE_PRIVATE(PushClient)
1456+};
1457+
1458+} // namespace
1459+
1460+#endif // AP_PUSH_CLIENT_H
1461
1462=== removed directory 'accounts'
1463=== removed file 'accounts/account-watcher.c'
1464--- accounts/account-watcher.c 2016-08-02 14:34:52 +0000
1465+++ accounts/account-watcher.c 1970-01-01 00:00:00 +0000
1466@@ -1,302 +0,0 @@
1467-/*
1468- Copyright 2014 Canonical Ltd.
1469-
1470- This program is free software: you can redistribute it and/or modify it
1471- under the terms of the GNU General Public License version 3, as published
1472- by the Free Software Foundation.
1473-
1474- This program is distributed in the hope that it will be useful, but
1475- WITHOUT ANY WARRANTY; without even the implied warranties of
1476- MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1477- PURPOSE. See the GNU General Public License for more details.
1478-
1479- You should have received a copy of the GNU General Public License along
1480- with this program. If not, see <http://www.gnu.org/licenses/>.
1481-*/
1482-#include <stdio.h>
1483-
1484-#include <glib.h>
1485-#include <libaccounts-glib/accounts-glib.h>
1486-#include <libsignon-glib/signon-glib.h>
1487-
1488-#include "account-watcher.h"
1489-
1490-/* #define DEBUG */
1491-#ifdef DEBUG
1492-# define trace(...) fprintf(stderr, __VA_ARGS__)
1493-#else
1494-# define trace(...)
1495-#endif
1496-
1497-struct _AccountWatcher {
1498- AgManager *manager;
1499- /* A hash table of the enabled accounts we know of.
1500- * Keys are "<accountId>/<serviceName>", and AccountInfo structs as values.
1501- */
1502- GHashTable *services;
1503-
1504- /* List of supported services' IDs */
1505- GSList *supported_services;
1506-
1507- AccountEnabledCallback callback;
1508- void *user_data;
1509-};
1510-
1511-typedef struct _AccountInfo AccountInfo;
1512-struct _AccountInfo {
1513- AccountWatcher *watcher;
1514- /* Manage signin session for account */
1515- AgAccountService *account_service;
1516- SignonAuthSession *session;
1517- GVariant *auth_params;
1518- GVariant *session_data;
1519-
1520- AgAccountId account_id;
1521-};
1522-
1523-static void account_info_clear_login(AccountInfo *info) {
1524- if (info->session_data) {
1525- g_variant_unref(info->session_data);
1526- info->session_data = NULL;
1527- }
1528- if (info->auth_params) {
1529- g_variant_unref(info->auth_params);
1530- info->auth_params = NULL;
1531- }
1532- if (info->session) {
1533- signon_auth_session_cancel(info->session);
1534- g_object_unref(info->session);
1535- info->session = NULL;
1536- }
1537-}
1538-
1539-static void account_info_free(AccountInfo *info) {
1540- account_info_clear_login(info);
1541- if (info->account_service) {
1542- g_object_unref(info->account_service);
1543- info->account_service = NULL;
1544- }
1545- g_free(info);
1546-}
1547-
1548-static void account_info_notify(AccountInfo *info, GError *error) {
1549- AgService *service = ag_account_service_get_service(info->account_service);
1550- const char *service_name = ag_service_get_name(service);
1551- const char *service_type = ag_service_get_service_type(service);
1552- char *client_id = NULL;
1553- char *client_secret = NULL;
1554- char *access_token = NULL;
1555- char *token_secret = NULL;
1556- char *secret = NULL;
1557- char *user_name = NULL;
1558-
1559- if (info->auth_params != NULL) {
1560- /* Look up OAuth 2 parameters, falling back to OAuth 1 names */
1561- g_variant_lookup(info->auth_params, "ClientId", "&s", &client_id);
1562- g_variant_lookup(info->auth_params, "ClientSecret", "&s", &client_secret);
1563- if (client_id == NULL) {
1564- g_variant_lookup(info->auth_params, "ConsumerKey", "&s", &client_id);
1565- }
1566- if (client_secret == NULL) {
1567- g_variant_lookup(info->auth_params, "ConsumerSecret", "&s", &client_secret);
1568- }
1569- }
1570- if (info->session_data != NULL) {
1571- g_variant_lookup(info->session_data, "AccessToken", "&s", &access_token);
1572- g_variant_lookup(info->session_data, "TokenSecret", "&s", &token_secret);
1573- g_variant_lookup(info->session_data, "Secret", "&s", &secret);
1574- g_variant_lookup(info->session_data, "UserName", "&s", &user_name);
1575- }
1576-
1577- info->watcher->callback(info->watcher,
1578- info->account_id,
1579- service_type,
1580- service_name,
1581- error,
1582- TRUE,
1583- client_id,
1584- client_secret,
1585- access_token,
1586- token_secret,
1587- user_name,
1588- secret,
1589- info->watcher->user_data);
1590-}
1591-
1592-static void account_info_login_cb(GObject *source, GAsyncResult *result, void *user_data) {
1593- SignonAuthSession *session = (SignonAuthSession *)source;
1594- AccountInfo *info = (AccountInfo *)user_data;
1595-
1596- trace("Authentication for account %u complete\n", info->account_id);
1597-
1598- GError *error = NULL;
1599- info->session_data = signon_auth_session_process_finish(session, result, &error);
1600- account_info_notify(info, error);
1601-
1602- if (error != NULL) {
1603- trace("Authentication failed: %s\n", error->message);
1604- g_error_free(error);
1605- }
1606-}
1607-
1608-static void account_info_login(AccountInfo *info) {
1609- account_info_clear_login(info);
1610-
1611- AgAuthData *auth_data = ag_account_service_get_auth_data(info->account_service);
1612- GError *error = NULL;
1613- trace("Starting authentication session for account %u\n", info->account_id);
1614- info->session = signon_auth_session_new(
1615- ag_auth_data_get_credentials_id(auth_data),
1616- ag_auth_data_get_method(auth_data), &error);
1617- if (error != NULL) {
1618- trace("Could not set up auth session: %s\n", error->message);
1619- account_info_notify(info, error);
1620- g_error_free(error);
1621- g_object_unref(auth_data);
1622- return;
1623- }
1624-
1625- /* Tell libsignon-glib not to open a trust session as we have no UI */
1626- GVariantBuilder builder;
1627- g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
1628- g_variant_builder_add(&builder, "{sv}",
1629- SIGNON_SESSION_DATA_UI_POLICY,
1630- g_variant_new_int32(SIGNON_POLICY_NO_USER_INTERACTION));
1631-
1632- info->auth_params = g_variant_ref_sink(
1633- ag_auth_data_get_login_parameters(
1634- auth_data,
1635- g_variant_builder_end(&builder)));
1636-
1637- signon_auth_session_process_async(
1638- info->session,
1639- info->auth_params,
1640- ag_auth_data_get_mechanism(auth_data),
1641- NULL, /* cancellable */
1642- account_info_login_cb, info);
1643- ag_auth_data_unref(auth_data);
1644-}
1645-
1646-static AccountInfo *account_info_new(AccountWatcher *watcher, AgAccountService *account_service) {
1647- AccountInfo *info = g_new0(AccountInfo, 1);
1648- info->watcher = watcher;
1649- info->account_service = g_object_ref(account_service);
1650-
1651- AgAccount *account = ag_account_service_get_account(account_service);
1652- g_object_get(account, "id", &info->account_id, NULL);
1653-
1654- return info;
1655-}
1656-
1657-static gboolean service_is_supported(AccountWatcher *watcher,
1658- const char *service_id)
1659-{
1660- GSList *node = g_slist_find_custom(watcher->supported_services,
1661- service_id,
1662- (GCompareFunc)g_strcmp0);
1663- return node != NULL;
1664-}
1665-
1666-static gboolean account_watcher_setup(void *user_data) {
1667- AccountWatcher *watcher = (AccountWatcher *)user_data;
1668-
1669- /* Now check initial state */
1670- GList *enabled_accounts =
1671- ag_manager_get_enabled_account_services(watcher->manager);
1672- GList *old_services = g_hash_table_get_keys(watcher->services);
1673-
1674- /* Update the services table */
1675- GList *l;
1676- for (l = enabled_accounts; l != NULL; l = l->next) {
1677- AgAccountService *account_service = l->data;
1678- AgAccountId id = ag_account_service_get_account(account_service)->id;
1679- AgService *service = ag_account_service_get_service(account_service);
1680- const char *service_id = ag_service_get_name(service);
1681-
1682- if (!service_is_supported(watcher, service_id)) continue;
1683-
1684- char *key = g_strdup_printf("%d/%s", id, service_id);
1685-
1686- AccountInfo *info = g_hash_table_lookup(watcher->services, key);
1687- if (info) {
1688- GList *node = g_list_find_custom(old_services, key,
1689- (GCompareFunc)g_strcmp0);
1690- old_services = g_list_remove_link(old_services, node);
1691- g_free(key);
1692- } else {
1693- trace("adding account %s\n", key);
1694- info = account_info_new(watcher, account_service);
1695- g_hash_table_insert(watcher->services, key, info);
1696- }
1697- account_info_login(info);
1698- }
1699- g_list_free_full(enabled_accounts, g_object_unref);
1700-
1701- /* Remove from the table the accounts which are no longer enabled */
1702- for (l = old_services; l != NULL; l = l->next) {
1703- char *key = l->data;
1704- trace("removing account %s\n", key);
1705- g_hash_table_remove(watcher->services, key);
1706- }
1707- g_list_free(old_services);
1708-
1709- return G_SOURCE_REMOVE;
1710-}
1711-
1712-AccountWatcher *account_watcher_new(AccountEnabledCallback callback,
1713- void *user_data) {
1714- AccountWatcher *watcher = g_new0(AccountWatcher, 1);
1715-
1716- watcher->manager = ag_manager_new();
1717- watcher->services = g_hash_table_new_full(
1718- g_str_hash, g_str_equal, g_free, (GDestroyNotify)account_info_free);
1719- watcher->supported_services = NULL;
1720- watcher->callback = callback;
1721- watcher->user_data = user_data;
1722-
1723- return watcher;
1724-}
1725-
1726-void account_watcher_add_service(AccountWatcher *watcher,
1727- char *serviceId) {
1728- watcher->supported_services =
1729- g_slist_prepend(watcher->supported_services, serviceId);
1730-}
1731-
1732-void account_watcher_run(AccountWatcher *watcher) {
1733- /* Make sure main setup occurs within the mainloop thread */
1734- g_idle_add(account_watcher_setup, watcher);
1735-}
1736-
1737-struct refresh_info {
1738- AccountWatcher *watcher;
1739- AgAccountId account_id;
1740- char *service_name;
1741-};
1742-
1743-static void refresh_info_free(struct refresh_info *data) {
1744- g_free(data->service_name);
1745- g_free(data);
1746-}
1747-
1748-static gboolean account_watcher_refresh_cb(void *user_data) {
1749- struct refresh_info *data = (struct refresh_info *)user_data;
1750-
1751- char *key = g_strdup_printf("%d/%s", data->account_id, data->service_name);
1752- AccountInfo *info = g_hash_table_lookup(data->watcher->services, key);
1753- if (info != NULL) {
1754- account_info_login(info);
1755- }
1756-
1757- return G_SOURCE_REMOVE;
1758-}
1759-
1760-void account_watcher_refresh(AccountWatcher *watcher, unsigned int account_id,
1761- const char *service_name) {
1762- struct refresh_info *data = g_new(struct refresh_info, 1);
1763- data->watcher = watcher;
1764- data->account_id = account_id;
1765- data->service_name = g_strdup(service_name);
1766- g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, account_watcher_refresh_cb,
1767- data, (GDestroyNotify)refresh_info_free);
1768-}
1769
1770=== removed file 'accounts/account-watcher.h'
1771--- accounts/account-watcher.h 2016-08-02 14:34:52 +0000
1772+++ accounts/account-watcher.h 1970-01-01 00:00:00 +0000
1773@@ -1,47 +0,0 @@
1774-/*
1775- Copyright 2014 Canonical Ltd.
1776-
1777- This program is free software: you can redistribute it and/or modify it
1778- under the terms of the GNU General Public License version 3, as published
1779- by the Free Software Foundation.
1780-
1781- This program is distributed in the hope that it will be useful, but
1782- WITHOUT ANY WARRANTY; without even the implied warranties of
1783- MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1784- PURPOSE. See the GNU General Public License for more details.
1785-
1786- You should have received a copy of the GNU General Public License along
1787- with this program. If not, see <http://www.gnu.org/licenses/>.
1788-*/
1789-#ifndef ACCOUNT_WATCHER_H
1790-#define ACCOUNT_WATCHER_H
1791-
1792-#include <glib.h>
1793-
1794-typedef struct _AccountWatcher AccountWatcher;
1795-
1796-typedef void (*AccountEnabledCallback)(AccountWatcher *watcher,
1797- unsigned int account_id,
1798- const char *service_type,
1799- const char *service_name,
1800- GError *error,
1801- int enabled,
1802- const char *client_id,
1803- const char *client_secret,
1804- const char *access_token,
1805- const char *token_secret,
1806- const char *user_name,
1807- const char *secret,
1808- void *user_data);
1809-
1810-AccountWatcher *account_watcher_new(AccountEnabledCallback callback,
1811- void *user_data);
1812-void account_watcher_add_service(AccountWatcher *watcher,
1813- char *serviceId);
1814-void account_watcher_run(AccountWatcher *watcher);
1815-
1816-void account_watcher_refresh(AccountWatcher *watcher,
1817- unsigned int account_id,
1818- const char *service_name);
1819-
1820-#endif
1821
1822=== removed file 'accounts/accounts.c'
1823--- accounts/accounts.c 2016-08-02 14:34:52 +0000
1824+++ accounts/accounts.c 1970-01-01 00:00:00 +0000
1825@@ -1,26 +0,0 @@
1826-#include "_cgo_export.h"
1827-
1828-AccountWatcher *watch() {
1829- /* Transfer service names to hash table */
1830- if (FALSE) {
1831- /* The Go callback doesn't quite match the
1832- * AccountEnabledCallback function prototype, so we cast the
1833- * argument in the account_watcher_new() call below.
1834- *
1835- * This is just a check to see that the function still has the
1836- * prototype we expect.
1837- */
1838- void (*unused)(void *watcher,
1839- unsigned int account_id,
1840- char *service_type, char *service_name,
1841- GError *error, int enabled,
1842- char *client_id, char *client_secret,
1843- char *access_token, char *token_secret,
1844- char *user_name, char *secret,
1845- void *user_data) = authCallback;
1846- }
1847-
1848- AccountWatcher *watcher = account_watcher_new(
1849- (AccountEnabledCallback)authCallback, NULL);
1850- return watcher;
1851-}
1852
1853=== removed file 'accounts/accounts.go'
1854--- accounts/accounts.go 2016-08-02 14:34:52 +0000
1855+++ accounts/accounts.go 1970-01-01 00:00:00 +0000
1856@@ -1,130 +0,0 @@
1857-/*
1858- Copyright 2014 Canonical Ltd.
1859-
1860- This program is free software: you can redistribute it and/or modify it
1861- under the terms of the GNU General Public License version 3, as published
1862- by the Free Software Foundation.
1863-
1864- This program is distributed in the hope that it will be useful, but
1865- WITHOUT ANY WARRANTY; without even the implied warranties of
1866- MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1867- PURPOSE. See the GNU General Public License for more details.
1868-
1869- You should have received a copy of the GNU General Public License along
1870- with this program. If not, see <http://www.gnu.org/licenses/>.
1871-*/
1872-
1873-package accounts
1874-
1875-/*
1876-#cgo pkg-config: glib-2.0 libaccounts-glib libsignon-glib
1877-#include <stdlib.h>
1878-#include <glib.h>
1879-#include "account-watcher.h"
1880-
1881-AccountWatcher *watch();
1882-*/
1883-import "C"
1884-import (
1885- "errors"
1886- "sync"
1887- "unsafe"
1888-)
1889-
1890-type Watcher struct {
1891- C <-chan AuthData
1892- watcher *C.AccountWatcher
1893-}
1894-
1895-type AuthData struct {
1896- AccountId uint
1897- ServiceName string
1898- ServiceType string
1899- Error error
1900- Enabled bool
1901-
1902- ClientId string
1903- ClientSecret string
1904- AccessToken string
1905- TokenSecret string
1906- Secret string
1907- UserName string
1908-}
1909-
1910-var (
1911- authChannels = make(map[*C.AccountWatcher]chan<- AuthData)
1912- authChannelsLock sync.Mutex
1913-)
1914-
1915-// NewWatcher creates a new account watcher
1916-func NewWatcher() *Watcher {
1917- w := new(Watcher)
1918- w.watcher = C.watch()
1919-
1920- ch := make(chan AuthData)
1921- w.C = ch
1922- authChannelsLock.Lock()
1923- authChannels[w.watcher] = ch
1924- authChannelsLock.Unlock()
1925-
1926- return w
1927-}
1928-
1929-func (w *Watcher) AddService(serviceId string) {
1930- C.account_watcher_add_service(w.watcher, C.CString(serviceId))
1931-}
1932-
1933-// Walk through the enabled accounts, and get auth tokens for each of them.
1934-// The new access token will be delivered over the watcher's channel.
1935-func (w *Watcher) Run() {
1936- C.account_watcher_run(w.watcher)
1937-}
1938-
1939-// Refresh requests that the token for the given account be refreshed.
1940-// The new access token will be delivered over the watcher's channel.
1941-func (w *Watcher) Refresh(accountId uint, serviceName string) {
1942- C.account_watcher_refresh(w.watcher, C.uint(accountId), C.CString(serviceName))
1943-}
1944-
1945-//export authCallback
1946-func authCallback(watcher unsafe.Pointer, accountId C.uint, serviceType *C.char, serviceName *C.char, error *C.GError, enabled C.int, clientId, clientSecret, accessToken, tokenSecret *C.char, userName *C.char, secret *C.char, userData unsafe.Pointer) {
1947- // Ideally the first argument would be of type
1948- // *C.AccountWatcher, but that fails with Go 1.2.
1949- authChannelsLock.Lock()
1950- ch := authChannels[(*C.AccountWatcher)(watcher)]
1951- authChannelsLock.Unlock()
1952- if ch == nil {
1953- // Log the error
1954- return
1955- }
1956-
1957- var data AuthData
1958- data.AccountId = uint(accountId)
1959- data.ServiceName = C.GoString(serviceName)
1960- data.ServiceType = C.GoString(serviceType)
1961- if error != nil {
1962- data.Error = errors.New(C.GoString((*C.char)(error.message)))
1963- }
1964- if enabled != 0 {
1965- data.Enabled = true
1966- }
1967- if clientId != nil {
1968- data.ClientId = C.GoString(clientId)
1969- }
1970- if clientSecret != nil {
1971- data.ClientSecret = C.GoString(clientSecret)
1972- }
1973- if accessToken != nil {
1974- data.AccessToken = C.GoString(accessToken)
1975- }
1976- if tokenSecret != nil {
1977- data.TokenSecret = C.GoString(tokenSecret)
1978- }
1979- if secret != nil {
1980- data.Secret = C.GoString(secret)
1981- }
1982- if userName != nil {
1983- data.UserName = C.GoString(userName)
1984- }
1985- ch <- data
1986-}
1987
1988=== added directory 'click-hook'
1989=== added file 'click-hook/account-polld.hook.in'
1990--- click-hook/account-polld.hook.in 1970-01-01 00:00:00 +0000
1991+++ click-hook/account-polld.hook.in 2016-09-21 07:16:22 +0000
1992@@ -0,0 +1,4 @@
1993+Pattern: ${home}/.local/share/account-polld/plugins/${id}.json
1994+User-Level: yes
1995+Hook-Name: account-polld
1996+Exec: $${hook_helper.path}/$${hook_helper.files}
1997
1998=== added file 'click-hook/click-hook'
1999--- click-hook/click-hook 1970-01-01 00:00:00 +0000
2000+++ click-hook/click-hook 2016-09-21 07:16:22 +0000
2001@@ -0,0 +1,108 @@
2002+#!/usr/bin/python3
2003+# -*- python -*-
2004+"""Collect helpers hook data into a single json file"""
2005+
2006+import json
2007+import os
2008+import sys
2009+import time
2010+
2011+import xdg.BaseDirectory
2012+
2013+hook_ext = '.json'
2014+
2015+
2016+class HookProcessor:
2017+ def __init__(self):
2018+ self.xdg_data_home = xdg.BaseDirectory.xdg_data_home
2019+ self.xdg_data_dirs = xdg.BaseDirectory.xdg_data_dirs
2020+ self.plugins_data_path = os.path.join(self.xdg_data_home, 'account-polld',
2021+ 'plugin_data.json')
2022+ self.plugins_data_path_tmp = os.path.join(self.xdg_data_home, 'account-polld',
2023+ '.plugin_data_%s.tmp')
2024+ os.makedirs(os.path.join(self.xdg_data_home, 'account-polld'), exist_ok=True)
2025+
2026+
2027+ def write_plugin_data(self):
2028+ plugin_data = {}
2029+ for path in self.xdg_data_dirs:
2030+ data = self.collect_plugins(path)
2031+ plugin_data.update(data)
2032+
2033+ # write the collected data to a temp file and rename the original once
2034+ # everything is on disk
2035+ try:
2036+ tmp_filename = self.plugins_data_path_tmp % (time.time(),)
2037+ with open(tmp_filename, 'w') as dest:
2038+ json.dump(plugin_data, dest)
2039+ dest.flush()
2040+ os.rename(tmp_filename, self.plugins_data_path)
2041+ except Exception as e:
2042+ print('Writing file %s failed: %s' % (self.plugins_data_path, e), file=sys.stderr)
2043+ return False
2044+ return True
2045+
2046+
2047+ def collect_plugins(self, base_path):
2048+ trusted = False if base_path == self.xdg_data_home else True
2049+ hooks_path = os.path.join(base_path, 'account-polld', 'plugins')
2050+ plugins_data = {}
2051+ if not os.path.isdir(hooks_path):
2052+ return plugins_data
2053+ for hook_fname in os.listdir(hooks_path):
2054+ if not hook_fname.endswith(hook_ext):
2055+ continue
2056+ try:
2057+ with open(os.path.join(hooks_path, hook_fname), 'r') as fd:
2058+ data = json.load(fd)
2059+ except Exception:
2060+ print('Unable to parse JSON from %s' % (hook_fname,), file=sys.stderr)
2061+ continue
2062+
2063+ helper_id = os.path.splitext(hook_fname)[0]
2064+ profile = 'unconfined' if trusted else helper_id
2065+ if helper_id.count('_') == 2:
2066+ helper_short_id = '_'.join(helper_id.split('_')[0:2])
2067+ else:
2068+ helper_short_id = helper_id
2069+
2070+ exec_path = data['exec']
2071+ if exec_path != "":
2072+ realpath = os.path.realpath(os.path.join(hooks_path,
2073+ hook_fname))
2074+ exec_path = os.path.join(os.path.dirname(realpath), exec_path)
2075+ app_id = data.get('app_id', None)
2076+ if app_id is None:
2077+ # no app_id, use the package name from the helper_id
2078+ app_id = helper_short_id
2079+ elif app_id.count('_') >= 2:
2080+ # remove the version from the app_id
2081+ app_id = '_'.join(app_id.split('_')[0:2])
2082+ if not trusted:
2083+ # check that the plugin comes from the same package as the app
2084+ plugin_package = helper_id.split('_')[0]
2085+ app_package = app_id.split('_')[0]
2086+ if plugin_package != app_package:
2087+ print('Skipping %s as it\'s unrelated to package %s' % (hook_fname, app_package), file=sys.stderr)
2088+ continue
2089+
2090+ parsed = {
2091+ 'exec': exec_path,
2092+ 'appId': app_id,
2093+ 'profile': profile,
2094+ }
2095+ parsed['needsAuthData'] = data.get('needs_authentication_data', False)
2096+ if 'service_ids' in data:
2097+ parsed['services'] = data['service_ids']
2098+ if 'interval' in data:
2099+ parsed['interval'] = data['interval']
2100+ plugins_data[helper_short_id] = parsed
2101+
2102+ return plugins_data
2103+
2104+
2105+if __name__ == "__main__":
2106+ processor = HookProcessor()
2107+ ok = processor.write_plugin_data()
2108+
2109+ sys.exit(0 if ok else 1)
2110
2111=== added file 'click-hook/click-hook.pro'
2112--- click-hook/click-hook.pro 1970-01-01 00:00:00 +0000
2113+++ click-hook/click-hook.pro 2016-09-21 07:16:22 +0000
2114@@ -0,0 +1,20 @@
2115+include(../common-project-config.pri)
2116+
2117+TEMPLATE = aux
2118+TARGET = ""
2119+
2120+QMAKE_SUBSTITUTES += \
2121+ account-polld.hook.in
2122+
2123+OTHER_FILES += \
2124+ click-hook
2125+
2126+hook_helper.files = \
2127+ click-hook
2128+hook_helper.path = $${INSTALL_PREFIX}/lib/account-polld
2129+INSTALLS += hook_helper
2130+
2131+hooks.files = \
2132+ account-polld.hook
2133+hooks.path = $${INSTALL_PREFIX}/share/click/hooks
2134+INSTALLS += hooks
2135
2136=== removed directory 'cmd'
2137=== removed directory 'cmd/account-polld'
2138=== removed file 'cmd/account-polld/account_service.go'
2139--- cmd/account-polld/account_service.go 2016-08-02 14:34:52 +0000
2140+++ cmd/account-polld/account_service.go 1970-01-01 00:00:00 +0000
2141@@ -1,186 +0,0 @@
2142-/*
2143- Copyright 2014 Canonical Ltd.
2144-
2145- This program is free software: you can redistribute it and/or modify it
2146- under the terms of the GNU General Public License version 3, as published
2147- by the Free Software Foundation.
2148-
2149- This program is distributed in the hope that it will be useful, but
2150- WITHOUT ANY WARRANTY; without even the implied warranties of
2151- MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
2152- PURPOSE. See the GNU General Public License for more details.
2153-
2154- You should have received a copy of the GNU General Public License along
2155- with this program. If not, see <http://www.gnu.org/licenses/>.
2156-*/
2157-
2158-package main
2159-
2160-import (
2161- "errors"
2162- "log"
2163- "time"
2164-
2165- "launchpad.net/account-polld/accounts"
2166- "launchpad.net/account-polld/plugins"
2167- "launchpad.net/ubuntu-push/click"
2168-)
2169-
2170-type AccountService struct {
2171- watcher *accounts.Watcher
2172- authData accounts.AuthData
2173- plugin plugins.Plugin
2174- interval time.Duration
2175- postWatch chan *PostWatch
2176- authChan chan accounts.AuthData
2177- doneChan chan error
2178- penaltyCount int
2179- authFailureCount int
2180-}
2181-
2182-var (
2183- pollTimeout = time.Duration(30 * time.Second)
2184- bootstrapPollTimeout = time.Duration(4 * time.Minute)
2185- maxCounter = 4
2186- authTriesUntilPenalty = 3
2187- authFailurePenalty = 10
2188-)
2189-
2190-var (
2191- authError = errors.New("Skipped account")
2192- clickNotInstalledError = errors.New("Click not installed")
2193-)
2194-
2195-func NewAccountService(watcher *accounts.Watcher, postWatch chan *PostWatch, plugin plugins.Plugin) *AccountService {
2196- return &AccountService{
2197- watcher: watcher,
2198- plugin: plugin,
2199- postWatch: postWatch,
2200- authChan: make(chan accounts.AuthData, 1),
2201- doneChan: make(chan error, 1),
2202- }
2203-}
2204-
2205-func (a *AccountService) Delete() {
2206- close(a.authChan)
2207- close(a.doneChan)
2208-}
2209-
2210-// Poll() always needs to be called asynchronously as otherwise qtcontacs' GetAvatar()
2211-// will raise an error: "QSocketNotifier: Can only be used with threads started with QThread"
2212-func (a *AccountService) Poll(bootstrap bool) {
2213- gotNewAuthData := false
2214- if a.authData, gotNewAuthData = <-a.authChan; !gotNewAuthData {
2215- log.Println("Account", a.authData.AccountId, "no longer enabled")
2216- return
2217- }
2218-
2219- if a.penaltyCount > 0 {
2220- log.Printf("Leaving poll for account %d as penalty count is %d", a.authData.AccountId, a.penaltyCount)
2221- a.penaltyCount--
2222- return
2223- } else if !gotNewAuthData && a.authData.Error != nil {
2224- // Retry to poll the account with a previous auth failure as that results in reauthentication in case of token expiry and in ignoring temporary network issues
2225- log.Println("Retrying to poll account with previous auth failure and id", a.authData.AccountId, "(results in reauthentication in case of token expiry and in ignoring temporary network issues)")
2226- a.authData.Error = nil
2227- }
2228-
2229- timeout := pollTimeout
2230- if bootstrap {
2231- timeout = bootstrapPollTimeout
2232- }
2233-
2234- log.Printf("Starting poll for account %d", a.authData.AccountId)
2235- go a.poll()
2236-
2237- select {
2238- case <-time.After(timeout):
2239- log.Println("Poll for account", a.authData.AccountId, "has timed out out after", timeout)
2240- a.penaltyCount++
2241- case err := <-a.doneChan:
2242- if err == nil {
2243- log.Println("Poll for account", a.authData.AccountId, "was successful")
2244- a.authFailureCount = 0
2245- a.penaltyCount = 0
2246- } else {
2247- if err != clickNotInstalledError && err != authError { // Do not log the error twice
2248- log.Println("Poll for account", a.authData.AccountId, "has failed:", err)
2249- }
2250- if err == authError || err == plugins.ErrTokenExpired {
2251- // Increase the authFailureCount counter, except for when we did a poll which
2252- // raised a token expiry error when we did not get any new auth data this time.
2253- if err != plugins.ErrTokenExpired || gotNewAuthData {
2254- log.Println("Increasing the auth failure counter for account", a.authData.AccountId)
2255- a.authFailureCount++
2256- } else {
2257- log.Println("Not increasing the auth failure counter for account", a.authData.AccountId, "as we do not have new auth data")
2258- }
2259- if a.authFailureCount >= authTriesUntilPenalty {
2260- a.penaltyCount = authFailurePenalty
2261- a.authFailureCount = 0
2262- log.Println(authTriesUntilPenalty, "auth failures in a row for account", a.authData.AccountId, "-> skipping it for the next", a.penaltyCount, "poll cycles")
2263- } else if err == plugins.ErrTokenExpired && !gotNewAuthData {
2264- // If the error indicates that the authentication token has expired, request reauthentication
2265- // and mark the data as disabled.
2266- // Do not refresh immediately when we just got new (faulty) auth data as immediately trying
2267- // again is probably not going to help. Instead, we wait for the next poll cycle.
2268- a.watcher.Refresh(a.authData.AccountId, a.authData.ServiceName)
2269- a.authData.Enabled = false
2270- a.authData.Error = err
2271- }
2272- } else if a.penaltyCount < maxCounter {
2273- a.authFailureCount = 0
2274- a.penaltyCount++
2275- }
2276- }
2277- }
2278- log.Printf("Ending poll for account %d", a.authData.AccountId)
2279-}
2280-
2281-func (a *AccountService) poll() {
2282- log.Println("Polling account", a.authData.AccountId)
2283- if !isClickInstalled(a.plugin.ApplicationId()) {
2284- log.Println(
2285- "Skipping account", a.authData.AccountId, "as target click",
2286- a.plugin.ApplicationId(), "is not installed")
2287- a.doneChan <- clickNotInstalledError
2288- return
2289- }
2290-
2291- if a.authData.Error != nil {
2292- log.Println("Account", a.authData.AccountId, "failed to authenticate:", a.authData.Error)
2293- a.doneChan <- authError
2294- return
2295- }
2296-
2297- if bs, err := a.plugin.Poll(&a.authData); err != nil {
2298- log.Print("Error while polling ", a.authData.AccountId, ": ", err)
2299- a.doneChan <- err
2300- } else {
2301- for _, b := range bs {
2302- log.Println("Account", a.authData.AccountId, "has", len(b.Messages), b.Tag, "updates to report")
2303- }
2304- a.postWatch <- &PostWatch{batches: bs, appId: a.plugin.ApplicationId()}
2305- a.doneChan <- nil
2306- }
2307-}
2308-
2309-func (a *AccountService) updateAuthData(authData accounts.AuthData) {
2310- a.authChan <- authData
2311-}
2312-
2313-func isClickInstalled(appId plugins.ApplicationId) bool {
2314- user, err := click.User()
2315- if err != nil {
2316- log.Println("User instance for click cannot be created to determine if click application", appId, "was installed")
2317- return false
2318- }
2319-
2320- app, err := click.ParseAppId(string(appId))
2321- if err != nil {
2322- log.Println("Could not parse APP_ID for", appId)
2323- return false
2324- }
2325-
2326- return user.Installed(app, false)
2327-}
2328
2329=== removed file 'cmd/account-polld/main.go'
2330--- cmd/account-polld/main.go 2016-08-02 14:34:52 +0000
2331+++ cmd/account-polld/main.go 1970-01-01 00:00:00 +0000
2332@@ -1,260 +0,0 @@
2333-/*
2334- Copyright 2014 Canonical Ltd.
2335-
2336- This program is free software: you can redistribute it and/or modify it
2337- under the terms of the GNU General Public License version 3, as published
2338- by the Free Software Foundation.
2339-
2340- This program is distributed in the hope that it will be useful, but
2341- WITHOUT ANY WARRANTY; without even the implied warranties of
2342- MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
2343- PURPOSE. See the GNU General Public License for more details.
2344-
2345- You should have received a copy of the GNU General Public License along
2346- with this program. If not, see <http://www.gnu.org/licenses/>.
2347-*/
2348-
2349-package main
2350-
2351-import (
2352- "encoding/json"
2353- "fmt"
2354- "strings"
2355- "sync"
2356-
2357- "log"
2358-
2359- "launchpad.net/account-polld/accounts"
2360- "launchpad.net/account-polld/gettext"
2361- "launchpad.net/account-polld/plugins"
2362- "launchpad.net/account-polld/plugins/caldav"
2363- "launchpad.net/account-polld/plugins/dekko"
2364- "launchpad.net/account-polld/plugins/gcalendar"
2365- "launchpad.net/account-polld/plugins/gmail"
2366- "launchpad.net/account-polld/plugins/twitter"
2367- "launchpad.net/account-polld/pollbus"
2368- "launchpad.net/account-polld/qtcontact"
2369- "launchpad.net/go-dbus/v1"
2370-)
2371-
2372-type PostWatch struct {
2373- appId plugins.ApplicationId
2374- batches []*plugins.PushMessageBatch
2375-}
2376-
2377-type AccountKey struct {
2378- serviceId string
2379- accountId uint
2380-}
2381-
2382-/* Use identifiers and API keys provided by the respective webapps which are the official
2383- end points for the notifications */
2384-const (
2385- SERVICENAME_DEKKO = "dekko.dekkoproject_dekko"
2386- SERVICENAME_GMAIL = "com.ubuntu.developer.webapps.webapp-gmail_webapp-gmail"
2387- SERVICENAME_TWITTER = "com.ubuntu.developer.webapps.webapp-twitter_webapp-twitter"
2388- SERVICENAME_GCALENDAR = "google-caldav"
2389- SERVICENAME_OCALENDAR = "owncloud-caldav"
2390-)
2391-
2392-const (
2393- POSTAL_SERVICE = "com.ubuntu.Postal"
2394- POSTAL_INTERFACE = "com.ubuntu.Postal"
2395- POSTAL_OBJECT_PATH_PART = "/com/ubuntu/Postal/"
2396-)
2397-
2398-var mainLoopOnce sync.Once
2399-
2400-func init() {
2401- startMainLoop()
2402-}
2403-
2404-func startMainLoop() {
2405- mainLoopOnce.Do(func() {
2406- go qtcontact.MainLoopStart()
2407- })
2408-}
2409-
2410-func main() {
2411- // TODO NewAccount called here is just for playing purposes.
2412- postWatch := make(chan *PostWatch)
2413-
2414- // Initialize i18n
2415- gettext.SetLocale(gettext.LC_ALL, "")
2416- gettext.Textdomain("account-polld")
2417- gettext.BindTextdomain("account-polld", "/usr/share/locale")
2418-
2419- bus, err := dbus.Connect(dbus.SessionBus)
2420- if err != nil {
2421- log.Fatal("Cannot connect to bus", err)
2422- }
2423-
2424- pollBus := pollbus.New(bus)
2425- go postOffice(bus, postWatch)
2426- go monitorAccounts(postWatch, pollBus)
2427-
2428- if err := pollBus.Init(); err != nil {
2429- log.Fatal("Issue while setting up the poll bus:", err)
2430- }
2431-
2432- done := make(chan bool)
2433- <-done
2434-}
2435-
2436-func monitorAccounts(postWatch chan *PostWatch, pollBus *pollbus.PollBus) {
2437- watcher := accounts.NewWatcher()
2438- watcher.AddService(SERVICENAME_DEKKO)
2439- watcher.AddService(SERVICENAME_GMAIL)
2440- watcher.AddService(SERVICENAME_GCALENDAR)
2441- watcher.AddService(SERVICENAME_TWITTER)
2442-
2443- mgr := make(map[AccountKey]*AccountService)
2444-
2445- var wg sync.WaitGroup
2446-
2447- pullAccount := func(data accounts.AuthData) bool {
2448- accountKey := AccountKey{data.ServiceName, data.AccountId}
2449- if account, ok := mgr[accountKey]; ok {
2450- if data.Enabled {
2451- log.Println("New account data for existing account with id", data.AccountId)
2452- account.penaltyCount = 0
2453- account.updateAuthData(data)
2454- wg.Add(1)
2455- go func() {
2456- defer wg.Done()
2457- // Poll() needs to be called asynchronously as otherwise qtcontacs' GetAvatar() will
2458- // raise an error: "QSocketNotifier: Can only be used with threads started with QThread"
2459- account.Poll(false)
2460- }()
2461- // No wg.Wait() here as it would break GetAvatar() again.
2462- // Instead we have a wg.Wait() before the PollChan polling below.
2463- } else {
2464- account.Delete()
2465- delete(mgr, accountKey)
2466- }
2467- } else if data.Enabled {
2468- var plugin plugins.Plugin
2469- log.Println("Creating plugin for service: ", data.ServiceName)
2470- switch data.ServiceName {
2471- case SERVICENAME_DEKKO:
2472- log.Println("Creating account with id", data.AccountId, "for", data.ServiceName)
2473- plugin = dekko.New(data.AccountId)
2474- case SERVICENAME_GMAIL:
2475- log.Println("Creating account with id", data.AccountId, "for", data.ServiceName)
2476- plugin = gmail.New(data.AccountId)
2477- case SERVICENAME_GCALENDAR:
2478- log.Println("Creating account with id", data.AccountId, "for", data.ServiceName)
2479- plugin = gcalendar.New(data.AccountId)
2480- case SERVICENAME_TWITTER:
2481- // This is just stubbed until the plugin exists.
2482- log.Println("Creating account with id", data.AccountId, "for", data.ServiceName)
2483- plugin = twitter.New()
2484- case SERVICENAME_OCALENDAR:
2485- log.Println("Creating account with id", data.AccountId, "for", data.ServiceName)
2486- plugin = caldav.New(data.AccountId)
2487- default:
2488- log.Println("Unhandled account with id", data.AccountId, "for", data.ServiceName)
2489- return false
2490- }
2491- mgr[accountKey] = NewAccountService(watcher, postWatch, plugin)
2492- mgr[accountKey].updateAuthData(data)
2493- wg.Add(1)
2494- go func() {
2495- defer wg.Done()
2496- // Poll() needs to be called asynchronously as otherwise qtcontacs' GetAvatar() will
2497- // raise an error: "QSocketNotifier: Can only be used with threads started with QThread"
2498- mgr[accountKey].Poll(true)
2499- }()
2500- // No wg.Wait() here as it would break GetAvatar() again.
2501- // Instead we have a wg.Wait() before the PollChan polling below.
2502- }
2503- return true
2504- }
2505-
2506-L:
2507- for {
2508- select {
2509- case data := <-watcher.C:
2510- if pullAccount(data) == false {
2511- log.Println("pullAccount returned false, continuing")
2512- continue L
2513- }
2514- case <-pollBus.PollChan:
2515- wg.Wait() // Finish all running Poll() calls before potentially polling the same accounts again
2516- watcher.Run()
2517- wg.Wait()
2518- pollBus.SignalDone()
2519- }
2520- }
2521-}
2522-
2523-func postOffice(bus *dbus.Connection, postWatch chan *PostWatch) {
2524- for post := range postWatch {
2525- obj := bus.Object(POSTAL_SERVICE, pushObjectPath(post.appId))
2526-
2527- for _, batch := range post.batches {
2528-
2529- notifs := batch.Messages
2530- overflowing := len(notifs) > batch.Limit
2531-
2532- for i, n := range notifs {
2533- // Play sound and vibrate on first notif only.
2534- if i > 0 {
2535- n.Notification.Vibrate = false
2536- n.Notification.Sound = ""
2537- }
2538-
2539- // We're overflowing, so no popups.
2540- // See LP: #1527171
2541- if overflowing {
2542- n.Notification.Card.Popup = false
2543- }
2544- }
2545-
2546- if overflowing {
2547- n := batch.OverflowHandler(notifs)
2548- n.Notification.Card.Persist = false
2549- n.Notification.Vibrate = false
2550- notifs = append(notifs, n)
2551- }
2552-
2553- for _, n := range notifs {
2554- var pushMessage string
2555- if out, err := json.Marshal(n); err == nil {
2556- pushMessage = string(out)
2557- } else {
2558- log.Printf("Cannot marshall %#v to json: %s", n, err)
2559- continue
2560- }
2561- if _, err := obj.Call(POSTAL_INTERFACE, "Post", post.appId, pushMessage); err != nil {
2562- log.Println("Cannot call the Post Office:", err)
2563- log.Println("Message missed posting:", pushMessage)
2564- }
2565- }
2566- }
2567- }
2568-}
2569-
2570-// pushObjectPath returns the object path of the ApplicationId
2571-// for Push Notifications with the Quoted Package Name in the form of
2572-// /com/ubuntu/PushNotifications/QUOTED_PKGNAME
2573-//
2574-// e.g.; if the APP_ID is com.ubuntu.music", the returned object path
2575-// would be "/com/ubuntu/PushNotifications/com_2eubuntu_2eubuntu_2emusic
2576-func pushObjectPath(id plugins.ApplicationId) dbus.ObjectPath {
2577- idParts := strings.Split(string(id), "_")
2578- if len(idParts) < 2 {
2579- panic(fmt.Sprintf("APP_ID '%s' is not valid", id))
2580- }
2581-
2582- pkg := POSTAL_OBJECT_PATH_PART
2583- for _, c := range idParts[0] {
2584- switch c {
2585- case '+', '.', '-', ':', '~', '_':
2586- pkg += fmt.Sprintf("_%x", string(c))
2587- default:
2588- pkg += string(c)
2589- }
2590- }
2591- return dbus.ObjectPath(pkg)
2592-}
2593
2594=== removed directory 'cmd/account-watcher-test'
2595=== removed file 'cmd/account-watcher-test/main.go'
2596--- cmd/account-watcher-test/main.go 2016-06-17 13:51:55 +0000
2597+++ cmd/account-watcher-test/main.go 1970-01-01 00:00:00 +0000
2598@@ -1,17 +0,0 @@
2599-package main
2600-
2601-import (
2602- "fmt"
2603-
2604- "launchpad.net/account-polld/accounts"
2605-)
2606-
2607-func main() {
2608- for data := range accounts.NewWatcher().C {
2609- if data.Error != nil {
2610- fmt.Println("Failed to authenticate account", data.AccountId, ":", data.Error)
2611- } else {
2612- fmt.Printf("%#v\n", data)
2613- }
2614- }
2615-}
2616
2617=== removed directory 'cmd/qtcontact-test'
2618=== removed file 'cmd/qtcontact-test/main.go'
2619--- cmd/qtcontact-test/main.go 2015-03-20 14:34:48 +0000
2620+++ cmd/qtcontact-test/main.go 1970-01-01 00:00:00 +0000
2621@@ -1,36 +0,0 @@
2622-/*
2623- Copyright 2014 Canonical Ltd.
2624-
2625- This program is free software: you can redistribute it and/or modify it
2626- under the terms of the GNU General Public License version 3, as published
2627- by the Free Software Foundation.
2628-
2629- This program is distributed in the hope that it will be useful, but
2630- WITHOUT ANY WARRANTY; without even the implied warranties of
2631- MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
2632- PURPOSE. See the GNU General Public License for more details.
2633-
2634- You should have received a copy of the GNU General Public License along
2635- with this program. If not, see <http://www.gnu.org/licenses/>.
2636-*/
2637-
2638-package main
2639-
2640-import (
2641- "fmt"
2642- "os"
2643-
2644- "launchpad.net/account-polld/qtcontact"
2645-)
2646-
2647-func main() {
2648- qtcontact.MainLoopStart()
2649-
2650- if len(os.Args) != 2 {
2651- fmt.Println("usage:", os.Args[0], "[email address]")
2652- os.Exit(1)
2653- }
2654-
2655- path := qtcontact.GetAvatar(os.Args[1])
2656- fmt.Println("Avatar found:", path)
2657-}
2658
2659=== added file 'common-installs-config.pri'
2660--- common-installs-config.pri 1970-01-01 00:00:00 +0000
2661+++ common-installs-config.pri 2016-09-21 07:16:22 +0000
2662@@ -0,0 +1,43 @@
2663+#-----------------------------------------------------------------------------
2664+# Common installation configuration for all projects.
2665+#-----------------------------------------------------------------------------
2666+
2667+
2668+#-----------------------------------------------------------------------------
2669+# default installation target for applications
2670+#-----------------------------------------------------------------------------
2671+contains(TEMPLATE, app) {
2672+ target.path = $${INSTALL_PREFIX}/bin
2673+ INSTALLS += target
2674+ message("====")
2675+ message("==== INSTALLS += target")
2676+}
2677+
2678+
2679+#-----------------------------------------------------------------------------
2680+# default installation target for libraries
2681+#-----------------------------------------------------------------------------
2682+contains(TEMPLATE, lib) {
2683+ isEmpty(target.path) {
2684+ target.path = $${INSTALL_LIBDIR}
2685+ }
2686+ INSTALLS += target
2687+ message("====")
2688+ message("==== INSTALLS += target")
2689+}
2690+
2691+#-----------------------------------------------------------------------------
2692+# target for header files
2693+#-----------------------------------------------------------------------------
2694+!isEmpty(headers.files) {
2695+ headers.path = $${INSTALL_PREFIX}/include/$${TARGET}
2696+ INSTALLS += headers
2697+ message("====")
2698+ message("==== INSTALLS += headers")
2699+} else {
2700+ message("====")
2701+ message("==== NOTE: Remember to add your API headers into `headers.files' for installation!")
2702+}
2703+
2704+
2705+# End of File
2706
2707=== added file 'common-pkgconfig.pri'
2708--- common-pkgconfig.pri 1970-01-01 00:00:00 +0000
2709+++ common-pkgconfig.pri 2016-09-21 07:16:22 +0000
2710@@ -0,0 +1,12 @@
2711+# Include this file after defining the pkgconfig.files variable
2712+
2713+!isEmpty(pkgconfig.files) {
2714+ QMAKE_SUBSTITUTES += $${pkgconfig.files}.in
2715+ pkgconfig.CONFIG = no_check_exist
2716+ pkgconfig.path = $${INSTALL_LIBDIR}/pkgconfig
2717+ QMAKE_EXTRA_TARGETS += pkgconfig
2718+
2719+ INSTALLS += pkgconfig
2720+
2721+ QMAKE_CLEAN += $${pkgconfig.files}
2722+}
2723
2724=== added file 'common-project-config.pri'
2725--- common-project-config.pri 1970-01-01 00:00:00 +0000
2726+++ common-project-config.pri 2016-09-21 07:16:22 +0000
2727@@ -0,0 +1,36 @@
2728+#-----------------------------------------------------------------------------
2729+# Common configuration for all projects.
2730+#-----------------------------------------------------------------------------
2731+
2732+# we don't like warnings...
2733+QMAKE_CXXFLAGS += -Werror
2734+# Disable RTTI
2735+QMAKE_CXXFLAGS += -fno-exceptions -fno-rtti
2736+
2737+CONFIG += c++11
2738+
2739+!defined(TOP_SRC_DIR, var) {
2740+ TOP_SRC_DIR = $$PWD
2741+ TOP_BUILD_DIR = $${TOP_SRC_DIR}/$(BUILD_DIR)
2742+}
2743+
2744+include(coverage.pri)
2745+
2746+#-----------------------------------------------------------------------------
2747+# setup the installation prefix
2748+#-----------------------------------------------------------------------------
2749+INSTALL_PREFIX = /usr # default installation prefix
2750+
2751+# default prefix can be overriden by defining PREFIX when running qmake
2752+isEmpty(PREFIX) {
2753+ message("====")
2754+ message("==== NOTE: To override the installation path run: `qmake PREFIX=/custom/path'")
2755+ message("==== (current installation path is `$${INSTALL_PREFIX}')")
2756+} else {
2757+ INSTALL_PREFIX = $${PREFIX}
2758+ message("====")
2759+ message("==== install prefix set to `$${INSTALL_PREFIX}'")
2760+}
2761+
2762+I18N_DOMAIN="account-polld"
2763+PLUGIN_DATA_FILE="account-polld/plugin_data.json"
2764
2765=== added file 'common-vars.pri'
2766--- common-vars.pri 1970-01-01 00:00:00 +0000
2767+++ common-vars.pri 2016-09-21 07:16:22 +0000
2768@@ -0,0 +1,18 @@
2769+#-----------------------------------------------------------------------------
2770+# Common variables for all projects.
2771+#-----------------------------------------------------------------------------
2772+
2773+
2774+#-----------------------------------------------------------------------------
2775+# Project name (used e.g. in include file and doc install path).
2776+# remember to update debian/* files if you changes this
2777+#-----------------------------------------------------------------------------
2778+PROJECT_NAME = account-polld
2779+
2780+#-----------------------------------------------------------------------------
2781+# Project version
2782+# remember to update debian/* files if you changes this
2783+#-----------------------------------------------------------------------------
2784+PROJECT_VERSION = 0.2
2785+
2786+# End of File
2787
2788=== added file 'coverage.pri'
2789--- coverage.pri 1970-01-01 00:00:00 +0000
2790+++ coverage.pri 2016-09-21 07:16:22 +0000
2791@@ -0,0 +1,48 @@
2792+# Coverage
2793+CONFIG(coverage) {
2794+ OBJECTS_DIR =
2795+ MOC_DIR =
2796+
2797+ LIBS += -lgcov
2798+ QMAKE_CXXFLAGS += --coverage
2799+ QMAKE_LDFLAGS += --coverage
2800+
2801+ QMAKE_EXTRA_TARGETS += coverage cov
2802+ QMAKE_EXTRA_TARGETS += clean-gcno clean-gcda coverage-html \
2803+ generate-coverage-html clean-coverage-html coverage-gcovr \
2804+ generate-gcovr generate-coverage-gcovr clean-coverage-gcovr
2805+
2806+ clean-gcno.commands = \
2807+ "@echo Removing old coverage instrumentation"; \
2808+ "find -name '*.gcno' -print | xargs -r rm"
2809+
2810+ clean-gcda.commands = \
2811+ "@echo Removing old coverage results"; \
2812+ "find -name '*.gcda' -print | xargs -r rm"
2813+
2814+ coverage-html.depends = clean-gcda check generate-coverage-html
2815+
2816+ generate-coverage-html.commands = \
2817+ "@echo Collecting coverage data"; \
2818+ "lcov --directory $${TOP_SRC_DIR} --capture --output-file coverage.info --no-checksum --compat-libtool"; \
2819+ "lcov --extract coverage.info \"*/account-polld/*.cpp\" -o coverage.info"; \
2820+ "lcov --remove coverage.info \"moc_*.cpp\" --remove coverage.info \"tests/*.cpp\" -o coverage.info"; \
2821+ "LANG=C genhtml --prefix $${TOP_SRC_DIR} --output-directory coverage-html --title \"Code Coverage\" --legend --show-details coverage.info"
2822+
2823+ clean-coverage-html.depends = clean-gcda
2824+ clean-coverage-html.commands = \
2825+ "lcov --directory $${TOP_SRC_DIR} -z"; \
2826+ "rm -rf coverage.info coverage-html"
2827+
2828+ coverage-gcovr.depends = clean-gcda check generate-coverage-gcovr
2829+
2830+ generate-coverage-gcovr.commands = \
2831+ "@echo Generating coverage GCOVR report"; \
2832+ "gcovr -x -r $${TOP_SRC_DIR} -o $${TOP_SRC_DIR}/coverage.xml -e \".*/moc_.*\" -e \"tests/.*\" -e \".*\\.h\""
2833+
2834+ clean-coverage-gcovr.depends = clean-gcda
2835+ clean-coverage-gcovr.commands = \
2836+ "rm -rf $${TOP_SRC_DIR}/coverage.xml"
2837+
2838+ QMAKE_CLEAN += *.gcda *.gcno coverage.info coverage.xml
2839+}
2840
2841=== modified file 'debian/control'
2842--- debian/control 2015-09-09 08:50:05 +0000
2843+++ debian/control 2016-09-21 07:16:22 +0000
2844@@ -3,22 +3,19 @@
2845 Priority: optional
2846 Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
2847 Build-Depends: debhelper (>= 9),
2848- dh-golang,
2849 dh-translations,
2850- golang-go,
2851- golang-go-dbus-dev,
2852- golang-go-xdg-dev,
2853- golang-gocheck-dev,
2854- golang-ubuntu-push-dev,
2855- libaccounts-glib-dev,
2856- libclick-0.4-dev,
2857- libsignon-glib-dev,
2858+ libaccounts-qt5-dev,
2859+ libqtdbusmock1-dev,
2860+ libqtdbustest1-dev,
2861+ libsignon-qt5-dev,
2862+ pkg-config,
2863 qt5-default,
2864 qtbase5-dev,
2865- qtpim5-dev,
2866+ python3,
2867+ python3-xdg,
2868 Standards-Version: 3.9.5
2869 Homepage: https://launchpad.net/account-polld
2870-Vcs-Browser: http://bazaar.launchpad.net/~phablet-team/account-polld/trunk/files
2871+Vcs-Browser: http://bazaar.launchpad.net/~online-accounts/account-polld/trunk/files
2872 Vcs-Bzr: lp:account-polld
2873
2874 Package: account-polld
2875@@ -27,10 +24,8 @@
2876 ${misc:Depends},
2877 ${shlibs:Depends},
2878 Built-Using: ${misc:Built-Using}
2879-Recommends: accountsservice,
2880+Recommends: account-polld-plugins-go,
2881 Description: Poll daemon for notifications though the Ubuntu Push Client
2882- This component polls twitter and gmail for updates and
2883- communicates with the postal service provided by the ubuntu push client
2884- to expose notifications for the click webapps for the aforementioned
2885- services.
2886-X-Ubuntu-Use-Langpack: yes
2887+ This component polls remote services for updates and communicates with the
2888+ postal service provided by the ubuntu push client to expose notifications for
2889+ the click webapps for the aforementioned services.
2890
2891=== modified file 'debian/rules'
2892--- debian/rules 2014-10-01 13:02:24 +0000
2893+++ debian/rules 2016-09-21 07:16:22 +0000
2894@@ -2,38 +2,7 @@
2895 # -*- makefile -*-
2896
2897 export DH_OPTIONS
2898-export DH_GOPKG := launchpad.net/account-polld
2899-export DH_GOLANG_INSTALL_ALL := 1
2900+
2901
2902 %:
2903- dh $@ \
2904- --buildsystem=golang \
2905- --with=golang \
2906- --with=translations \
2907- --fail-missing
2908-
2909-override_dh_auto_install:
2910- dh_auto_install -O--buildsystem=golang
2911- rm \
2912- ${CURDIR}/debian/account-polld/usr/bin/account-watcher-test
2913- # all our libs are private
2914- rm -r \
2915- ${CURDIR}/debian/account-polld/usr/share/gocode
2916- # setup online accounts service files
2917- mkdir -p \
2918- ${CURDIR}/debian/account-polld/usr/share/applications
2919- cp ${CURDIR}/data/account-polld.desktop \
2920- ${CURDIR}/debian/account-polld/usr/share/applications/
2921- # translations
2922- appname=account-polld; \
2923- for pofile in po/*.po; do \
2924- pofilename="$${pofile##*/}"; \
2925- langcode="$${pofilename%.*}"; \
2926- localedir="debian/$$appname/usr/share/locale/$$langcode/LC_MESSAGES"; \
2927- mkdir -p $$localedir; \
2928- mofile="$$localedir/$$appname.mo"; \
2929- msgfmt -o $$mofile $$pofile; \
2930- done
2931-
2932-override_dh_strip:
2933- echo "Skipping strip (LP: #1318027)"
2934+ dh $@
2935
2936=== removed directory 'gettext'
2937=== removed file 'gettext/LICENSE'
2938--- gettext/LICENSE 2014-07-29 18:02:58 +0000
2939+++ gettext/LICENSE 1970-01-01 00:00:00 +0000
2940@@ -1,20 +0,0 @@
2941-Copyright (c) 2012-2013 José Carlos Nieto, http://xiam.menteslibres.org/
2942-
2943-Permission is hereby granted, free of charge, to any person obtaining
2944-a copy of this software and associated documentation files (the
2945-"Software"), to deal in the Software without restriction, including
2946-without limitation the rights to use, copy, modify, merge, publish,
2947-distribute, sublicense, and/or sell copies of the Software, and to
2948-permit persons to whom the Software is furnished to do so, subject to
2949-the following conditions:
2950-
2951-The above copyright notice and this permission notice shall be
2952-included in all copies or substantial portions of the Software.
2953-
2954-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
2955-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
2956-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
2957-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
2958-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
2959-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
2960-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2961
2962=== removed file 'gettext/README.md'
2963--- gettext/README.md 2014-07-29 18:02:58 +0000
2964+++ gettext/README.md 1970-01-01 00:00:00 +0000
2965@@ -1,94 +0,0 @@
2966-# gosexy/gettext
2967-
2968-Go bindings for [GNU gettext][1], an internationalization and localization
2969-library for writing multilingual systems.
2970-
2971-## Requeriments
2972-
2973-The GNU C library. If you're using GNU/Linux, FreeBSD or OSX you should already
2974-have it.
2975-
2976-## Installation
2977-
2978-Use `go get` to download and install the binding:
2979-
2980-```sh
2981-go get github.com/gosexy/gettext
2982-```
2983-
2984-## Usage
2985-
2986-```go
2987-package main
2988-
2989-import (
2990- "github.com/gosexy/gettext"
2991- "fmt"
2992- "os"
2993-)
2994-
2995-func main() {
2996- gettext.BindTextdomain("example", ".")
2997- gettext.Textdomain("example")
2998-
2999- os.Setenv("LANGUAGE", "es_MX.utf8")
3000-
3001- gettext.SetLocale(gettext.LC_ALL, "")
3002-
3003- fmt.Println(gettext.Gettext("Hello, world!"))
3004-}
3005-```
3006-
3007-You can use `os.Setenv` to set the `LANGUAGE` environment variable or set it
3008-on a terminal:
3009-
3010-```sh
3011-export LANGUAGE="es_MX.utf8"
3012-./gettext-program
3013-```
3014-
3015-Note that `xgettext` does not officially support Go syntax yet, however, you
3016-can generate a valid `.pot` file by forcing `xgettest` to use the C++
3017-syntax:
3018-
3019-```sh
3020-xgettext -d example -s gettext_test.go -o example.pot -L c++ -i \
3021---keyword=NGettext:1,2 --keyword=Gettext
3022-```
3023-
3024-This will generate a `example.pot` file.
3025-
3026-After translating the `.pot` file, you must generate `.po` and `.mo` files and
3027-remember to set the UTF-8 charset.
3028-
3029-```sh
3030-msginit -l es_MX -o example.po -i example.pot
3031-msgfmt -c -v -o example.mo example.po
3032-```
3033-
3034-Finally, move the `.mo` file to an appropriate location.
3035-
3036-```sh
3037-mv example.mo examples/es_MX.utf8/LC_MESSAGES/example.mo
3038-```
3039-
3040-## Documentation
3041-
3042-You can read `gosexy/gettext` documentation from a terminal
3043-
3044-```sh
3045-go doc github.com/gosexy/gettext
3046-```
3047-
3048-Or you can [browse it](http://godoc.org/github.com/gosexy/gettext) online.
3049-
3050-The original gettext documentation could be very useful as well:
3051-
3052-```sh
3053-man 3 gettext
3054-```
3055-
3056-Here's another [good tutorial][2] on using gettext.
3057-
3058-[1]: http://www.gnu.org/software/gettext/
3059-[2]: http://oriya.sarovar.org/docs/gettext_single.html
3060
3061=== removed file 'gettext/gettext.go'
3062--- gettext/gettext.go 2014-07-29 18:02:58 +0000
3063+++ gettext/gettext.go 1970-01-01 00:00:00 +0000
3064@@ -1,207 +0,0 @@
3065-/*
3066- Copyright (c) 2012 José Carlos Nieto, http://xiam.menteslibres.org/
3067-
3068- Permission is hereby granted, free of charge, to any person obtaining
3069- a copy of this software and associated documentation files (the
3070- "Software"), to deal in the Software without restriction, including
3071- without limitation the rights to use, copy, modify, merge, publish,
3072- distribute, sublicense, and/or sell copies of the Software, and to
3073- permit persons to whom the Software is furnished to do so, subject to
3074- the following conditions:
3075-
3076- The above copyright notice and this permission notice shall be
3077- included in all copies or substantial portions of the Software.
3078-
3079- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
3080- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
3081- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
3082- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
3083- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
3084- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
3085- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
3086-*/
3087-
3088-package gettext
3089-
3090-/*
3091-
3092-#include <libintl.h>
3093-#include <locale.h>
3094-#include <stdlib.h>
3095-*/
3096-import "C"
3097-
3098-import (
3099- "fmt"
3100- "strings"
3101- "unsafe"
3102-)
3103-
3104-var (
3105- // For all of the locale.
3106- LC_ALL = uint(C.LC_ALL)
3107-
3108- // For regular expression matching (it determines the meaning of range
3109- // expressions and equivalence classes) and string collation.
3110- LC_COLATE = uint(C.LC_ALL)
3111-
3112- // For regular expression matching, character classification, conversion,
3113- // case-sensitive comparison, and wide character functions.
3114- LC_CTYPE = uint(C.LC_CTYPE)
3115-
3116- // For localizable natural-language messages.
3117- LC_MESSAGES = uint(C.LC_MESSAGES)
3118-
3119- // For monetary formatting.
3120- LC_MONETARY = uint(C.LC_MONETARY)
3121-
3122- // For number formatting (such as the decimal point and the thousands
3123- // separator).
3124- LC_NUMERIC = uint(C.LC_NUMERIC)
3125-
3126- // For time and date formatting.
3127- LC_TIME = uint(C.LC_TIME)
3128-)
3129-
3130-// Sets or queries the program's current locale.
3131-func SetLocale(category uint, locale string) string {
3132- clocale := C.CString(locale)
3133-
3134- res := C.GoString(C.setlocale(C.int(category), clocale))
3135-
3136- C.free(unsafe.Pointer(clocale))
3137- return res
3138-}
3139-
3140-// Sets directory containing message catalogs.
3141-func BindTextdomain(domainname string, dirname string) string {
3142- cdirname := C.CString(dirname)
3143- cdomainname := C.CString(domainname)
3144-
3145- res := C.GoString(C.bindtextdomain(cdomainname, cdirname))
3146-
3147- C.free(unsafe.Pointer(cdirname))
3148- C.free(unsafe.Pointer(cdomainname))
3149- return res
3150-}
3151-
3152-// Sets the output codeset for message catalogs for domain domainname.
3153-func BindTextdomainCodeset(domainname string, codeset string) string {
3154- cdomainname := C.CString(domainname)
3155- ccodeset := C.CString(codeset)
3156-
3157- res := C.GoString(C.bind_textdomain_codeset(cdomainname, ccodeset))
3158-
3159- C.free(unsafe.Pointer(cdomainname))
3160- C.free(unsafe.Pointer(ccodeset))
3161- return res
3162-}
3163-
3164-// Sets or retrieves the current message domain.
3165-func Textdomain(domainname string) string {
3166- cdomainname := C.CString(domainname)
3167-
3168- res := C.GoString(C.textdomain(cdomainname))
3169-
3170- C.free(unsafe.Pointer(cdomainname))
3171- return res
3172-}
3173-
3174-// Attempt to translate a text string into the user's native language, by
3175-// looking up the translation in a message catalog.
3176-func Gettext(msgid string) string {
3177- cmsgid := C.CString(msgid)
3178-
3179- res := C.GoString(C.gettext(cmsgid))
3180-
3181- C.free(unsafe.Pointer(cmsgid))
3182- return res
3183-}
3184-
3185-// Like Gettext(), but looking up the message in the specified domain.
3186-func DGettext(domain string, msgid string) string {
3187- cdomain := C.CString(domain)
3188- cmsgid := C.CString(msgid)
3189-
3190- res := C.GoString(C.dgettext(cdomain, cmsgid))
3191-
3192- C.free(unsafe.Pointer(cdomain))
3193- C.free(unsafe.Pointer(cmsgid))
3194- return res
3195-}
3196-
3197-// Like Gettext(), but looking up the message in the specified domain and
3198-// category.
3199-func DCGettext(domain string, msgid string, category uint) string {
3200- cdomain := C.CString(domain)
3201- cmsgid := C.CString(msgid)
3202-
3203- res := C.GoString(C.dcgettext(cdomain, cmsgid, C.int(category)))
3204-
3205- C.free(unsafe.Pointer(cdomain))
3206- C.free(unsafe.Pointer(cmsgid))
3207- return res
3208-}
3209-
3210-// Attempt to translate a text string into the user's native language, by
3211-// looking up the appropriate plural form of the translation in a message
3212-// catalog.
3213-func NGettext(msgid string, msgid_plural string, n uint64) string {
3214- cmsgid := C.CString(msgid)
3215- cmsgid_plural := C.CString(msgid_plural)
3216-
3217- res := C.GoString(C.ngettext(cmsgid, cmsgid_plural, C.ulong(n)))
3218-
3219- C.free(unsafe.Pointer(cmsgid))
3220- C.free(unsafe.Pointer(cmsgid_plural))
3221-
3222- return res
3223-}
3224-
3225-// Like fmt.Sprintf() but without %!(EXTRA) errors.
3226-func Sprintf(format string, a ...interface{}) string {
3227- expects := strings.Count(format, "%") - strings.Count(format, "%%")
3228-
3229- if expects > 0 {
3230- arguments := make([]interface{}, expects)
3231- for i := 0; i < expects; i++ {
3232- if len(a) > i {
3233- arguments[i] = a[i]
3234- }
3235- }
3236- return fmt.Sprintf(format, arguments...)
3237- }
3238-
3239- return format
3240-}
3241-
3242-// Like NGettext(), but looking up the message in the specified domain.
3243-func DNGettext(domainname string, msgid string, msgid_plural string, n uint64) string {
3244- cdomainname := C.CString(domainname)
3245- cmsgid := C.CString(msgid)
3246- cmsgid_plural := C.CString(msgid_plural)
3247-
3248- res := C.GoString(C.dngettext(cdomainname, cmsgid, cmsgid_plural, C.ulong(n)))
3249-
3250- C.free(unsafe.Pointer(cdomainname))
3251- C.free(unsafe.Pointer(cmsgid))
3252- C.free(unsafe.Pointer(cmsgid_plural))
3253-
3254- return res
3255-}
3256-
3257-// Like NGettext(), but looking up the message in the specified domain and
3258-// category.
3259-func DCNGettext(domainname string, msgid string, msgid_plural string, n uint64, category uint) string {
3260- cdomainname := C.CString(domainname)
3261- cmsgid := C.CString(msgid)
3262- cmsgid_plural := C.CString(msgid_plural)
3263-
3264- res := C.GoString(C.dcngettext(cdomainname, cmsgid, cmsgid_plural, C.ulong(n), C.int(category)))
3265-
3266- C.free(unsafe.Pointer(cdomainname))
3267- C.free(unsafe.Pointer(cmsgid))
3268- C.free(unsafe.Pointer(cmsgid_plural))
3269-
3270- return res
3271-}
3272
3273=== removed directory 'plugins'
3274=== removed directory 'plugins/caldav'
3275=== removed file 'plugins/caldav/api.go'
3276--- plugins/caldav/api.go 2016-07-12 19:40:07 +0000
3277+++ plugins/caldav/api.go 1970-01-01 00:00:00 +0000
3278@@ -1,54 +0,0 @@
3279-/*
3280- Copyright 2014 Canonical Ltd.
3281-
3282- This program is free software: you can redistribute it and/or modify it
3283- under the terms of the GNU General Public License version 3, as published
3284- by the Free Software Foundation.
3285-
3286- This program is distributed in the hope that it will be useful, but
3287- WITHOUT ANY WARRANTY; without even the implied warranties of
3288- MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
3289- PURPOSE. See the GNU General Public License for more details.
3290-
3291- You should have received a copy of the GNU General Public License along
3292- with this program. If not, see <http://www.gnu.org/licenses/>.
3293-*/
3294-
3295-package caldav
3296-
3297-import (
3298- "fmt"
3299-)
3300-
3301-// eventList holds a response to call to Calendar.events: list
3302-// defined in https://developers.google.com/google-apps/calendar/v3/reference/events/list#response
3303-type eventList struct {
3304- // Messages holds a list of message.
3305- Events []event `json:"items"`
3306-}
3307-
3308-// event holds the event data response for a Calendar.event.
3309-// The full definition of a message is defined in
3310-// https://developers.google.com/google-apps/calendar/v3/reference/events#resource-representations
3311-type event struct {
3312- // Id is the immutable ID of the message.
3313- Etag string `json:"etag"`
3314- // ThreadId is the ID of the thread the message belongs to.
3315- Summary string `json:"summary"`
3316-}
3317-
3318-func (e event) String() string {
3319- return fmt.Sprintf("Id: %s, snippet: '%s'\n", e.Etag, e.Summary)
3320-}
3321-
3322-type errorResp struct {
3323- Err struct {
3324- Code uint64 `json:"code"`
3325- Message string `json:"message"`
3326- Errors []struct {
3327- Domain string `json:"domain"`
3328- Reason string `json:"reason"`
3329- Message string `json:"message"`
3330- } `json:"errors"`
3331- } `json:"error"`
3332-}
3333
3334=== removed file 'plugins/caldav/caldav.go'
3335--- plugins/caldav/caldav.go 2016-08-03 11:59:28 +0000
3336+++ plugins/caldav/caldav.go 1970-01-01 00:00:00 +0000
3337@@ -1,201 +0,0 @@
3338-/*
3339- Copyright 2016 Canonical Ltd.
3340-
3341- This program is free software: you can redistribute it and/or modify it
3342- under the terms of the GNU General Public License version 3, as published
3343- by the Free Software Foundation.
3344-
3345- This program is distributed in the hope that it will be useful, but
3346- WITHOUT ANY WARRANTY; without even the implied warranties of
3347- MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
3348- PURPOSE. See the GNU General Public License for more details.
3349-
3350- You should have received a copy of the GNU General Public License along
3351- with this program. If not, see <http://www.gnu.org/licenses/>.
3352-*/
3353-
3354-package caldav
3355-
3356-import (
3357- "bytes"
3358- "fmt"
3359- "io/ioutil"
3360- "log"
3361- "net/http"
3362- "net/url"
3363- "os"
3364- "strings"
3365- "time"
3366-
3367- "launchpad.net/account-polld/accounts"
3368- "launchpad.net/account-polld/plugins"
3369- "launchpad.net/account-polld/syncmonitor"
3370-)
3371-
3372-const (
3373- APP_ID = "com.ubuntu.calendar_calendar"
3374- pluginName = "caldav"
3375-)
3376-
3377-type CalDavPlugin struct {
3378- accountId uint
3379-}
3380-
3381-func New(accountId uint) *CalDavPlugin {
3382- return &CalDavPlugin{accountId: accountId}
3383-}
3384-
3385-func (p *CalDavPlugin) ApplicationId() plugins.ApplicationId {
3386- return plugins.ApplicationId(APP_ID)
3387-}
3388-
3389-func (p *CalDavPlugin) Poll(authData *accounts.AuthData) ([]*plugins.PushMessageBatch, error) {
3390- // This envvar check is to ease testing.
3391- if token := os.Getenv("ACCOUNT_POLLD_TOKEN_CALDAV"); token != "" {
3392- log.Print("Using token from: ACCOUNT_POLLD_TOKEN_CALDAV env var")
3393- authData.AccessToken = token
3394- }
3395-
3396- log.Print("Check calendar changes for account:", p.accountId)
3397-
3398- syncMonitor := syncmonitor.NewSyncMonitor()
3399- if syncMonitor == nil {
3400- log.Print("Sync monitor not available yet.")
3401- return nil, nil
3402- }
3403-
3404- state, err := syncMonitor.State()
3405- if err != nil {
3406- log.Print("Fail to retrieve sync monitor state ", err)
3407- return nil, nil
3408- }
3409- if state != "idle" {
3410- log.Print("Sync monitor is not on 'idle' state, try later!")
3411- return nil, nil
3412- }
3413-
3414- calendars, err := syncMonitor.ListCalendarsByAccount(p.accountId)
3415- if err != nil {
3416- log.Print("Calendar plugin ", p.accountId, ": cannot load calendars: ", err)
3417- return nil, nil
3418- }
3419-
3420- var calendarsToSync []string
3421- log.Print("Number of calendars for account:", p.accountId, " size:", len(calendars))
3422-
3423- for id, calendar := range calendars {
3424- lastSyncDate, err := syncMonitor.LastSyncDate(p.accountId, id)
3425- if err != nil {
3426- log.Print("\tcalendar: ", id, ", cannot load previous sync date: ", err, ". Try next time.")
3427- continue
3428- } else {
3429- log.Print("\tcalendar: ", id, " Url: ", calendar, " last sync date: ", lastSyncDate)
3430- }
3431-
3432- var needSync bool
3433- needSync = (len(lastSyncDate) == 0)
3434-
3435- if !needSync {
3436- resp, err := p.requestChanges(authData, calendar, lastSyncDate)
3437- if err != nil {
3438- log.Print("\tERROR: Fail to query for changes: ", err)
3439- continue
3440- }
3441-
3442- needSync, err = p.containEvents(resp)
3443- if err != nil {
3444- log.Print("\tERROR: Fail to parse changes: ", err)
3445- if err == plugins.ErrTokenExpired {
3446- log.Print("\t\tAbort poll")
3447- return nil, err
3448- } else {
3449- continue
3450- }
3451- }
3452- }
3453-
3454- if needSync {
3455- log.Print("\tCalendar needs sync: ", id)
3456- calendarsToSync = append(calendarsToSync, id)
3457- } else {
3458- log.Print("\tFound no calendar updates for account: ", p.accountId, " calendar: ", id)
3459- }
3460- }
3461-
3462- if len(calendarsToSync) > 0 {
3463- log.Print("Request account sync")
3464- err = syncMonitor.SyncAccount(p.accountId, calendarsToSync)
3465- if err != nil {
3466- log.Print("ERROR: Fail to start account sync ", p.accountId, " message: ", err)
3467- }
3468- }
3469-
3470- return nil, nil
3471-}
3472-
3473-func (p *CalDavPlugin) containEvents(resp *http.Response) (bool, error) {
3474- defer resp.Body.Close()
3475- log.Print("RESPONSE CODE ----:", resp.StatusCode)
3476-
3477- if resp.StatusCode != 207 {
3478- var errResp errorResp
3479- log.Print("Invalid response:", errResp.Err.Code)
3480- return false, nil
3481- } else {
3482- data, err := ioutil.ReadAll(resp.Body)
3483- if err != nil {
3484- return false, err
3485- }
3486- fmt.Printf("DATA: %s", data)
3487- return strings.Contains(string(data), "BEGIN:VEVENT"), nil
3488- }
3489-
3490- return false, nil
3491-}
3492-
3493-func (p *CalDavPlugin) requestChanges(authData *accounts.AuthData, calendar string, lastSyncDate string) (*http.Response, error) {
3494- u, err := url.Parse(calendar)
3495- if err != nil {
3496- return nil, err
3497- }
3498- startDate, err := time.Parse(time.RFC3339, lastSyncDate)
3499- if err != nil {
3500- log.Print("Fail to parse date: ", lastSyncDate)
3501- return nil, err
3502- }
3503-
3504- // Start date will be one minute before last sync
3505- startDate = startDate.Add(time.Duration(-1) * time.Minute)
3506-
3507- // End Date will be one year in the future from now
3508- endDate := time.Now().AddDate(1, 0, 0).UTC()
3509-
3510- log.Print("Calendar Url:", calendar)
3511-
3512- query := "<c:calendar-query xmlns:d=\"DAV:\" xmlns:c=\"urn:ietf:params:xml:ns:caldav\">\n"
3513- query += "<d:prop>\n"
3514- query += "<d:getetag />\n"
3515- query += "<c:calendar-data />\n"
3516- query += "</d:prop>\n"
3517- query += "<c:filter>\n"
3518- query += "<c:comp-filter name=\"VCALENDAR\">\n"
3519- query += "<c:comp-filter name=\"VEVENT\">\n"
3520- query += "<c:prop-filter name=\"LAST-MODIFIED\">\n"
3521- query += "<c:time-range start=\"" + startDate.Format("20060102T150405Z") + "\" end=\"" + endDate.Format("20060102T150405Z") + "\"/>\n"
3522- query += "</c:prop-filter>\n"
3523- query += "</c:comp-filter>\n"
3524- query += "</c:comp-filter>\n"
3525- query += "</c:filter>\n"
3526- query += "</c:calendar-query>\n"
3527- log.Print("Query: ", query)
3528- req, err := http.NewRequest("REPORT", u.String(), bytes.NewBufferString(query))
3529- if err != nil {
3530- return nil, err
3531- }
3532- req.Header.Set("Depth", "1")
3533- req.Header.Set("Prefer", "return-minimal")
3534- req.Header.Set("Content-Type", "application/xml; charset=utf-8")
3535- req.SetBasicAuth(authData.UserName, authData.Secret)
3536-
3537- return http.DefaultClient.Do(req)
3538-}
3539
3540=== removed directory 'plugins/dekko'
3541=== removed file 'plugins/dekko/api.go'
3542--- plugins/dekko/api.go 2016-06-15 14:41:33 +0000
3543+++ plugins/dekko/api.go 1970-01-01 00:00:00 +0000
3544@@ -1,127 +0,0 @@
3545-/*
3546- Copyright 2014 Canonical Ltd.
3547-
3548- This program is free software: you can redistribute it and/or modify it
3549- under the terms of the GNU General Public License version 3, as published
3550- by the Free Software Foundation.
3551-
3552- This program is distributed in the hope that it will be useful, but
3553- WITHOUT ANY WARRANTY; without even the implied warranties of
3554- MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
3555- PURPOSE. See the GNU General Public License for more details.
3556-
3557- You should have received a copy of the GNU General Public License along
3558- with this program. If not, see <http://www.gnu.org/licenses/>.
3559-*/
3560-
3561-package dekko
3562-
3563-import (
3564- "fmt"
3565- "time"
3566-
3567- "launchpad.net/account-polld/plugins"
3568-)
3569-
3570-const gmailTime = "Mon, 2 Jan 2006 15:04:05 -0700"
3571-
3572-type pushes map[string]*plugins.PushMessage
3573-type headers map[string]string
3574-
3575-// messageList holds a response to call to Users.messages: list
3576-// defined in https://developers.google.com/gmail/api/v1/reference/users/messages/list
3577-type messageList struct {
3578- // Messages holds a list of message.
3579- Messages []message `json:"messages"`
3580- // NextPageToken is used to retrieve the next page of results in the list.
3581- NextPageToken string `json:"nextPageToken"`
3582- // ResultSizeEstimage is the estimated total number of results.
3583- ResultSizeEstimage uint64 `json:"resultSizeEstimate"`
3584-}
3585-
3586-// message holds a partial response for a Users.messages.
3587-// The full definition of a message is defined in
3588-// https://developers.google.com/gmail/api/v1/reference/users/messages#resource
3589-type message struct {
3590- // Id is the immutable ID of the message.
3591- Id string `json:"id"`
3592- // ThreadId is the ID of the thread the message belongs to.
3593- ThreadId string `json:"threadId"`
3594- // HistoryId is the ID of the last history record that modified
3595- // this message.
3596- HistoryId string `json:"historyId"`
3597- // Snippet is a short part of the message text. This text is
3598- // used for the push message summary.
3599- Snippet string `json:"snippet"`
3600- // Payload represents the message payload.
3601- Payload payload `json:"payload"`
3602-}
3603-
3604-func (m message) String() string {
3605- return fmt.Sprintf("Id: %d, snippet: '%s'\n", m.Id, m.Snippet[:10])
3606-}
3607-
3608-// ById implements sort.Interface for []message based on
3609-// the Id field.
3610-type byId []message
3611-
3612-func (m byId) Len() int { return len(m) }
3613-func (m byId) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
3614-func (m byId) Less(i, j int) bool { return m[i].Id < m[j].Id }
3615-
3616-// payload represents the message payload.
3617-type payload struct {
3618- Headers []messageHeader `json:"headers"`
3619-}
3620-
3621-func (p *payload) mapHeaders() headers {
3622- headers := make(map[string]string)
3623- for _, hdr := range p.Headers {
3624- headers[hdr.Name] = hdr.Value
3625- }
3626- return headers
3627-}
3628-
3629-func (hdr headers) getTimestamp() time.Time {
3630- timestamp, ok := hdr[hdrDATE]
3631- if !ok {
3632- return time.Now()
3633- }
3634-
3635- if t, err := time.Parse(gmailTime, timestamp); err == nil {
3636- return t
3637- }
3638- return time.Now()
3639-}
3640-
3641-func (hdr headers) getEpoch() int64 {
3642- return hdr.getTimestamp().Unix()
3643-}
3644-
3645-// messageHeader represents the message headers.
3646-type messageHeader struct {
3647- Name string `json:"name"`
3648- Value string `json:"value"`
3649-}
3650-
3651-type errorResp struct {
3652- Err struct {
3653- Code uint64 `json:"code"`
3654- Message string `json:"message"`
3655- Errors []struct {
3656- Domain string `json:"domain"`
3657- Reason string `json:"reason"`
3658- Message string `json:"message"`
3659- } `json:"errors"`
3660- } `json:"error"`
3661-}
3662-
3663-func (err *errorResp) Error() string {
3664- return fmt.Sprint("backend response:", err.Err.Message)
3665-}
3666-
3667-const (
3668- hdrDATE = "Date"
3669- hdrSUBJECT = "Subject"
3670- hdrFROM = "From"
3671-)
3672
3673=== removed file 'plugins/dekko/dekko.go'
3674--- plugins/dekko/dekko.go 2016-07-22 09:46:36 +0000
3675+++ plugins/dekko/dekko.go 1970-01-01 00:00:00 +0000
3676@@ -1,346 +0,0 @@
3677-/*
3678- Copyright 2014 Canonical Ltd.
3679-
3680- This program is free software: you can redistribute it and/or modify it
3681- under the terms of the GNU General Public License version 3, as published
3682- by the Free Software Foundation.
3683-
3684- This program is distributed in the hope that it will be useful, but
3685- WITHOUT ANY WARRANTY; without even the implied warranties of
3686- MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
3687- PURPOSE. See the GNU General Public License for more details.
3688-
3689- You should have received a copy of the GNU General Public License along
3690- with this program. If not, see <http://www.gnu.org/licenses/>.
3691-*/
3692-
3693-package dekko
3694-
3695-import (
3696- "encoding/json"
3697- "fmt"
3698- "net/http"
3699- "net/mail"
3700- "net/url"
3701- "os"
3702- "regexp"
3703- "sort"
3704- "strings"
3705- "time"
3706-
3707- "log"
3708-
3709- "launchpad.net/account-polld/accounts"
3710- "launchpad.net/account-polld/gettext"
3711- "launchpad.net/account-polld/plugins"
3712- "launchpad.net/account-polld/qtcontact"
3713-)
3714-
3715-const (
3716- APP_ID = "dekko.dekkoproject_dekko"
3717- dekkoDispatchUrl = "dekko://notify/%d/%s/%s"
3718- // If there's more than 10 emails in one batch, we don't show 10 notification
3719- // bubbles, but instead show one summary. We always show all notifications in the
3720- // indicator.
3721- individualNotificationsLimit = 10
3722- pluginName = "dekko"
3723-)
3724-
3725-type reportedIdMap map[string]time.Time
3726-
3727-var baseUrl, _ = url.Parse("https://www.googleapis.com/gmail/v1/users/me/")
3728-
3729-// timeDelta defines how old messages can be to be reported.
3730-var timeDelta = time.Duration(time.Hour * 24)
3731-
3732-// trackDelta defines how old messages can be before removed from tracking
3733-var trackDelta = time.Duration(time.Hour * 24 * 7)
3734-
3735-// relativeTimeDelta is the same as timeDelta
3736-var relativeTimeDelta string = "1d"
3737-
3738-// regexp for identifying non-ascii characters
3739-var nonAsciiChars, _ = regexp.Compile("[^\x00-\x7F]")
3740-
3741-type GmailPlugin struct {
3742- // reportedIds holds the messages that have already been notified. This
3743- // approach is taken against timestamps as it avoids needing to call
3744- // get on the message.
3745- reportedIds reportedIdMap
3746- accountId uint
3747-}
3748-
3749-func idsFromPersist(accountId uint) (ids reportedIdMap, err error) {
3750- err = plugins.FromPersist(pluginName, accountId, &ids)
3751- if err != nil {
3752- return nil, err
3753- }
3754- // discard old ids
3755- timestamp := time.Now()
3756- for k, v := range ids {
3757- delta := timestamp.Sub(v)
3758- if delta > trackDelta {
3759- log.Print("gmail plugin ", accountId, ": deleting ", k, " as ", delta, " is greater than ", trackDelta)
3760- delete(ids, k)
3761- }
3762- }
3763- return ids, nil
3764-}
3765-
3766-func (ids reportedIdMap) persist(accountId uint) (err error) {
3767- err = plugins.Persist(pluginName, accountId, ids)
3768- if err != nil {
3769- log.Print("gmail plugin ", accountId, ": failed to save state: ", err)
3770- }
3771- return nil
3772-}
3773-
3774-func New(accountId uint) *GmailPlugin {
3775- reportedIds, err := idsFromPersist(accountId)
3776- if err != nil {
3777- log.Print("gmail plugin ", accountId, ": cannot load previous state from storage: ", err)
3778- } else {
3779- log.Print("gmail plugin ", accountId, ": last state loaded from storage")
3780- }
3781- return &GmailPlugin{reportedIds: reportedIds, accountId: accountId}
3782-}
3783-
3784-func (p *GmailPlugin) ApplicationId() plugins.ApplicationId {
3785- return plugins.ApplicationId(APP_ID)
3786-}
3787-
3788-func (p *GmailPlugin) Poll(authData *accounts.AuthData) ([]*plugins.PushMessageBatch, error) {
3789- // This envvar check is to ease testing.
3790- if token := os.Getenv("ACCOUNT_POLLD_TOKEN_GMAIL"); token != "" {
3791- authData.AccessToken = token
3792- }
3793-
3794- resp, err := p.requestMessageList(authData.AccessToken)
3795- if err != nil {
3796- return nil, err
3797- }
3798- messages, err := p.parseMessageListResponse(resp)
3799- if err != nil {
3800- return nil, err
3801- }
3802-
3803- // TODO use the batching API defined in https://developers.google.com/gmail/api/guides/batch
3804- for i := range messages {
3805- resp, err := p.requestMessage(messages[i].Id, authData.AccessToken)
3806- if err != nil {
3807- return nil, err
3808- }
3809- messages[i], err = p.parseMessageResponse(resp)
3810- if err != nil {
3811- return nil, err
3812- }
3813- }
3814- notif, err := p.createNotifications(messages)
3815- if err != nil {
3816- return nil, err
3817- }
3818- return []*plugins.PushMessageBatch{
3819- &plugins.PushMessageBatch{
3820- Messages: notif,
3821- Limit: individualNotificationsLimit,
3822- OverflowHandler: p.handleOverflow,
3823- Tag: "dekko",
3824- }}, nil
3825-
3826-}
3827-
3828-func (p *GmailPlugin) reported(id string) bool {
3829- _, ok := p.reportedIds[id]
3830- return ok
3831-}
3832-
3833-func (p *GmailPlugin) createNotifications(messages []message) ([]*plugins.PushMessage, error) {
3834- timestamp := time.Now()
3835- pushMsgMap := make(pushes)
3836-
3837- for _, msg := range messages {
3838- hdr := msg.Payload.mapHeaders()
3839-
3840- from := hdr[hdrFROM]
3841- var avatarPath string
3842-
3843- emailAddress, err := mail.ParseAddress(from)
3844- if err != nil {
3845- // If the email address contains non-ascii characters, we get an
3846- // error so we're going to try again, this time mangling the name
3847- // by removing all non-ascii characters. We only care about the email
3848- // address here anyway.
3849- // XXX: We can't check the error message due to [1]: the error
3850- // message is different in go < 1.3 and > 1.5.
3851- // [1] https://github.com/golang/go/issues/12492
3852- mangledAddr := nonAsciiChars.ReplaceAllString(from, "")
3853- mangledEmail, mangledParseError := mail.ParseAddress(mangledAddr)
3854- if mangledParseError == nil {
3855- emailAddress = mangledEmail
3856- }
3857- } else if emailAddress.Name != "" {
3858- // We only want the Name if the first ParseAddress
3859- // call was successful. I.e. we do not want the name
3860- // from a mangled email address.
3861- from = emailAddress.Name
3862- }
3863-
3864- if emailAddress != nil {
3865- avatarPath = qtcontact.GetAvatar(emailAddress.Address)
3866- // If icon path starts with a path separator, assume local file path,
3867- // encode it and prepend file scheme defined in RFC 1738.
3868- if strings.HasPrefix(avatarPath, string(os.PathSeparator)) {
3869- avatarPath = url.QueryEscape(avatarPath)
3870- avatarPath = "file://" + avatarPath
3871- }
3872- }
3873-
3874- msgStamp := hdr.getTimestamp()
3875-
3876- if _, ok := pushMsgMap[msg.ThreadId]; ok {
3877- // TRANSLATORS: the %s is an appended "from" corresponding to an specific email thread
3878- pushMsgMap[msg.ThreadId].Notification.Card.Summary += fmt.Sprintf(gettext.Gettext(", %s"), from)
3879- } else if timestamp.Sub(msgStamp) < timeDelta {
3880- // TRANSLATORS: the %s is the "from" header corresponding to a specific email
3881- summary := fmt.Sprintf(gettext.Gettext("%s"), from)
3882- // TRANSLATORS: the first %s refers to the email "subject", the second %s refers "from"
3883- body := fmt.Sprintf(gettext.Gettext("%s\n%s"), hdr[hdrSUBJECT], msg.Snippet)
3884- // fmt with label personal and threadId
3885- action := fmt.Sprintf(dekkoDispatchUrl, p.accountId, "INBOX", msg.Id)
3886- epoch := hdr.getEpoch()
3887- pushMsgMap[msg.ThreadId] = plugins.NewStandardPushMessage(summary, body, action, avatarPath, epoch)
3888- } else {
3889- log.Print("gmail plugin ", p.accountId, ": skipping message id ", msg.Id, " with date ", msgStamp, " older than ", timeDelta)
3890- }
3891- }
3892- pushMsg := make([]*plugins.PushMessage, 0, len(pushMsgMap))
3893- for _, v := range pushMsgMap {
3894- pushMsg = append(pushMsg, v)
3895- }
3896- return pushMsg, nil
3897-
3898-}
3899-func (p *GmailPlugin) handleOverflow(pushMsg []*plugins.PushMessage) *plugins.PushMessage {
3900- // TODO it would probably be better to grab the estimate that google returns in the message list.
3901- approxUnreadMessages := len(pushMsg)
3902-
3903- // TRANSLATORS: the %d refers to the number of new email messages.
3904- summary := fmt.Sprintf(gettext.Gettext("You have %d new messages"), approxUnreadMessages)
3905-
3906- body := ""
3907-
3908- // fmt with label personal and no threadId
3909- action := fmt.Sprintf(dekkoDispatchUrl, p.accountId, "INBOX")
3910- epoch := time.Now().Unix()
3911-
3912- return plugins.NewStandardPushMessage(summary, body, action, "", epoch)
3913-}
3914-
3915-func (p *GmailPlugin) parseMessageListResponse(resp *http.Response) ([]message, error) {
3916- defer resp.Body.Close()
3917- decoder := json.NewDecoder(resp.Body)
3918-
3919- if resp.StatusCode != http.StatusOK {
3920- var errResp errorResp
3921- if err := decoder.Decode(&errResp); err != nil {
3922- return nil, err
3923- }
3924- if errResp.Err.Code == 401 {
3925- return nil, plugins.ErrTokenExpired
3926- }
3927- return nil, &errResp
3928- }
3929-
3930- var messages messageList
3931- if err := decoder.Decode(&messages); err != nil {
3932- return nil, err
3933- }
3934-
3935- filteredMsg := p.messageListFilter(messages.Messages)
3936-
3937- return filteredMsg, nil
3938-}
3939-
3940-// messageListFilter returns a subset of unread messages where the subset
3941-// depends on not being in reportedIds. Before returning, reportedIds is
3942-// updated with the new list of unread messages.
3943-func (p *GmailPlugin) messageListFilter(messages []message) []message {
3944- sort.Sort(byId(messages))
3945- var reportMsg []message
3946- var ids = make(reportedIdMap)
3947-
3948- for _, msg := range messages {
3949- if !p.reported(msg.Id) {
3950- reportMsg = append(reportMsg, msg)
3951- }
3952- ids[msg.Id] = time.Now()
3953- }
3954- p.reportedIds = ids
3955- p.reportedIds.persist(p.accountId)
3956- return reportMsg
3957-}
3958-
3959-func (p *GmailPlugin) parseMessageResponse(resp *http.Response) (message, error) {
3960- defer resp.Body.Close()
3961- decoder := json.NewDecoder(resp.Body)
3962-
3963- if resp.StatusCode != http.StatusOK {
3964- var errResp errorResp
3965- if err := decoder.Decode(&errResp); err != nil {
3966- return message{}, err
3967- }
3968- return message{}, &errResp
3969- }
3970-
3971- var msg message
3972- if err := decoder.Decode(&msg); err != nil {
3973- return message{}, err
3974- }
3975-
3976- return msg, nil
3977-}
3978-
3979-func (p *GmailPlugin) requestMessage(id, accessToken string) (*http.Response, error) {
3980- u, err := baseUrl.Parse("messages/" + id)
3981- if err != nil {
3982- return nil, err
3983- }
3984-
3985- query := u.Query()
3986- // only request specific fields
3987- query.Add("fields", "snippet,threadId,id,payload/headers")
3988- // get the full message to get From and Subject from headers
3989- query.Add("format", "full")
3990- u.RawQuery = query.Encode()
3991-
3992- req, err := http.NewRequest("GET", u.String(), nil)
3993- if err != nil {
3994- return nil, err
3995- }
3996- req.Header.Set("Authorization", "Bearer "+accessToken)
3997-
3998- return http.DefaultClient.Do(req)
3999-}
4000-
4001-func (p *GmailPlugin) requestMessageList(accessToken string) (*http.Response, error) {
4002- u, err := baseUrl.Parse("messages")
4003- if err != nil {
4004- return nil, err
4005- }
4006-
4007- query := u.Query()
4008-
4009- // get all unread inbox emails received after
4010- // the last time we checked. If this is the first
4011- // time we check, get unread emails after timeDelta
4012- query.Add("q", fmt.Sprintf("is:unread in:inbox newer_than:%s", relativeTimeDelta))
4013- u.RawQuery = query.Encode()
4014-
4015- req, err := http.NewRequest("GET", u.String(), nil)
4016- if err != nil {
4017- return nil, err
4018- }
4019- req.Header.Set("Authorization", "Bearer "+accessToken)
4020-
4021- return http.DefaultClient.Do(req)
4022-}
4023
4024=== removed directory 'plugins/gcalendar'
4025=== removed file 'plugins/gcalendar/api.go'
4026--- plugins/gcalendar/api.go 2016-04-14 14:58:10 +0000
4027+++ plugins/gcalendar/api.go 1970-01-01 00:00:00 +0000
4028@@ -1,54 +0,0 @@
4029-/*
4030- Copyright 2014 Canonical Ltd.
4031-
4032- This program is free software: you can redistribute it and/or modify it
4033- under the terms of the GNU General Public License version 3, as published
4034- by the Free Software Foundation.
4035-
4036- This program is distributed in the hope that it will be useful, but
4037- WITHOUT ANY WARRANTY; without even the implied warranties of
4038- MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
4039- PURPOSE. See the GNU General Public License for more details.
4040-
4041- You should have received a copy of the GNU General Public License along
4042- with this program. If not, see <http://www.gnu.org/licenses/>.
4043-*/
4044-
4045-package gcalendar
4046-
4047-import (
4048- "fmt"
4049-)
4050-
4051-// eventList holds a response to call to Calendar.events: list
4052-// defined in https://developers.google.com/google-apps/calendar/v3/reference/events/list#response
4053-type eventList struct {
4054- // Messages holds a list of message.
4055- Events []event `json:"items"`
4056-}
4057-
4058-// event holds the event data response for a Calendar.event.
4059-// The full definition of a message is defined in
4060-// https://developers.google.com/google-apps/calendar/v3/reference/events#resource-representations
4061-type event struct {
4062- // Id is the immutable ID of the message.
4063- Etag string `json:"etag"`
4064- // ThreadId is the ID of the thread the message belongs to.
4065- Summary string `json:"summary"`
4066-}
4067-
4068-func (e event) String() string {
4069- return fmt.Sprintf("Id: %s, snippet: '%s'\n", e.Etag, e.Summary)
4070-}
4071-
4072-type errorResp struct {
4073- Err struct {
4074- Code uint64 `json:"code"`
4075- Message string `json:"message"`
4076- Errors []struct {
4077- Domain string `json:"domain"`
4078- Reason string `json:"reason"`
4079- Message string `json:"message"`
4080- } `json:"errors"`
4081- } `json:"error"`
4082-}
4083
4084=== removed file 'plugins/gcalendar/gcalendar.go'
4085--- plugins/gcalendar/gcalendar.go 2016-08-02 14:16:26 +0000
4086+++ plugins/gcalendar/gcalendar.go 1970-01-01 00:00:00 +0000
4087@@ -1,189 +0,0 @@
4088-/*
4089- Copyright 2016 Canonical Ltd.
4090-
4091- This program is free software: you can redistribute it and/or modify it
4092- under the terms of the GNU General Public License version 3, as published
4093- by the Free Software Foundation.
4094-
4095- This program is distributed in the hope that it will be useful, but
4096- WITHOUT ANY WARRANTY; without even the implied warranties of
4097- MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
4098- PURPOSE. See the GNU General Public License for more details.
4099-
4100- You should have received a copy of the GNU General Public License along
4101- with this program. If not, see <http://www.gnu.org/licenses/>.
4102-*/
4103-
4104-package gcalendar
4105-
4106-import (
4107- "encoding/json"
4108- "log"
4109- "net/http"
4110- "net/url"
4111- "os"
4112-
4113- "launchpad.net/account-polld/accounts"
4114- "launchpad.net/account-polld/plugins"
4115- "launchpad.net/account-polld/syncmonitor"
4116-)
4117-
4118-const (
4119- APP_ID = "com.ubuntu.calendar_calendar"
4120- pluginName = "gcalendar"
4121-)
4122-
4123-var baseUrl, _ = url.Parse("https://www.googleapis.com/calendar/v3/calendars/")
4124-
4125-type GCalendarPlugin struct {
4126- accountId uint
4127-}
4128-
4129-func New(accountId uint) *GCalendarPlugin {
4130- return &GCalendarPlugin{accountId: accountId}
4131-}
4132-
4133-func (p *GCalendarPlugin) ApplicationId() plugins.ApplicationId {
4134- return plugins.ApplicationId(APP_ID)
4135-}
4136-
4137-func (p *GCalendarPlugin) Poll(authData *accounts.AuthData) ([]*plugins.PushMessageBatch, error) {
4138- // This envvar check is to ease testing.
4139- if token := os.Getenv("ACCOUNT_POLLD_TOKEN_GCALENDAR"); token != "" {
4140- log.Print("calendar: Using token from: ACCOUNT_POLLD_TOKEN_GCALENDAR env var")
4141- authData.AccessToken = token
4142- }
4143-
4144- log.Print("calendar: Check calendar changes for account:", p.accountId)
4145-
4146- syncMonitor := syncmonitor.NewSyncMonitor()
4147- if syncMonitor == nil {
4148- log.Print("calendar: Sync monitor not available yet.")
4149- return nil, nil
4150- }
4151-
4152- state, err := syncMonitor.State()
4153- if err != nil {
4154- log.Print("calendar: Fail to retrieve sync monitor state ", err)
4155- return nil, nil
4156- }
4157- if state != "idle" {
4158- log.Print("calendar: Sync monitor is not on 'idle' state, try later!")
4159- return nil, nil
4160- }
4161-
4162- calendars, err := syncMonitor.ListCalendarsByAccount(p.accountId)
4163- if err != nil {
4164- log.Print("calendar: Calendar plugin ", p.accountId, ": cannot load calendars: ", err)
4165- return nil, nil
4166- }
4167-
4168- var calendarsToSync []string
4169- log.Print("calendar: Number of calendars for account:", p.accountId, " size:", len(calendars))
4170-
4171- for id, calendar := range calendars {
4172- lastSyncDate, err := syncMonitor.LastSyncDate(p.accountId, id)
4173- if err != nil {
4174- log.Print("\tcalendar: ", calendar, ", cannot load previous sync date: ", err, ". Try next time.")
4175- continue
4176- } else {
4177- log.Print("\tcalendar: ", calendar, " Id: ", id, ": last sync date: ", lastSyncDate)
4178- }
4179-
4180- var needSync bool
4181- needSync = (len(lastSyncDate) == 0)
4182-
4183- if !needSync {
4184- resp, err := p.requestChanges(authData.AccessToken, id, lastSyncDate)
4185- if err != nil {
4186- log.Print("\tcalendar: ERROR: Fail to query for changes: ", err)
4187- continue
4188- }
4189-
4190- messages, err := p.parseChangesResponse(resp)
4191- if err != nil {
4192- log.Print("\tcalendar: ERROR: Fail to parse changes: ", err)
4193- if err == plugins.ErrTokenExpired {
4194- log.Print("\t\tcalendar: Abort poll")
4195- return nil, err
4196- } else {
4197- continue
4198- }
4199- }
4200- needSync = (len(messages) > 0)
4201- }
4202-
4203- if needSync {
4204- log.Print("\tcalendar: Calendar needs sync: ", calendar)
4205- calendarsToSync = append(calendarsToSync, id)
4206- } else {
4207- log.Print("\tcalendar: Found no calendar updates for account: ", p.accountId, " calendar: ", calendar)
4208- }
4209- }
4210-
4211- if len(calendarsToSync) > 0 {
4212- log.Print("calendar: Request account sync")
4213- err = syncMonitor.SyncAccount(p.accountId, calendarsToSync)
4214- if err != nil {
4215- log.Print("calendar: ERROR: Fail to start account sync ", p.accountId, " message: ", err)
4216- }
4217- }
4218-
4219- return nil, nil
4220-}
4221-
4222-func (p *GCalendarPlugin) parseChangesResponse(resp *http.Response) ([]event, error) {
4223- defer resp.Body.Close()
4224- decoder := json.NewDecoder(resp.Body)
4225-
4226- if resp.StatusCode != http.StatusOK {
4227- var errResp errorResp
4228- log.Print("calendar: Invalid response:", errResp.Err.Code)
4229- if err := decoder.Decode(&errResp); err != nil {
4230- return nil, err
4231- }
4232- if errResp.Err.Code == 401 {
4233- return nil, plugins.ErrTokenExpired
4234- }
4235- return nil, nil
4236- }
4237-
4238- var events eventList
4239- if err := decoder.Decode(&events); err != nil {
4240- log.Print("calendar: Fail to decode")
4241- return nil, err
4242- }
4243-
4244- for _, ev := range events.Events {
4245- log.Print("calendar: Found event: ", ev.Etag, ev.Summary)
4246- }
4247-
4248- return events.Events, nil
4249-}
4250-
4251-func (p *GCalendarPlugin) requestChanges(accessToken string, calendar string, lastSyncDate string) (*http.Response, error) {
4252- u, err := baseUrl.Parse("")
4253- if err != nil {
4254- return nil, err
4255- }
4256- u.Path += calendar + "/events"
4257-
4258- //GET https://www.googleapis.com/calendar/v3/calendars/<calendar>/events?showDeleted=true&singleEvents=true&updatedMin=2016-04-06T10%3A00%3A00.00Z&fields=description%2Citems(description%2Cetag%2Csummary)&key={YOUR_API_KEY}
4259- query := u.Query()
4260- query.Add("showDeleted", "true")
4261- query.Add("singleEvents", "true")
4262- query.Add("fields", "description,items(summary,etag)")
4263- query.Add("maxResults", "1")
4264- if len(lastSyncDate) > 0 {
4265- query.Add("updatedMin", lastSyncDate)
4266- }
4267- u.RawQuery = query.Encode()
4268-
4269- req, err := http.NewRequest("GET", u.String(), nil)
4270- if err != nil {
4271- return nil, err
4272- }
4273- req.Header.Set("Authorization", "Bearer "+accessToken)
4274-
4275- return http.DefaultClient.Do(req)
4276-}
4277
4278=== removed directory 'plugins/gmail'
4279=== removed file 'plugins/gmail/api.go'
4280--- plugins/gmail/api.go 2015-03-20 14:34:48 +0000
4281+++ plugins/gmail/api.go 1970-01-01 00:00:00 +0000
4282@@ -1,127 +0,0 @@
4283-/*
4284- Copyright 2014 Canonical Ltd.
4285-
4286- This program is free software: you can redistribute it and/or modify it
4287- under the terms of the GNU General Public License version 3, as published
4288- by the Free Software Foundation.
4289-
4290- This program is distributed in the hope that it will be useful, but
4291- WITHOUT ANY WARRANTY; without even the implied warranties of
4292- MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
4293- PURPOSE. See the GNU General Public License for more details.
4294-
4295- You should have received a copy of the GNU General Public License along
4296- with this program. If not, see <http://www.gnu.org/licenses/>.
4297-*/
4298-
4299-package gmail
4300-
4301-import (
4302- "fmt"
4303- "time"
4304-
4305- "launchpad.net/account-polld/plugins"
4306-)
4307-
4308-const gmailTime = "Mon, 2 Jan 2006 15:04:05 -0700"
4309-
4310-type pushes map[string]*plugins.PushMessage
4311-type headers map[string]string
4312-
4313-// messageList holds a response to call to Users.messages: list
4314-// defined in https://developers.google.com/gmail/api/v1/reference/users/messages/list
4315-type messageList struct {
4316- // Messages holds a list of message.
4317- Messages []message `json:"messages"`
4318- // NextPageToken is used to retrieve the next page of results in the list.
4319- NextPageToken string `json:"nextPageToken"`
4320- // ResultSizeEstimage is the estimated total number of results.
4321- ResultSizeEstimage uint64 `json:"resultSizeEstimate"`
4322-}
4323-
4324-// message holds a partial response for a Users.messages.
4325-// The full definition of a message is defined in
4326-// https://developers.google.com/gmail/api/v1/reference/users/messages#resource
4327-type message struct {
4328- // Id is the immutable ID of the message.
4329- Id string `json:"id"`
4330- // ThreadId is the ID of the thread the message belongs to.
4331- ThreadId string `json:"threadId"`
4332- // HistoryId is the ID of the last history record that modified
4333- // this message.
4334- HistoryId string `json:"historyId"`
4335- // Snippet is a short part of the message text. This text is
4336- // used for the push message summary.
4337- Snippet string `json:"snippet"`
4338- // Payload represents the message payload.
4339- Payload payload `json:"payload"`
4340-}
4341-
4342-func (m message) String() string {
4343- return fmt.Sprintf("Id: %d, snippet: '%s'\n", m.Id, m.Snippet[:10])
4344-}
4345-
4346-// ById implements sort.Interface for []message based on
4347-// the Id field.
4348-type byId []message
4349-
4350-func (m byId) Len() int { return len(m) }
4351-func (m byId) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
4352-func (m byId) Less(i, j int) bool { return m[i].Id < m[j].Id }
4353-
4354-// payload represents the message payload.
4355-type payload struct {
4356- Headers []messageHeader `json:"headers"`
4357-}
4358-
4359-func (p *payload) mapHeaders() headers {
4360- headers := make(map[string]string)
4361- for _, hdr := range p.Headers {
4362- headers[hdr.Name] = hdr.Value
4363- }
4364- return headers
4365-}
4366-
4367-func (hdr headers) getTimestamp() time.Time {
4368- timestamp, ok := hdr[hdrDATE]
4369- if !ok {
4370- return time.Now()
4371- }
4372-
4373- if t, err := time.Parse(gmailTime, timestamp); err == nil {
4374- return t
4375- }
4376- return time.Now()
4377-}
4378-
4379-func (hdr headers) getEpoch() int64 {
4380- return hdr.getTimestamp().Unix()
4381-}
4382-
4383-// messageHeader represents the message headers.
4384-type messageHeader struct {
4385- Name string `json:"name"`
4386- Value string `json:"value"`
4387-}
4388-
4389-type errorResp struct {
4390- Err struct {
4391- Code uint64 `json:"code"`
4392- Message string `json:"message"`
4393- Errors []struct {
4394- Domain string `json:"domain"`
4395- Reason string `json:"reason"`
4396- Message string `json:"message"`
4397- } `json:"errors"`
4398- } `json:"error"`
4399-}
4400-
4401-func (err *errorResp) Error() string {
4402- return fmt.Sprint("backend response:", err.Err.Message)
4403-}
4404-
4405-const (
4406- hdrDATE = "Date"
4407- hdrSUBJECT = "Subject"
4408- hdrFROM = "From"
4409-)
4410
4411=== removed file 'plugins/gmail/gmail.go'
4412--- plugins/gmail/gmail.go 2016-04-20 11:10:17 +0000
4413+++ plugins/gmail/gmail.go 1970-01-01 00:00:00 +0000
4414@@ -1,346 +0,0 @@
4415-/*
4416- Copyright 2014 Canonical Ltd.
4417-
4418- This program is free software: you can redistribute it and/or modify it
4419- under the terms of the GNU General Public License version 3, as published
4420- by the Free Software Foundation.
4421-
4422- This program is distributed in the hope that it will be useful, but
4423- WITHOUT ANY WARRANTY; without even the implied warranties of
4424- MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
4425- PURPOSE. See the GNU General Public License for more details.
4426-
4427- You should have received a copy of the GNU General Public License along
4428- with this program. If not, see <http://www.gnu.org/licenses/>.
4429-*/
4430-
4431-package gmail
4432-
4433-import (
4434- "encoding/json"
4435- "fmt"
4436- "net/http"
4437- "net/mail"
4438- "net/url"
4439- "os"
4440- "regexp"
4441- "sort"
4442- "strings"
4443- "time"
4444-
4445- "log"
4446-
4447- "launchpad.net/account-polld/accounts"
4448- "launchpad.net/account-polld/gettext"
4449- "launchpad.net/account-polld/plugins"
4450- "launchpad.net/account-polld/qtcontact"
4451-)
4452-
4453-const (
4454- APP_ID = "com.ubuntu.developer.webapps.webapp-gmail_webapp-gmail"
4455- gmailDispatchUrl = "https://mail.google.com/mail/mu/mp/#cv/priority/^smartlabel_%s/%s"
4456- // If there's more than 10 emails in one batch, we don't show 10 notification
4457- // bubbles, but instead show one summary. We always show all notifications in the
4458- // indicator.
4459- individualNotificationsLimit = 10
4460- pluginName = "gmail"
4461-)
4462-
4463-type reportedIdMap map[string]time.Time
4464-
4465-var baseUrl, _ = url.Parse("https://www.googleapis.com/gmail/v1/users/me/")
4466-
4467-// timeDelta defines how old messages can be to be reported.
4468-var timeDelta = time.Duration(time.Hour * 24)
4469-
4470-// trackDelta defines how old messages can be before removed from tracking
4471-var trackDelta = time.Duration(time.Hour * 24 * 7)
4472-
4473-// relativeTimeDelta is the same as timeDelta
4474-var relativeTimeDelta string = "1d"
4475-
4476-// regexp for identifying non-ascii characters
4477-var nonAsciiChars, _ = regexp.Compile("[^\x00-\x7F]")
4478-
4479-type GmailPlugin struct {
4480- // reportedIds holds the messages that have already been notified. This
4481- // approach is taken against timestamps as it avoids needing to call
4482- // get on the message.
4483- reportedIds reportedIdMap
4484- accountId uint
4485-}
4486-
4487-func idsFromPersist(accountId uint) (ids reportedIdMap, err error) {
4488- err = plugins.FromPersist(pluginName, accountId, &ids)
4489- if err != nil {
4490- return nil, err
4491- }
4492- // discard old ids
4493- timestamp := time.Now()
4494- for k, v := range ids {
4495- delta := timestamp.Sub(v)
4496- if delta > trackDelta {
4497- log.Print("gmail plugin ", accountId, ": deleting ", k, " as ", delta, " is greater than ", trackDelta)
4498- delete(ids, k)
4499- }
4500- }
4501- return ids, nil
4502-}
4503-
4504-func (ids reportedIdMap) persist(accountId uint) (err error) {
4505- err = plugins.Persist(pluginName, accountId, ids)
4506- if err != nil {
4507- log.Print("gmail plugin ", accountId, ": failed to save state: ", err)
4508- }
4509- return nil
4510-}
4511-
4512-func New(accountId uint) *GmailPlugin {
4513- reportedIds, err := idsFromPersist(accountId)
4514- if err != nil {
4515- log.Print("gmail plugin ", accountId, ": cannot load previous state from storage: ", err)
4516- } else {
4517- log.Print("gmail plugin ", accountId, ": last state loaded from storage")
4518- }
4519- return &GmailPlugin{reportedIds: reportedIds, accountId: accountId}
4520-}
4521-
4522-func (p *GmailPlugin) ApplicationId() plugins.ApplicationId {
4523- return plugins.ApplicationId(APP_ID)
4524-}
4525-
4526-func (p *GmailPlugin) Poll(authData *accounts.AuthData) ([]*plugins.PushMessageBatch, error) {
4527- // This envvar check is to ease testing.
4528- if token := os.Getenv("ACCOUNT_POLLD_TOKEN_GMAIL"); token != "" {
4529- authData.AccessToken = token
4530- }
4531-
4532- resp, err := p.requestMessageList(authData.AccessToken)
4533- if err != nil {
4534- return nil, err
4535- }
4536- messages, err := p.parseMessageListResponse(resp)
4537- if err != nil {
4538- return nil, err
4539- }
4540-
4541- // TODO use the batching API defined in https://developers.google.com/gmail/api/guides/batch
4542- for i := range messages {
4543- resp, err := p.requestMessage(messages[i].Id, authData.AccessToken)
4544- if err != nil {
4545- return nil, err
4546- }
4547- messages[i], err = p.parseMessageResponse(resp)
4548- if err != nil {
4549- return nil, err
4550- }
4551- }
4552- notif, err := p.createNotifications(messages)
4553- if err != nil {
4554- return nil, err
4555- }
4556- return []*plugins.PushMessageBatch{
4557- &plugins.PushMessageBatch{
4558- Messages: notif,
4559- Limit: individualNotificationsLimit,
4560- OverflowHandler: p.handleOverflow,
4561- Tag: "gmail",
4562- }}, nil
4563-
4564-}
4565-
4566-func (p *GmailPlugin) reported(id string) bool {
4567- _, ok := p.reportedIds[id]
4568- return ok
4569-}
4570-
4571-func (p *GmailPlugin) createNotifications(messages []message) ([]*plugins.PushMessage, error) {
4572- timestamp := time.Now()
4573- pushMsgMap := make(pushes)
4574-
4575- for _, msg := range messages {
4576- hdr := msg.Payload.mapHeaders()
4577-
4578- from := hdr[hdrFROM]
4579- var avatarPath string
4580-
4581- emailAddress, err := mail.ParseAddress(from)
4582- if err != nil {
4583- // If the email address contains non-ascii characters, we get an
4584- // error so we're going to try again, this time mangling the name
4585- // by removing all non-ascii characters. We only care about the email
4586- // address here anyway.
4587- // XXX: We can't check the error message due to [1]: the error
4588- // message is different in go < 1.3 and > 1.5.
4589- // [1] https://github.com/golang/go/issues/12492
4590- mangledAddr := nonAsciiChars.ReplaceAllString(from, "")
4591- mangledEmail, mangledParseError := mail.ParseAddress(mangledAddr)
4592- if mangledParseError == nil {
4593- emailAddress = mangledEmail
4594- }
4595- } else if emailAddress.Name != "" {
4596- // We only want the Name if the first ParseAddress
4597- // call was successful. I.e. we do not want the name
4598- // from a mangled email address.
4599- from = emailAddress.Name
4600- }
4601-
4602- if emailAddress != nil {
4603- avatarPath = qtcontact.GetAvatar(emailAddress.Address)
4604- // If icon path starts with a path separator, assume local file path,
4605- // encode it and prepend file scheme defined in RFC 1738.
4606- if strings.HasPrefix(avatarPath, string(os.PathSeparator)) {
4607- avatarPath = url.QueryEscape(avatarPath)
4608- avatarPath = "file://" + avatarPath
4609- }
4610- }
4611-
4612- msgStamp := hdr.getTimestamp()
4613-
4614- if _, ok := pushMsgMap[msg.ThreadId]; ok {
4615- // TRANSLATORS: the %s is an appended "from" corresponding to an specific email thread
4616- pushMsgMap[msg.ThreadId].Notification.Card.Summary += fmt.Sprintf(gettext.Gettext(", %s"), from)
4617- } else if timestamp.Sub(msgStamp) < timeDelta {
4618- // TRANSLATORS: the %s is the "from" header corresponding to a specific email
4619- summary := fmt.Sprintf(gettext.Gettext("%s"), from)
4620- // TRANSLATORS: the first %s refers to the email "subject", the second %s refers "from"
4621- body := fmt.Sprintf(gettext.Gettext("%s\n%s"), hdr[hdrSUBJECT], msg.Snippet)
4622- // fmt with label personal and threadId
4623- action := fmt.Sprintf(gmailDispatchUrl, "personal", msg.ThreadId)
4624- epoch := hdr.getEpoch()
4625- pushMsgMap[msg.ThreadId] = plugins.NewStandardPushMessage(summary, body, action, avatarPath, epoch)
4626- } else {
4627- log.Print("gmail plugin ", p.accountId, ": skipping message id ", msg.Id, " with date ", msgStamp, " older than ", timeDelta)
4628- }
4629- }
4630- pushMsg := make([]*plugins.PushMessage, 0, len(pushMsgMap))
4631- for _, v := range pushMsgMap {
4632- pushMsg = append(pushMsg, v)
4633- }
4634- return pushMsg, nil
4635-
4636-}
4637-func (p *GmailPlugin) handleOverflow(pushMsg []*plugins.PushMessage) *plugins.PushMessage {
4638- // TODO it would probably be better to grab the estimate that google returns in the message list.
4639- approxUnreadMessages := len(pushMsg)
4640-
4641- // TRANSLATORS: the %d refers to the number of new email messages.
4642- summary := fmt.Sprintf(gettext.Gettext("You have %d new messages"), approxUnreadMessages)
4643-
4644- body := ""
4645-
4646- // fmt with label personal and no threadId
4647- action := fmt.Sprintf(gmailDispatchUrl, "personal")
4648- epoch := time.Now().Unix()
4649-
4650- return plugins.NewStandardPushMessage(summary, body, action, "", epoch)
4651-}
4652-
4653-func (p *GmailPlugin) parseMessageListResponse(resp *http.Response) ([]message, error) {
4654- defer resp.Body.Close()
4655- decoder := json.NewDecoder(resp.Body)
4656-
4657- if resp.StatusCode != http.StatusOK {
4658- var errResp errorResp
4659- if err := decoder.Decode(&errResp); err != nil {
4660- return nil, err
4661- }
4662- if errResp.Err.Code == 401 {
4663- return nil, plugins.ErrTokenExpired
4664- }
4665- return nil, &errResp
4666- }
4667-
4668- var messages messageList
4669- if err := decoder.Decode(&messages); err != nil {
4670- return nil, err
4671- }
4672-
4673- filteredMsg := p.messageListFilter(messages.Messages)
4674-
4675- return filteredMsg, nil
4676-}
4677-
4678-// messageListFilter returns a subset of unread messages where the subset
4679-// depends on not being in reportedIds. Before returning, reportedIds is
4680-// updated with the new list of unread messages.
4681-func (p *GmailPlugin) messageListFilter(messages []message) []message {
4682- sort.Sort(byId(messages))
4683- var reportMsg []message
4684- var ids = make(reportedIdMap)
4685-
4686- for _, msg := range messages {
4687- if !p.reported(msg.Id) {
4688- reportMsg = append(reportMsg, msg)
4689- }
4690- ids[msg.Id] = time.Now()
4691- }
4692- p.reportedIds = ids
4693- p.reportedIds.persist(p.accountId)
4694- return reportMsg
4695-}
4696-
4697-func (p *GmailPlugin) parseMessageResponse(resp *http.Response) (message, error) {
4698- defer resp.Body.Close()
4699- decoder := json.NewDecoder(resp.Body)
4700-
4701- if resp.StatusCode != http.StatusOK {
4702- var errResp errorResp
4703- if err := decoder.Decode(&errResp); err != nil {
4704- return message{}, err
4705- }
4706- return message{}, &errResp
4707- }
4708-
4709- var msg message
4710- if err := decoder.Decode(&msg); err != nil {
4711- return message{}, err
4712- }
4713-
4714- return msg, nil
4715-}
4716-
4717-func (p *GmailPlugin) requestMessage(id, accessToken string) (*http.Response, error) {
4718- u, err := baseUrl.Parse("messages/" + id)
4719- if err != nil {
4720- return nil, err
4721- }
4722-
4723- query := u.Query()
4724- // only request specific fields
4725- query.Add("fields", "snippet,threadId,id,payload/headers")
4726- // get the full message to get From and Subject from headers
4727- query.Add("format", "full")
4728- u.RawQuery = query.Encode()
4729-
4730- req, err := http.NewRequest("GET", u.String(), nil)
4731- if err != nil {
4732- return nil, err
4733- }
4734- req.Header.Set("Authorization", "Bearer "+accessToken)
4735-
4736- return http.DefaultClient.Do(req)
4737-}
4738-
4739-func (p *GmailPlugin) requestMessageList(accessToken string) (*http.Response, error) {
4740- u, err := baseUrl.Parse("messages")
4741- if err != nil {
4742- return nil, err
4743- }
4744-
4745- query := u.Query()
4746-
4747- // get all unread inbox emails received after
4748- // the last time we checked. If this is the first
4749- // time we check, get unread emails after timeDelta
4750- query.Add("q", fmt.Sprintf("is:unread in:inbox newer_than:%s", relativeTimeDelta))
4751- u.RawQuery = query.Encode()
4752-
4753- req, err := http.NewRequest("GET", u.String(), nil)
4754- if err != nil {
4755- return nil, err
4756- }
4757- req.Header.Set("Authorization", "Bearer "+accessToken)
4758-
4759- return http.DefaultClient.Do(req)
4760-}
4761
4762=== removed file 'plugins/plugins.go'
4763--- plugins/plugins.go 2016-02-08 19:21:31 +0000
4764+++ plugins/plugins.go 1970-01-01 00:00:00 +0000
4765@@ -1,239 +0,0 @@
4766-/*
4767- Copyright 2014 Canonical Ltd.
4768-
4769- This program is free software: you can redistribute it and/or modify it
4770- under the terms of the GNU General Public License version 3, as published
4771- by the Free Software Foundation.
4772-
4773- This program is distributed in the hope that it will be useful, but
4774- WITHOUT ANY WARRANTY; without even the implied warranties of
4775- MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
4776- PURPOSE. See the GNU General Public License for more details.
4777-
4778- You should have received a copy of the GNU General Public License along
4779- with this program. If not, see <http://www.gnu.org/licenses/>.
4780-*/
4781-
4782-package plugins
4783-
4784-import (
4785- "bufio"
4786- "encoding/json"
4787- "errors"
4788- "fmt"
4789- "os"
4790- "path/filepath"
4791- "reflect"
4792-
4793- "launchpad.net/account-polld/accounts"
4794- "launchpad.net/go-xdg/v0"
4795-)
4796-
4797-func init() {
4798- cmdName = filepath.Base(os.Args[0])
4799-}
4800-
4801-// Plugin is an interface which the plugins will adhere to for the poll
4802-// daemon to interact with.
4803-//
4804-// Poll interacts with the backend service with the means the plugin defines
4805-// and returns a list of Notifications to send to the Push service. If an
4806-// error occurs and is returned the daemon can decide to throttle the service.
4807-//
4808-// ApplicationId returns the APP_ID of the delivery target for Post Office.
4809-type Plugin interface {
4810- ApplicationId() ApplicationId
4811- Poll(*accounts.AuthData) ([]*PushMessageBatch, error)
4812-}
4813-
4814-// AuthTokens is a map with tokens the plugins are to use to make requests.
4815-type AuthTokens map[string]interface{}
4816-
4817-// ApplicationId represents the application id to direct posts to.
4818-// e.g.: com.ubuntu.diaspora_diaspora or com.ubuntu.diaspora_diaspora_1.0
4819-type ApplicationId string
4820-
4821-// NewStandardPushMessage creates a base Notification with common
4822-// components (members) setup.
4823-func NewStandardPushMessage(summary, body, action, icon string, epoch int64) *PushMessage {
4824- pm := &PushMessage{
4825- Notification: Notification{
4826- Card: &Card{
4827- Summary: summary,
4828- Body: body,
4829- Actions: []string{action},
4830- Icon: icon,
4831- Timestamp: epoch,
4832- Popup: true,
4833- Persist: true,
4834- },
4835- Sound: DefaultSound(),
4836- Vibrate: true,
4837- Tag: cmdName,
4838- },
4839- }
4840- return pm
4841-}
4842-
4843-// PushMessageBatch represents a logical grouping of PushMessages that
4844-// have a limit on the number of their notifications that want to be
4845-// presented to the user at the same time, and a way to handle the
4846-// overflow. All Notifications that are part of a Batch share the same
4847-// tag (Tag). ${Tag}-overflow is the overflow notification tag.
4848-//
4849-// TODO: support notifications sharing just the prefix (so the app can
4850-// tell them apart by tag).
4851-type PushMessageBatch struct {
4852- Messages []*PushMessage
4853- Limit int
4854- OverflowHandler func([]*PushMessage) *PushMessage
4855- Tag string
4856-}
4857-
4858-// PushMessage represents a data structure to be sent over to the
4859-// Post Office. It consists of a Notification and a Message.
4860-type PushMessage struct {
4861- // Message represents a JSON object that is passed as-is to the
4862- // application
4863- Message string `json:"message,omitempty"`
4864- // Notification (optional) describes the user-facing notifications
4865- // triggered by this push message.
4866- Notification Notification `json:"notification,omitempty"`
4867-}
4868-
4869-// Notification (optional) describes the user-facing notifications
4870-// triggered by this push message.
4871-type Notification struct {
4872- // Sound (optional) is the path to a sound file which can or
4873- // cannot be played depending on user preferences.
4874- Sound string `json:"sound,omitempty"`
4875- // Card represents a specific bubble to give to the user
4876- Card *Card `json:"card,omitempty"`
4877- // Vibrate is the haptic feedback part of a notification.
4878- Vibrate bool `json:"vibrate,omitempty"`
4879- // EmblemCounter represents and application counter hint
4880- // related to the notification.
4881- EmblemCounter *EmblemCounter `json:"emblem-counter,omitempty"`
4882- // Tag represents a tag to identify persistent notifications
4883- Tag string `json:"tag,omitempty"`
4884-}
4885-
4886-// Card is part of a notification and represents the user visible hints for
4887-// a specific notification.
4888-type Card struct {
4889- // Summary is a required title. The card will not be presented if this is missing.
4890- Summary string `json:"summary"`
4891- // Body is the longer text.
4892- Body string `json:"body,omitempty"`
4893- // Whether to show a bubble. Users can disable this, and can easily miss
4894- // them, so don’t rely on it exclusively.
4895- Popup bool `json:"popup,omitempty"`
4896- // Actions provides actions for the bubble's snap decissions.
4897- Actions []string `json:"actions,omitempty"`
4898- // Icon is a path to an icon to display with the notification bubble.
4899- Icon string `json:"icon,omitempty"`
4900- // Whether to show in notification centre.
4901- Persist bool `json:"persist,omitempty"`
4902- // Seconds since the unix epoch, useful for persistent cards.
4903- Timestamp int64 `json:"Timestamp,omitempty"`
4904-}
4905-
4906-// EmblemCounter is part of a notification and represents the application visual
4907-// hints related to a notification.
4908-type EmblemCounter struct {
4909- // Count is a number to be displayed over the application’s icon in the
4910- // launcher.
4911- Count uint `json:"count"`
4912- // Visible determines if the counter is visible or not.
4913- Visible bool `json:"visible"`
4914-}
4915-
4916-// The constanst defined here determine the polling aggressivenes with the following criteria
4917-// MAXIMUM: calls, health warning
4918-// HIGH: SMS, chat message, new email
4919-// DEFAULT: social media updates
4920-// LOW: software updates, junk email
4921-const (
4922- PRIORITY_MAXIMUM = 0
4923- PRIORITY_HIGH
4924- PRIORITY_DEFAULT
4925- PRIORITY_LOW
4926-)
4927-
4928-const (
4929- PLUGIN_EMAIL = 0
4930- PLUGIN_SOCIAL
4931-)
4932-
4933-// ErrTokenExpired is the error returned by a plugin to indicate that
4934-// the web service reported that the authentication token has expired.
4935-var ErrTokenExpired = errors.New("Token expired")
4936-
4937-var cmdName string
4938-
4939-var XdgDataFind = xdg.Data.Find
4940-var XdgDataEnsure = xdg.Data.Ensure
4941-
4942-// Persist stores the plugins data in a common location to a json file
4943-// from which it can recover later
4944-func Persist(pluginName string, accountId uint, data interface{}) (err error) {
4945- var p string
4946- defer func() {
4947- if err != nil && p != "" {
4948- os.Remove(p)
4949- }
4950- }()
4951- p, err = XdgDataEnsure(filepath.Join(cmdName, fmt.Sprintf("%s-%d.json", pluginName, accountId)))
4952- if err != nil {
4953- return err
4954- }
4955- file, err := os.Create(p)
4956- if err != nil {
4957- return err
4958- }
4959- defer file.Close()
4960- w := bufio.NewWriter(file)
4961- defer w.Flush()
4962- jsonWriter := json.NewEncoder(w)
4963- if err := jsonWriter.Encode(data); err != nil {
4964- return err
4965- }
4966- return nil
4967-}
4968-
4969-// FromPersist restores the plugins data from a common location which
4970-// was stored in a json file
4971-func FromPersist(pluginName string, accountId uint, data interface{}) (err error) {
4972- if reflect.ValueOf(data).Kind() != reflect.Ptr {
4973- return errors.New("decode target is not a pointer")
4974- }
4975- var p string
4976- defer func() {
4977- if err != nil {
4978- if p != "" {
4979- os.Remove(p)
4980- }
4981- }
4982- }()
4983- p, err = XdgDataFind(filepath.Join(cmdName, fmt.Sprintf("%s-%d.json", pluginName, accountId)))
4984- if err != nil {
4985- return err
4986- }
4987- file, err := os.Open(p)
4988- if err != nil {
4989- return err
4990- }
4991- defer file.Close()
4992- jsonReader := json.NewDecoder(file)
4993- if err := jsonReader.Decode(&data); err != nil {
4994- return err
4995- }
4996-
4997- return nil
4998-}
4999-
5000-// DefaultSound returns the path to the default sound for a Notification
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches