Merge lp:~mardy/account-polld/external-plugins into lp:~ubuntu-push-hackers/account-polld/trunk

Proposed by Alberto Mardegan
Status: Superseded
Proposed branch: lp:~mardy/account-polld/external-plugins
Merge into: lp:~ubuntu-push-hackers/account-polld/trunk
Diff against target: 8508 lines (+2883/-5145) (has conflicts)
78 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 (+283/-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 (+63/-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/account-polld.pot (+0/-88)
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 (+52/-0)
tests/account-polld/push_client.py (+39/-0)
tests/account-polld/signond.py (+93/-0)
tests/account-polld/test_plugin.py (+43/-0)
tests/account-polld/tst_account_polld.cpp (+475/-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)
Conflict: can't delete po because it is not empty.  Not deleting.
Conflict because po is not versioned, but has versioned children.  Versioned directory.
Contents conflict in po/aa.po
Contents conflict in po/am.po
Contents conflict in po/ast.po
Contents conflict in po/az.po
Contents conflict in po/br.po
Contents conflict in po/bs.po
Contents conflict in po/ca.po
Contents conflict in po/ca@valencia.po
Contents conflict in po/cs.po
Contents conflict in po/da.po
Contents conflict in po/de.po
Contents conflict in po/el.po
Contents conflict in po/en_AU.po
Contents conflict in po/en_GB.po
Contents conflict in po/es.po
Contents conflict in po/eu.po
Contents conflict in po/fa.po
Contents conflict in po/fi.po
Contents conflict in po/fr.po
Contents conflict in po/gd.po
Contents conflict in po/gl.po
Contents conflict in po/he.po
Contents conflict in po/hr.po
Contents conflict in po/hu.po
Contents conflict in po/is.po
Contents conflict in po/it.po
Contents conflict in po/ja.po
Contents conflict in po/km.po
Contents conflict in po/ko.po
Contents conflict in po/lt.po
Contents conflict in po/lv.po
Contents conflict in po/nb.po
Contents conflict in po/ne.po
Contents conflict in po/nl.po
Contents conflict in po/pl.po
Contents conflict in po/pt.po
Contents conflict in po/pt_BR.po
Contents conflict in po/ro.po
Contents conflict in po/ru.po
Contents conflict in po/sl.po
Contents conflict in po/sr.po
Contents conflict in po/sv.po
Contents conflict in po/ug.po
Contents conflict in po/uk.po
Contents conflict in po/vi.po
Contents conflict in po/zh_CN.po
Contents conflict in po/zh_TW.po
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+305223@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.
198. By Alberto Mardegan

Merge from trunk

199. By Alberto Mardegan

Update build dependencies

200. By Alberto Mardegan

Add python deps

201. By Alberto Mardegan

comma

202. By Alberto Mardegan

Enable C++11 for all components

203. By Alberto Mardegan

Hook: create dir if missing

204. By Alberto Mardegan

Rename output file

205. By Alberto Mardegan

fix creation of dir

206. By Alberto Mardegan

Fix hook

207. By Alberto Mardegan

Skip invalid applications

208. By Alberto Mardegan

Pass oauth client app data to plugin

209. By Alberto Mardegan

Make tests more robust

210. By Alberto Mardegan

Fix push object path

211. By Alberto Mardegan

Tests: increase plugin timeout

212. By Alberto Mardegan

more of the same

213. By Alberto Mardegan

Tests: wait till method returns

214. By Alberto Mardegan

Update VCS link

215. By Alberto Mardegan

Update dependencies and description

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

Subscribers

People subscribed via source and target branches