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

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

Commit message

Support out-of-process plugins

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

Description of the change

Support out-of-process plugins

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

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

Avoid cyclical deps

217. By Alberto Mardegan

Handle sigterm signal

This allows us to collect coverage results

218. By Alberto Mardegan

Fix reauthentication logic, add a test for that

Unmerged revisions

218. By Alberto Mardegan

Fix reauthentication logic, add a test for that

217. By Alberto Mardegan

Handle sigterm signal

This allows us to collect coverage results

216. By Alberto Mardegan

Avoid cyclical deps

215. By Alberto Mardegan

Update dependencies and description

214. By Alberto Mardegan

Update VCS link

213. By Alberto Mardegan

Tests: wait till method returns

212. By Alberto Mardegan

more of the same

211. By Alberto Mardegan

Tests: increase plugin timeout

210. By Alberto Mardegan

Fix push object path

209. By Alberto Mardegan

Make tests more robust

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file '.bzrignore'
--- .bzrignore 1970-01-01 00:00:00 +0000
+++ .bzrignore 2016-09-21 07:16:22 +0000
@@ -0,0 +1,12 @@
1*.moc
2Makefile*
3account-polld/account-polld
4click-hook/account-polld.hook
5debian/*.log
6debian/*.substvars
7debian/*-stamp
8debian/account-polld/
9debian/files
10moc_*
11tests/account-polld/tst_account_polld
12tests/click-hook/tst_click_hook
013
=== added file '.qmake.conf'
--- .qmake.conf 1970-01-01 00:00:00 +0000
+++ .qmake.conf 2016-09-21 07:16:22 +0000
@@ -0,0 +1,2 @@
1TOP_SRC_DIR = $$PWD
2TOP_BUILD_DIR = $$shadowed($$PWD)
03
=== added directory 'account-polld'
=== added file 'account-polld.pro'
--- account-polld.pro 1970-01-01 00:00:00 +0000
+++ account-polld.pro 2016-09-21 07:16:22 +0000
@@ -0,0 +1,11 @@
1include(common-vars.pri)
2include(common-project-config.pri)
3
4TEMPLATE = subdirs
5SUBDIRS = \
6 account-polld \
7 click-hook \
8 tests
9CONFIG += ordered
10
11include(common-installs-config.pri)
012
=== added file 'account-polld/account-polld.pro'
--- account-polld/account-polld.pro 1970-01-01 00:00:00 +0000
+++ account-polld/account-polld.pro 2016-09-21 07:16:22 +0000
@@ -0,0 +1,40 @@
1include(../common-project-config.pri)
2include($${TOP_SRC_DIR}/common-vars.pri)
3
4TEMPLATE = app
5TARGET = account-polld
6
7CONFIG += \
8 link_pkgconfig \
9 no_keywords \
10 qt
11
12QT += \
13 dbus
14
15PKGCONFIG += \
16 accounts-qt5 \
17 libsignon-qt5
18
19DEFINES += \
20 DEBUG_ENABLED \
21 PLUGIN_DATA_FILE=\\\"$${PLUGIN_DATA_FILE}\\\"
22
23SOURCES += \
24 account_manager.cpp \
25 app_manager.cpp \
26 debug.cpp \
27 main.cpp \
28 plugin.cpp \
29 poll_service.cpp \
30 push_client.cpp
31
32HEADERS += \
33 account_manager.h \
34 app_manager.h \
35 debug.h \
36 plugin.h \
37 poll_service.h \
38 push_client.h
39
40include($${TOP_SRC_DIR}/common-installs-config.pri)
041
=== added file 'account-polld/account_manager.cpp'
--- account-polld/account_manager.cpp 1970-01-01 00:00:00 +0000
+++ account-polld/account_manager.cpp 2016-09-21 07:16:22 +0000
@@ -0,0 +1,282 @@
1/*
2 * Copyright (C) 2016 Canonical Ltd.
3 *
4 * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
5 *
6 * This file is part of account-polld
7 *
8 * This program is free software: you can redistribute it and/or modify it
9 * under the terms of the GNU General Public License version 3, as published
10 * by the Free Software Foundation.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranties of
14 * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21#include "account_manager.h"
22
23#include "app_manager.h"
24#include "debug.h"
25
26#include <Accounts/Account>
27#include <Accounts/AccountService>
28#include <Accounts/Application>
29#include <Accounts/Manager>
30#include <Accounts/Service>
31#include <QMetaObject>
32#include <SignOn/AuthSession>
33#include <SignOn/Identity>
34#include <SignOn/SessionData>
35
36using namespace AccountPolld;
37
38namespace AccountPolld {
39
40class AccountManagerPrivate: public QObject
41{
42 Q_OBJECT
43 Q_DECLARE_PUBLIC(AccountManager)
44
45 struct AuthState {
46 QVariantMap lastAuthReply;
47 bool needNewToken;
48 };
49
50public:
51 AccountManagerPrivate(AccountManager *q, AppManager *appManager);
52 ~AccountManagerPrivate() {};
53
54 void loadApplications();
55 void activateAccount(Accounts::AccountService *as,
56 const QString &appKey);
57 void accountReady(Accounts::AccountService *as, const QString &appKey,
58 const QVariantMap &auth = QVariantMap());
59 static QString accountServiceKey(Accounts::AccountService *as);
60 static QString accountServiceKey(uint accountId, const QString &serviceId);
61
62 void markAuthFailure(const AccountData &data);
63 QVariantMap formatAuthReply(const Accounts::AuthData &authData,
64 const QVariantMap &reply) const;
65
66public Q_SLOTS:
67 void operationFinished();
68
69private:
70 Accounts::Manager m_manager;
71 AppManager *m_appManager;
72 Applications m_apps;
73 QHash<QString,Accounts::Application> m_accountApps;
74 QHash<QString,AuthState> m_authStates;
75 int m_pendingOperations;
76 AccountManager *q_ptr;
77};
78
79uint qHash(const AccountData &data) {
80 return ::qHash(data.pluginId) + ::qHash(data.accountId) +
81 ::qHash(data.serviceId);
82}
83
84} // namespace
85
86AccountManagerPrivate::AccountManagerPrivate(AccountManager *q,
87 AppManager *appManager):
88 QObject(q),
89 m_appManager(appManager),
90 m_pendingOperations(0),
91 q_ptr(q)
92{
93 qRegisterMetaType<AccountData>("AccountData");
94}
95
96QString AccountManagerPrivate::accountServiceKey(Accounts::AccountService *as)
97{
98 return accountServiceKey(as->account()->id(), as->service().name());
99}
100
101QString AccountManagerPrivate::accountServiceKey(uint accountId, const QString &serviceId)
102{
103 return QString("%1-%2").arg(accountId).arg(serviceId);
104}
105
106void AccountManagerPrivate::loadApplications()
107{
108 m_accountApps.clear();
109
110 m_apps = m_appManager->applications();
111 for (auto i = m_apps.constBegin(); i != m_apps.constEnd(); i++) {
112 Accounts::Application app = m_manager.application(i.value().appId);
113 if (app.isValid()) {
114 m_accountApps.insert(i.key(), app);
115 } else {
116 DEBUG() << "Application not found:" << i.value().appId;
117 }
118 }
119}
120
121QVariantMap AccountManagerPrivate::formatAuthReply(const Accounts::AuthData &authData,
122 const QVariantMap &reply) const
123{
124 QVariantMap formattedReply(reply);
125
126 QString mechanism = authData.mechanism();
127 const QVariantMap &parameters = authData.parameters();
128 if (mechanism == "HMAC-SHA1" || mechanism == "PLAINTEXT") {
129 /* For OAuth 1.0, let's return also the Consumer key and secret along
130 * with the reply. */
131 formattedReply["ClientId"] = parameters.value("ConsumerKey");
132 formattedReply["ClientSecret"] = parameters.value("ConsumerSecret");
133 } else if (mechanism == "web_server" || mechanism == "user_agent") {
134 formattedReply["ClientId"] = parameters.value("ClientId");
135 formattedReply["ClientSecret"] = parameters.value("ClientId");
136 }
137
138 return formattedReply;
139}
140
141void AccountManagerPrivate::accountReady(Accounts::AccountService *as,
142 const QString &appKey,
143 const QVariantMap &auth)
144{
145 Q_Q(AccountManager);
146 AccountData accountData;
147 accountData.pluginId = appKey;
148 accountData.accountId = as->account()->id();
149 accountData.serviceId = as->service().name();
150 accountData.auth = auth;
151 QMetaObject::invokeMethod(q, "accountReady", Qt::QueuedConnection,
152 Q_ARG(AccountData, accountData));
153}
154
155void AccountManagerPrivate::activateAccount(Accounts::AccountService *as,
156 const QString &appKey)
157{
158 const AppData &data = m_apps[appKey];
159 if (data.needsAuthData) {
160 Accounts::AuthData authData = as->authData();
161 QString key = accountServiceKey(as);
162
163 auto identity =
164 SignOn::Identity::existingIdentity(authData.credentialsId(), as);
165 auto authSession = identity->createSession(authData.method());
166 QObject::connect(authSession, &SignOn::AuthSession::response,
167 [this,as,appKey](const SignOn::SessionData &reply) {
168 as->deleteLater();
169
170 QVariantMap authReply = formatAuthReply(as->authData(), reply.toMap());
171 AuthState &authState = m_authStates[accountServiceKey(as)];
172 if (authState.needNewToken && authReply == authState.lastAuthReply) {
173 /* This account won't work, don't even check it */
174 operationFinished();
175 return;
176 }
177
178 authState.needNewToken = false;
179 accountReady(as, appKey, authReply);
180 operationFinished();
181 });
182 QObject::connect(authSession, &SignOn::AuthSession::error,
183 [this,as](const SignOn::Error &error) {
184 as->deleteLater();
185 operationFinished();
186 DEBUG() << "authentication error:" << error.message();
187 });
188
189 AuthState &authState = m_authStates[key];
190
191 QVariantMap sessionData = authData.parameters();
192 sessionData["UiPolicy"] = SignOn::NoUserInteractionPolicy;
193 if (authState.needNewToken) {
194 sessionData["ForceTokenRefresh"] = true;
195 }
196 m_pendingOperations++;
197 authSession->process(sessionData, authData.mechanism());
198 } else {
199 accountReady(as, appKey);
200 }
201}
202
203void AccountManagerPrivate::markAuthFailure(const AccountData &data)
204{
205 QString key = accountServiceKey(data.accountId, data.serviceId);
206 AuthState &authState = m_authStates[key];
207 authState.lastAuthReply = data.auth;
208 authState.needNewToken = true;
209}
210
211void AccountManagerPrivate::operationFinished()
212{
213 Q_Q(AccountManager);
214 m_pendingOperations--;
215 if (m_pendingOperations == 0) {
216 /* since the accountReady signal is sent in a queued connection, this
217 * signal must also be sent in that way, in order to be delivered after
218 * all the accountReady signals. */
219 QMetaObject::invokeMethod(q, "finished", Qt::QueuedConnection);
220 }
221}
222
223AccountManager::AccountManager(AppManager *appManager, QObject *parent):
224 QObject(parent),
225 d_ptr(new AccountManagerPrivate(this, appManager))
226{
227}
228
229AccountManager::~AccountManager()
230{
231 delete d_ptr;
232}
233
234void AccountManager::listAccounts()
235{
236 Q_D(AccountManager);
237
238 d->loadApplications();
239
240 d->m_pendingOperations++;
241
242 Accounts::AccountIdList accountIds = d->m_manager.accountListEnabled();
243 for (Accounts::AccountId accountId: accountIds) {
244 Accounts::Account *account = d->m_manager.account(accountId);
245 if (Q_UNLIKELY(!account)) continue;
246
247 Accounts::ServiceList services = account->enabledServices();
248
249 /* check if we have some plugins registered for this service */
250 for (auto i = d->m_accountApps.constBegin();
251 i != d->m_accountApps.constEnd(); i++) {
252 for (Accounts::Service &service: services) {
253 /* Check if the application can use this service */
254 if (i.value().serviceUsage(service).isEmpty()) {
255 continue;
256 }
257
258 /* Check if the plugin manifest allows using this service */
259 const AppData &appData = d->m_apps[i.key()];
260 if (!appData.services.isEmpty() &&
261 !appData.services.contains(service.name())) {
262 DEBUG() << "Skipping service" << service.name() <<
263 "for plugin" << i.key();
264 continue;
265 }
266
267 auto *as = new Accounts::AccountService(account, service);
268 d->activateAccount(as, i.key());
269 }
270 }
271 }
272
273 d->operationFinished();
274}
275
276void AccountManager::markAuthFailure(const AccountData &data)
277{
278 Q_D(AccountManager);
279 d->markAuthFailure(data);
280}
281
282#include "account_manager.moc"
0283
=== added file 'account-polld/account_manager.h'
--- account-polld/account_manager.h 1970-01-01 00:00:00 +0000
+++ account-polld/account_manager.h 2016-09-21 07:16:22 +0000
@@ -0,0 +1,78 @@
1/*
2 * Copyright (C) 2016 Canonical Ltd.
3 *
4 * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
5 *
6 * This file is part of account-polld
7 *
8 * This program is free software: you can redistribute it and/or modify it
9 * under the terms of the GNU General Public License version 3, as published
10 * by the Free Software Foundation.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranties of
14 * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21#ifndef AP_ACCOUNT_MANAGER_H
22#define AP_ACCOUNT_MANAGER_H
23
24#include <QObject>
25#include <QVariantMap>
26
27namespace AccountPolld {
28
29struct AccountData {
30 QString pluginId;
31 uint accountId;
32 QString serviceId;
33 QVariantMap auth;
34
35 /* This is needed for using the struct as a QHash key; the "auth" map is
36 * intentionally omitted from the comparison as we don't want to use that
37 * as a key, too. */
38 bool operator==(const AccountData &other) const {
39 return pluginId == other.pluginId && accountId == other.accountId &&
40 serviceId == other.serviceId;
41 }
42};
43
44uint qHash(const AccountData &data);
45
46class AppManager;
47
48class AccountManagerPrivate;
49class AccountManager: public QObject
50{
51 Q_OBJECT
52
53public:
54 explicit AccountManager(AppManager *appManager, QObject *parent = 0);
55 ~AccountManager();
56
57 /* Scan for accounts; for each valid account, the accountReady() signal
58 * will be emitted. A finished() signal will be emitted last. */
59 void listAccounts();
60
61 /* Call when the authentication data for an account is refused by the
62 * server because of token expiration */
63 void markAuthFailure(const AccountData &data);
64
65Q_SIGNALS:
66 void accountReady(const AccountData &data);
67 void finished();
68
69private:
70 AccountManagerPrivate *d_ptr;
71 Q_DECLARE_PRIVATE(AccountManager)
72};
73
74} // namespace
75
76Q_DECLARE_METATYPE(AccountPolld::AccountData)
77
78#endif // AP_ACCOUNT_MANAGER_H
079
=== added file 'account-polld/app_manager.cpp'
--- account-polld/app_manager.cpp 1970-01-01 00:00:00 +0000
+++ account-polld/app_manager.cpp 2016-09-21 07:16:22 +0000
@@ -0,0 +1,118 @@
1/*
2 * Copyright (C) 2016 Canonical Ltd.
3 *
4 * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
5 *
6 * This file is part of account-polld
7 *
8 * This program is free software: you can redistribute it and/or modify it
9 * under the terms of the GNU General Public License version 3, as published
10 * by the Free Software Foundation.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranties of
14 * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21#include "debug.h"
22#include "app_manager.h"
23
24#include <QFile>
25#include <QJsonArray>
26#include <QJsonDocument>
27#include <QJsonObject>
28#include <QStandardPaths>
29
30using namespace AccountPolld;
31
32namespace AccountPolld {
33
34class AppManagerPrivate: public QObject
35{
36 Q_OBJECT
37 Q_DECLARE_PUBLIC(AppManager)
38
39public:
40 AppManagerPrivate(AppManager *q);
41 ~AppManagerPrivate() {};
42
43 Applications readPluginData() const;
44
45private:
46 QString m_dataFilePath;
47 AppManager *q_ptr;
48};
49
50} // namespace
51
52AppManagerPrivate::AppManagerPrivate(AppManager *q):
53 QObject(q),
54 q_ptr(q)
55{
56 const QString localShare =
57 QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation);
58 m_dataFilePath = localShare + "/" PLUGIN_DATA_FILE;
59}
60
61Applications AppManagerPrivate::readPluginData() const
62{
63 Applications apps;
64
65 QFile file(m_dataFilePath);
66 if (!file.open(QIODevice::ReadOnly)) return apps;
67
68 QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
69 file.close();
70
71 QJsonObject mainObject = doc.object();
72 for (auto i = mainObject.begin(); i != mainObject.end(); i++) {
73 QJsonObject appObject = i.value().toObject();
74
75 AppData data;
76 data.profile = appObject.value("profile").toString();
77 data.execLine = appObject.value("exec").toString();
78 data.appId = appObject.value("appId").toString();
79 QJsonArray services = appObject.value("services").toArray();
80 for (const QJsonValue &v: services) {
81 data.services.append(v.toString());
82 }
83 data.interval = appObject.value("interval").toInt();
84 data.needsAuthData = appObject.value("needsAuthData").toBool();
85
86 if (data.profile.isEmpty() ||
87 data.execLine.isEmpty() ||
88 data.appId.isEmpty()) {
89 qWarning() << "Incomplete plugin data:" <<
90 QJsonDocument(appObject).toJson(QJsonDocument::Compact);
91 continue;
92 }
93
94 apps.insert(i.key(), data);
95 }
96
97 return apps;
98}
99
100AppManager::AppManager(QObject *parent):
101 QObject(parent),
102 d_ptr(new AppManagerPrivate(this))
103{
104}
105
106AppManager::~AppManager()
107{
108 delete d_ptr;
109}
110
111Applications AppManager::applications() const
112{
113 Q_D(const AppManager);
114
115 return d->readPluginData();
116}
117
118#include "app_manager.moc"
0119
=== added file 'account-polld/app_manager.h'
--- account-polld/app_manager.h 1970-01-01 00:00:00 +0000
+++ account-polld/app_manager.h 2016-09-21 07:16:22 +0000
@@ -0,0 +1,60 @@
1/*
2 * Copyright (C) 2016 Canonical Ltd.
3 *
4 * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
5 *
6 * This file is part of account-polld
7 *
8 * This program is free software: you can redistribute it and/or modify it
9 * under the terms of the GNU General Public License version 3, as published
10 * by the Free Software Foundation.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranties of
14 * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21#ifndef AP_APP_MANAGER_H
22#define AP_APP_MANAGER_H
23
24#include <QObject>
25#include <QStringList>
26#include <QHash>
27
28namespace AccountPolld {
29
30struct AppData {
31 QString profile; // apparmor label for the plugin process
32 QString execLine;
33 QString appId; // appId, for matching with OA
34 QStringList services;
35 int interval;
36 bool needsAuthData;
37};
38
39typedef QHash<QString,AppData> Applications;
40
41class AppManagerPrivate;
42
43class AppManager: public QObject
44{
45 Q_OBJECT
46
47public:
48 explicit AppManager(QObject *parent = 0);
49 ~AppManager();
50
51 Applications applications() const;
52
53private:
54 AppManagerPrivate *d_ptr;
55 Q_DECLARE_PRIVATE(AppManager)
56};
57
58} // namespace
59
60#endif // AP_APP_MANAGER_H
061
=== added file 'account-polld/debug.cpp'
--- account-polld/debug.cpp 1970-01-01 00:00:00 +0000
+++ account-polld/debug.cpp 2016-09-21 07:16:22 +0000
@@ -0,0 +1,29 @@
1/*
2 * Copyright (C) 2016 Canonical Ltd.
3 *
4 * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
5 *
6 * This file is part of account-polld
7 *
8 * This program is free software: you can redistribute it and/or modify it
9 * under the terms of the GNU General Public License version 3, as published
10 * by the Free Software Foundation.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranties of
14 * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21#include "debug.h"
22
23int appLoggingLevel = 1; // criticals
24
25void setLoggingLevel(int level)
26{
27 appLoggingLevel = level;
28}
29
030
=== added file 'account-polld/debug.h'
--- account-polld/debug.h 1970-01-01 00:00:00 +0000
+++ account-polld/debug.h 2016-09-21 07:16:22 +0000
@@ -0,0 +1,47 @@
1/*
2 * Copyright (C) 2016 Canonical Ltd.
3 *
4 * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
5 *
6 * This file is part of account-polld
7 *
8 * This program is free software: you can redistribute it and/or modify it
9 * under the terms of the GNU General Public License version 3, as published
10 * by the Free Software Foundation.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranties of
14 * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20#ifndef AP_DEBUG_H
21#define AP_DEBUG_H
22
23#include <QDebug>
24
25/* 0 - fatal, 1 - critical(default), 2 - info/debug */
26extern int appLoggingLevel;
27
28static inline bool debugEnabled()
29{
30 return appLoggingLevel >= 2;
31}
32
33static inline int loggingLevel()
34{
35 return appLoggingLevel;
36}
37
38void setLoggingLevel(int level);
39
40#ifdef DEBUG_ENABLED
41 #define DEBUG() \
42 if (debugEnabled()) qDebug() << __FILE__ << __LINE__ << __func__
43#else
44 #define DEBUG() while (0) qDebug()
45#endif
46
47#endif // AP_DEBUG_H
048
=== added file 'account-polld/main.cpp'
--- account-polld/main.cpp 1970-01-01 00:00:00 +0000
+++ account-polld/main.cpp 2016-09-21 07:16:22 +0000
@@ -0,0 +1,71 @@
1/*
2 * Copyright (C) 2016 Canonical Ltd.
3 *
4 * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
5 *
6 * This file is part of account-polld
7 *
8 * This program is free software: you can redistribute it and/or modify it
9 * under the terms of the GNU General Public License version 3, as published
10 * by the Free Software Foundation.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranties of
14 * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21#include <QCoreApplication>
22#include <QDBusConnection>
23#include <QProcessEnvironment>
24#include <QSettings>
25#include <signal.h>
26
27#include "debug.h"
28#include "poll_service.h"
29
30
31static void signalHandler(int)
32{
33 QCoreApplication::quit();
34}
35
36int main(int argc, char **argv)
37{
38 QCoreApplication app(argc, argv);
39
40 signal(SIGTERM, signalHandler);
41
42 QSettings settings("account-polld");
43
44 /* read environment variables */
45 QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
46 if (environment.contains(QLatin1String("AP_LOGGING_LEVEL"))) {
47 bool isOk;
48 int value = environment.value(
49 QLatin1String("AP_LOGGING_LEVEL")).toInt(&isOk);
50 if (isOk)
51 setLoggingLevel(value);
52 } else {
53 setLoggingLevel(settings.value("LoggingLevel", 1).toInt());
54 }
55
56 QDBusConnection connection = QDBusConnection::sessionBus();
57
58 auto service = new AccountPolld::PollService();
59 connection.registerObject(ACCOUNT_POLLD_OBJECT_PATH, service,
60 QDBusConnection::ExportAllContents);
61 connection.registerService(ACCOUNT_POLLD_SERVICE_NAME);
62
63 int ret = app.exec();
64
65 connection.unregisterService(ACCOUNT_POLLD_SERVICE_NAME);
66 connection.unregisterObject(ACCOUNT_POLLD_OBJECT_PATH);
67 delete service;
68
69 return ret;
70}
71
072
=== added file 'account-polld/plugin.cpp'
--- account-polld/plugin.cpp 1970-01-01 00:00:00 +0000
+++ account-polld/plugin.cpp 2016-09-21 07:16:22 +0000
@@ -0,0 +1,155 @@
1/*
2 * Copyright (C) 2016 Canonical Ltd.
3 *
4 * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
5 *
6 * This file is part of account-polld
7 *
8 * This program is free software: you can redistribute it and/or modify it
9 * under the terms of the GNU General Public License version 3, as published
10 * by the Free Software Foundation.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranties of
14 * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21#include "plugin.h"
22
23#include "debug.h"
24
25#include <QByteArray>
26#include <QJsonDocument>
27#include <QJsonObject>
28#include <QJsonParseError>
29#include <QProcess>
30#include <QTimer>
31#include <signal.h>
32#include <sys/types.h>
33
34using namespace AccountPolld;
35
36namespace AccountPolld {
37
38class PluginPrivate: public QProcess
39{
40 Q_OBJECT
41 Q_DECLARE_PUBLIC(Plugin)
42
43public:
44 PluginPrivate(Plugin *q, const QString &execLine, const QString &profile);
45 ~PluginPrivate() {};
46
47public Q_SLOTS:
48 void onReadyRead();
49 void killPlugin();
50
51private:
52 QString m_execLine;
53 QString m_profile;
54 QTimer m_timer;
55 QByteArray m_inputBuffer;
56 bool m_sigtermSent;
57 Plugin *q_ptr;
58};
59
60} // namespace
61
62PluginPrivate::PluginPrivate(Plugin *q,
63 const QString &execLine,
64 const QString &profile):
65 QProcess(q),
66 m_execLine(execLine),
67 m_profile(profile),
68 m_sigtermSent(false),
69 q_ptr(q)
70{
71 int killTime = 10;
72 QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
73 if (environment.contains("AP_PLUGIN_TIMEOUT")) {
74 killTime = environment.value("AP_PLUGIN_TIMEOUT").toInt();
75 }
76 m_timer.setInterval(killTime * 1000);
77 m_timer.setSingleShot(true);
78
79 setProcessChannelMode(QProcess::ForwardedErrorChannel);
80 QObject::connect(this, SIGNAL(started()), &m_timer, SLOT(start()));
81 QObject::connect(this, SIGNAL(started()), q, SIGNAL(ready()));
82 QObject::connect(this, SIGNAL(finished(int,QProcess::ExitStatus)),
83 q, SIGNAL(finished()));
84 QObject::connect(this, SIGNAL(readyReadStandardOutput()),
85 this, SLOT(onReadyRead()));
86 QObject::connect(&m_timer, SIGNAL(timeout()), this, SLOT(killPlugin()));
87}
88
89void PluginPrivate::onReadyRead()
90{
91 Q_Q(Plugin);
92
93 m_inputBuffer.append(readAllStandardOutput());
94 QJsonParseError error;
95 auto doc = QJsonDocument::fromJson(m_inputBuffer, &error);
96 if (error.error == QJsonParseError::NoError) {
97 m_inputBuffer.clear();
98 Q_EMIT q->response(doc.object());
99 }
100
101 /* otherwise continue reasing, the object is probably uncomplete */
102}
103
104void PluginPrivate::killPlugin()
105{
106 pid_t pid = processId();
107 DEBUG() << "killing plugin" << pid;
108 if (!m_sigtermSent) {
109 ::kill(pid, SIGTERM);
110 m_sigtermSent = true;
111 m_timer.setInterval(1 * 1000);
112 m_timer.start();
113 } else {
114 ::kill(pid, SIGKILL);
115 }
116}
117
118Plugin::Plugin(const QString &execLine, const QString &profile,
119 QObject *parent):
120 QObject(parent),
121 d_ptr(new PluginPrivate(this, execLine, profile))
122{
123}
124
125Plugin::~Plugin()
126{
127 delete d_ptr;
128}
129
130void Plugin::run()
131{
132 Q_D(Plugin);
133
134 QString command;
135
136 if (d->m_profile != "unconfined") {
137 command = QString("aa-exec-click -p %1 -- ").arg(d->m_profile);
138 }
139
140 command.append(d->m_execLine);
141
142 DEBUG() << "Starting" << command;
143 d->start(command);
144}
145
146void Plugin::poll(const QJsonObject &pollData)
147{
148 Q_D(Plugin);
149
150 DEBUG() << "Plugin input:" << pollData;
151 d->write(QJsonDocument(pollData).toJson(QJsonDocument::Compact));
152 d->write("\n");
153}
154
155#include "plugin.moc"
0156
=== added file 'account-polld/plugin.h'
--- account-polld/plugin.h 1970-01-01 00:00:00 +0000
+++ account-polld/plugin.h 2016-09-21 07:16:22 +0000
@@ -0,0 +1,55 @@
1/*
2 * Copyright (C) 2016 Canonical Ltd.
3 *
4 * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
5 *
6 * This file is part of account-polld
7 *
8 * This program is free software: you can redistribute it and/or modify it
9 * under the terms of the GNU General Public License version 3, as published
10 * by the Free Software Foundation.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranties of
14 * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21#ifndef AP_PLUGIN_H
22#define AP_PLUGIN_H
23
24#include <QObject>
25
26class QJsonObject;
27
28namespace AccountPolld {
29
30class PluginPrivate;
31class Plugin: public QObject
32{
33 Q_OBJECT
34
35public:
36 explicit Plugin(const QString &execLine, const QString &profile,
37 QObject *parent = 0);
38 ~Plugin();
39
40 void run();
41 void poll(const QJsonObject &pollData);
42
43Q_SIGNALS:
44 void ready();
45 void response(const QJsonObject &resp);
46 void finished();
47
48private:
49 PluginPrivate *d_ptr;
50 Q_DECLARE_PRIVATE(Plugin)
51};
52
53} // namespace
54
55#endif // AP_PLUGIN_H
056
=== added file 'account-polld/poll_service.cpp'
--- account-polld/poll_service.cpp 1970-01-01 00:00:00 +0000
+++ account-polld/poll_service.cpp 2016-09-21 07:16:22 +0000
@@ -0,0 +1,190 @@
1/*
2 * Copyright (C) 2016 Canonical Ltd.
3 *
4 * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
5 *
6 * This file is part of account-polld
7 *
8 * This program is free software: you can redistribute it and/or modify it
9 * under the terms of the GNU General Public License version 3, as published
10 * by the Free Software Foundation.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranties of
14 * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21#include "debug.h"
22#include "account_manager.h"
23#include "app_manager.h"
24#include "poll_service.h"
25#include "plugin.h"
26#include "push_client.h"
27
28#include <QDateTime>
29#include <QDBusArgument>
30#include <QDBusConnection>
31#include <QJsonArray>
32#include <QJsonObject>
33#include <QVariantMap>
34
35using namespace AccountPolld;
36
37namespace AccountPolld {
38
39class PollServicePrivate: public QObject
40{
41 Q_OBJECT
42 Q_DECLARE_PUBLIC(PollService)
43
44 struct PollData {
45 QDateTime lastPolled;
46 };
47
48public:
49 PollServicePrivate(PollService *q);
50 ~PollServicePrivate() {};
51
52 QJsonObject preparePluginInput(const AccountData &accountData,
53 const AppData &appData);
54 void handleResponse(const QJsonObject &response, const QString &appId,
55 const AccountData &accountData);
56
57private Q_SLOTS:
58 void poll();
59 void onAccountReady(const AccountData &data);
60 void operationFinished();
61
62private:
63 AppManager m_appManager;
64 AccountManager m_accountManager;
65 PushClient m_pushClient;
66 QHash<AccountData,PollData> m_polls;
67 int m_pendingOperations;
68 PollService *q_ptr;
69};
70
71} // namespace
72
73PollServicePrivate::PollServicePrivate(PollService *q):
74 QObject(q),
75 m_accountManager(&m_appManager),
76 m_pendingOperations(0),
77 q_ptr(q)
78{
79 QObject::connect(&m_accountManager,
80 SIGNAL(accountReady(const AccountData&)),
81 this,
82 SLOT(onAccountReady(const AccountData&)));
83 QObject::connect(&m_accountManager, SIGNAL(finished()),
84 this, SLOT(operationFinished()));
85}
86
87void PollServicePrivate::operationFinished()
88{
89 Q_Q(PollService);
90 m_pendingOperations--;
91 if (m_pendingOperations == 0) {
92 Q_EMIT q->Done();
93 }
94}
95
96QJsonObject
97PollServicePrivate::preparePluginInput(const AccountData &accountData,
98 const AppData &appData)
99{
100 QJsonObject object;
101 object["helperId"] = accountData.pluginId;
102 object["appId"] = appData.appId;
103 object["accountId"] = int(accountData.accountId);
104 if (appData.needsAuthData) {
105 object["auth"] = QJsonObject::fromVariantMap(accountData.auth);
106 }
107 return object;
108}
109
110void PollServicePrivate::handleResponse(const QJsonObject &response,
111 const QString &appId,
112 const AccountData &accountData)
113{
114 DEBUG() << "Plugin response:" << response;
115 QJsonObject error = response["error"].toObject();
116 if (error["code"].toString() == "ERR_INVALID_AUTH") {
117 m_accountManager.markAuthFailure(accountData);
118 return;
119 }
120
121 QJsonArray notifications = response["notifications"].toArray();
122 for (const QJsonValue &v: notifications) {
123 m_pushClient.post(appId, v.toObject());
124 }
125}
126
127void PollServicePrivate::poll()
128{
129 m_pendingOperations++;
130 m_accountManager.listAccounts();
131}
132
133void PollServicePrivate::onAccountReady(const AccountData &accountData)
134{
135 Applications apps = m_appManager.applications();
136 const auto i = apps.find(accountData.pluginId);
137 if (i == apps.end()) {
138 qWarning() << "Got account for plugin, but no app linked:" << accountData.pluginId;
139 return;
140 }
141
142 const AppData &appData = i.value();
143
144 /* Check that we are not polling more often than what the application
145 * wishes to */
146 PollData &pollData = m_polls[accountData];
147 QDateTime now = QDateTime::currentDateTime();
148 if (pollData.lastPolled.isValid() &&
149 pollData.lastPolled.secsTo(now) < appData.interval) {
150 DEBUG() << "Skipping poll, interval not yet expired:" << accountData.pluginId;
151 return;
152 }
153 pollData.lastPolled = now;
154
155 QJsonObject pluginInput = preparePluginInput(accountData, appData);
156
157 Plugin *plugin = new Plugin(appData.execLine, appData.profile, this);
158 QObject::connect(plugin, SIGNAL(finished()), plugin, SLOT(deleteLater()));
159 QObject::connect(plugin, SIGNAL(finished()), this, SLOT(operationFinished()));
160 QObject::connect(plugin, &Plugin::ready,
161 [plugin, pluginInput]() { plugin->poll(pluginInput); });
162 QObject::connect(plugin, &Plugin::response,
163 [this, accountData, appData](const QJsonObject &resp) {
164 handleResponse(resp, appData.appId, accountData);
165 });
166
167 m_pendingOperations++;
168 plugin->run();
169}
170
171PollService::PollService(QObject *parent):
172 QObject(parent),
173 d_ptr(new PollServicePrivate(this))
174{
175}
176
177PollService::~PollService()
178{
179 delete d_ptr;
180}
181
182void PollService::Poll()
183{
184 Q_D(PollService);
185
186 DEBUG() << "Got Poll";
187 QMetaObject::invokeMethod(d, "poll", Qt::QueuedConnection);
188}
189
190#include "poll_service.moc"
0191
=== added file 'account-polld/poll_service.h'
--- account-polld/poll_service.h 1970-01-01 00:00:00 +0000
+++ account-polld/poll_service.h 2016-09-21 07:16:22 +0000
@@ -0,0 +1,65 @@
1/*
2 * Copyright (C) 2016 Canonical Ltd.
3 *
4 * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
5 *
6 * This file is part of account-polld
7 *
8 * This program is free software: you can redistribute it and/or modify it
9 * under the terms of the GNU General Public License version 3, as published
10 * by the Free Software Foundation.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranties of
14 * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21#ifndef AP_POLL_SERVICE_H
22#define AP_POLL_SERVICE_H
23
24#include <QDBusContext>
25#include <QDBusMessage>
26#include <QObject>
27
28namespace AccountPolld {
29
30#define ACCOUNT_POLLD_OBJECT_PATH \
31 QStringLiteral("/com/ubuntu/AccountPolld")
32#define ACCOUNT_POLLD_SERVICE_NAME \
33 QStringLiteral("com.ubuntu.AccountPolld")
34
35class PollServicePrivate;
36
37class PollService: public QObject, protected QDBusContext
38{
39 Q_OBJECT
40 Q_CLASSINFO("D-Bus Interface", "com.ubuntu.AccountPolld")
41 Q_CLASSINFO("D-Bus Introspection", ""
42" <interface name=\"com.ubuntu.AccountPolld\">\n"
43" <method name=\"Poll\" />\n"
44" <signal name=\"Done\" />\n"
45" </interface>\n"
46 "")
47
48public:
49 explicit PollService(QObject *parent = 0);
50 ~PollService();
51
52public Q_SLOTS:
53 void Poll();
54
55Q_SIGNALS:
56 void Done();
57
58private:
59 PollServicePrivate *d_ptr;
60 Q_DECLARE_PRIVATE(PollService)
61};
62
63} // namespace
64
65#endif // AP_POLL_SERVICE_H
066
=== added file 'account-polld/push_client.cpp'
--- account-polld/push_client.cpp 1970-01-01 00:00:00 +0000
+++ account-polld/push_client.cpp 2016-09-21 07:16:22 +0000
@@ -0,0 +1,112 @@
1/*
2 * Copyright (C) 2016 Canonical Ltd.
3 *
4 * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
5 *
6 * This file is part of account-polld
7 *
8 * This program is free software: you can redistribute it and/or modify it
9 * under the terms of the GNU General Public License version 3, as published
10 * by the Free Software Foundation.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranties of
14 * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21#include "push_client.h"
22
23#include "debug.h"
24
25#include <QByteArray>
26#include <QDBusConnection>
27#include <QDBusMessage>
28#include <QJsonDocument>
29#include <QJsonObject>
30
31using namespace AccountPolld;
32
33namespace AccountPolld {
34
35class PushClientPrivate: public QObject
36{
37 Q_OBJECT
38 Q_DECLARE_PUBLIC(PushClient)
39
40public:
41 PushClientPrivate(PushClient *q);
42 ~PushClientPrivate() {};
43
44 static QByteArray makeObjectPath(const QString &appId);
45
46private:
47 QDBusConnection m_conn;
48 PushClient *q_ptr;
49};
50
51} // namespace
52
53PushClientPrivate::PushClientPrivate(PushClient *q):
54 QObject(q),
55 m_conn(QDBusConnection::sessionBus()),
56 q_ptr(q)
57{
58}
59
60QByteArray PushClientPrivate::makeObjectPath(const QString &appId)
61{
62 QByteArray path(QByteArrayLiteral("/com/ubuntu/Postal/"));
63
64 QByteArray pkg = appId.split('_').first().toUtf8();
65 for (int i = 0; i < pkg.count(); i++) {
66 char buffer[10];
67 char c = pkg[i];
68 switch (c) {
69 case '+':
70 case '.':
71 case '-':
72 case ':':
73 case '~':
74 case '_':
75 sprintf(buffer, "_%.2x", c);
76 path += buffer;
77 break;
78 default:
79 path += c;
80 }
81 }
82 return path;
83}
84
85PushClient::PushClient(QObject *parent):
86 QObject(parent),
87 d_ptr(new PushClientPrivate(this))
88{
89}
90
91PushClient::~PushClient()
92{
93 delete d_ptr;
94}
95
96void PushClient::post(const QString &appId, const QJsonObject &message)
97{
98 Q_D(PushClient);
99
100 QByteArray objectPath = d->makeObjectPath(appId);
101 QDBusMessage msg = QDBusMessage::createMethodCall("com.ubuntu.Postal",
102 objectPath,
103 "com.ubuntu.Postal",
104 "Post");
105 msg << appId;
106 QByteArray data = QJsonDocument(message).toJson(QJsonDocument::Compact);
107 msg << QString::fromUtf8(data);
108
109 d->m_conn.send(msg);
110}
111
112#include "push_client.moc"
0113
=== added file 'account-polld/push_client.h'
--- account-polld/push_client.h 1970-01-01 00:00:00 +0000
+++ account-polld/push_client.h 2016-09-21 07:16:22 +0000
@@ -0,0 +1,48 @@
1/*
2 * Copyright (C) 2016 Canonical Ltd.
3 *
4 * Contact: Alberto Mardegan <alberto.mardegan@canonical.com>
5 *
6 * This file is part of account-polld
7 *
8 * This program is free software: you can redistribute it and/or modify it
9 * under the terms of the GNU General Public License version 3, as published
10 * by the Free Software Foundation.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranties of
14 * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21#ifndef AP_PUSH_CLIENT_H
22#define AP_PUSH_CLIENT_H
23
24#include <QObject>
25
26class QJsonObject;
27
28namespace AccountPolld {
29
30class PushClientPrivate;
31class PushClient: public QObject
32{
33 Q_OBJECT
34
35public:
36 explicit PushClient(QObject *parent = 0);
37 ~PushClient();
38
39 void post(const QString &appId, const QJsonObject &message);
40
41private:
42 PushClientPrivate *d_ptr;
43 Q_DECLARE_PRIVATE(PushClient)
44};
45
46} // namespace
47
48#endif // AP_PUSH_CLIENT_H
049
=== removed directory 'accounts'
=== removed file 'accounts/account-watcher.c'
--- accounts/account-watcher.c 2016-08-02 14:34:52 +0000
+++ accounts/account-watcher.c 1970-01-01 00:00:00 +0000
@@ -1,302 +0,0 @@
1/*
2 Copyright 2014 Canonical Ltd.
3
4 This program is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License version 3, as published
6 by the Free Software Foundation.
7
8 This program is distributed in the hope that it will be useful, but
9 WITHOUT ANY WARRANTY; without even the implied warranties of
10 MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11 PURPOSE. See the GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License along
14 with this program. If not, see <http://www.gnu.org/licenses/>.
15*/
16#include <stdio.h>
17
18#include <glib.h>
19#include <libaccounts-glib/accounts-glib.h>
20#include <libsignon-glib/signon-glib.h>
21
22#include "account-watcher.h"
23
24/* #define DEBUG */
25#ifdef DEBUG
26# define trace(...) fprintf(stderr, __VA_ARGS__)
27#else
28# define trace(...)
29#endif
30
31struct _AccountWatcher {
32 AgManager *manager;
33 /* A hash table of the enabled accounts we know of.
34 * Keys are "<accountId>/<serviceName>", and AccountInfo structs as values.
35 */
36 GHashTable *services;
37
38 /* List of supported services' IDs */
39 GSList *supported_services;
40
41 AccountEnabledCallback callback;
42 void *user_data;
43};
44
45typedef struct _AccountInfo AccountInfo;
46struct _AccountInfo {
47 AccountWatcher *watcher;
48 /* Manage signin session for account */
49 AgAccountService *account_service;
50 SignonAuthSession *session;
51 GVariant *auth_params;
52 GVariant *session_data;
53
54 AgAccountId account_id;
55};
56
57static void account_info_clear_login(AccountInfo *info) {
58 if (info->session_data) {
59 g_variant_unref(info->session_data);
60 info->session_data = NULL;
61 }
62 if (info->auth_params) {
63 g_variant_unref(info->auth_params);
64 info->auth_params = NULL;
65 }
66 if (info->session) {
67 signon_auth_session_cancel(info->session);
68 g_object_unref(info->session);
69 info->session = NULL;
70 }
71}
72
73static void account_info_free(AccountInfo *info) {
74 account_info_clear_login(info);
75 if (info->account_service) {
76 g_object_unref(info->account_service);
77 info->account_service = NULL;
78 }
79 g_free(info);
80}
81
82static void account_info_notify(AccountInfo *info, GError *error) {
83 AgService *service = ag_account_service_get_service(info->account_service);
84 const char *service_name = ag_service_get_name(service);
85 const char *service_type = ag_service_get_service_type(service);
86 char *client_id = NULL;
87 char *client_secret = NULL;
88 char *access_token = NULL;
89 char *token_secret = NULL;
90 char *secret = NULL;
91 char *user_name = NULL;
92
93 if (info->auth_params != NULL) {
94 /* Look up OAuth 2 parameters, falling back to OAuth 1 names */
95 g_variant_lookup(info->auth_params, "ClientId", "&s", &client_id);
96 g_variant_lookup(info->auth_params, "ClientSecret", "&s", &client_secret);
97 if (client_id == NULL) {
98 g_variant_lookup(info->auth_params, "ConsumerKey", "&s", &client_id);
99 }
100 if (client_secret == NULL) {
101 g_variant_lookup(info->auth_params, "ConsumerSecret", "&s", &client_secret);
102 }
103 }
104 if (info->session_data != NULL) {
105 g_variant_lookup(info->session_data, "AccessToken", "&s", &access_token);
106 g_variant_lookup(info->session_data, "TokenSecret", "&s", &token_secret);
107 g_variant_lookup(info->session_data, "Secret", "&s", &secret);
108 g_variant_lookup(info->session_data, "UserName", "&s", &user_name);
109 }
110
111 info->watcher->callback(info->watcher,
112 info->account_id,
113 service_type,
114 service_name,
115 error,
116 TRUE,
117 client_id,
118 client_secret,
119 access_token,
120 token_secret,
121 user_name,
122 secret,
123 info->watcher->user_data);
124}
125
126static void account_info_login_cb(GObject *source, GAsyncResult *result, void *user_data) {
127 SignonAuthSession *session = (SignonAuthSession *)source;
128 AccountInfo *info = (AccountInfo *)user_data;
129
130 trace("Authentication for account %u complete\n", info->account_id);
131
132 GError *error = NULL;
133 info->session_data = signon_auth_session_process_finish(session, result, &error);
134 account_info_notify(info, error);
135
136 if (error != NULL) {
137 trace("Authentication failed: %s\n", error->message);
138 g_error_free(error);
139 }
140}
141
142static void account_info_login(AccountInfo *info) {
143 account_info_clear_login(info);
144
145 AgAuthData *auth_data = ag_account_service_get_auth_data(info->account_service);
146 GError *error = NULL;
147 trace("Starting authentication session for account %u\n", info->account_id);
148 info->session = signon_auth_session_new(
149 ag_auth_data_get_credentials_id(auth_data),
150 ag_auth_data_get_method(auth_data), &error);
151 if (error != NULL) {
152 trace("Could not set up auth session: %s\n", error->message);
153 account_info_notify(info, error);
154 g_error_free(error);
155 g_object_unref(auth_data);
156 return;
157 }
158
159 /* Tell libsignon-glib not to open a trust session as we have no UI */
160 GVariantBuilder builder;
161 g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
162 g_variant_builder_add(&builder, "{sv}",
163 SIGNON_SESSION_DATA_UI_POLICY,
164 g_variant_new_int32(SIGNON_POLICY_NO_USER_INTERACTION));
165
166 info->auth_params = g_variant_ref_sink(
167 ag_auth_data_get_login_parameters(
168 auth_data,
169 g_variant_builder_end(&builder)));
170
171 signon_auth_session_process_async(
172 info->session,
173 info->auth_params,
174 ag_auth_data_get_mechanism(auth_data),
175 NULL, /* cancellable */
176 account_info_login_cb, info);
177 ag_auth_data_unref(auth_data);
178}
179
180static AccountInfo *account_info_new(AccountWatcher *watcher, AgAccountService *account_service) {
181 AccountInfo *info = g_new0(AccountInfo, 1);
182 info->watcher = watcher;
183 info->account_service = g_object_ref(account_service);
184
185 AgAccount *account = ag_account_service_get_account(account_service);
186 g_object_get(account, "id", &info->account_id, NULL);
187
188 return info;
189}
190
191static gboolean service_is_supported(AccountWatcher *watcher,
192 const char *service_id)
193{
194 GSList *node = g_slist_find_custom(watcher->supported_services,
195 service_id,
196 (GCompareFunc)g_strcmp0);
197 return node != NULL;
198}
199
200static gboolean account_watcher_setup(void *user_data) {
201 AccountWatcher *watcher = (AccountWatcher *)user_data;
202
203 /* Now check initial state */
204 GList *enabled_accounts =
205 ag_manager_get_enabled_account_services(watcher->manager);
206 GList *old_services = g_hash_table_get_keys(watcher->services);
207
208 /* Update the services table */
209 GList *l;
210 for (l = enabled_accounts; l != NULL; l = l->next) {
211 AgAccountService *account_service = l->data;
212 AgAccountId id = ag_account_service_get_account(account_service)->id;
213 AgService *service = ag_account_service_get_service(account_service);
214 const char *service_id = ag_service_get_name(service);
215
216 if (!service_is_supported(watcher, service_id)) continue;
217
218 char *key = g_strdup_printf("%d/%s", id, service_id);
219
220 AccountInfo *info = g_hash_table_lookup(watcher->services, key);
221 if (info) {
222 GList *node = g_list_find_custom(old_services, key,
223 (GCompareFunc)g_strcmp0);
224 old_services = g_list_remove_link(old_services, node);
225 g_free(key);
226 } else {
227 trace("adding account %s\n", key);
228 info = account_info_new(watcher, account_service);
229 g_hash_table_insert(watcher->services, key, info);
230 }
231 account_info_login(info);
232 }
233 g_list_free_full(enabled_accounts, g_object_unref);
234
235 /* Remove from the table the accounts which are no longer enabled */
236 for (l = old_services; l != NULL; l = l->next) {
237 char *key = l->data;
238 trace("removing account %s\n", key);
239 g_hash_table_remove(watcher->services, key);
240 }
241 g_list_free(old_services);
242
243 return G_SOURCE_REMOVE;
244}
245
246AccountWatcher *account_watcher_new(AccountEnabledCallback callback,
247 void *user_data) {
248 AccountWatcher *watcher = g_new0(AccountWatcher, 1);
249
250 watcher->manager = ag_manager_new();
251 watcher->services = g_hash_table_new_full(
252 g_str_hash, g_str_equal, g_free, (GDestroyNotify)account_info_free);
253 watcher->supported_services = NULL;
254 watcher->callback = callback;
255 watcher->user_data = user_data;
256
257 return watcher;
258}
259
260void account_watcher_add_service(AccountWatcher *watcher,
261 char *serviceId) {
262 watcher->supported_services =
263 g_slist_prepend(watcher->supported_services, serviceId);
264}
265
266void account_watcher_run(AccountWatcher *watcher) {
267 /* Make sure main setup occurs within the mainloop thread */
268 g_idle_add(account_watcher_setup, watcher);
269}
270
271struct refresh_info {
272 AccountWatcher *watcher;
273 AgAccountId account_id;
274 char *service_name;
275};
276
277static void refresh_info_free(struct refresh_info *data) {
278 g_free(data->service_name);
279 g_free(data);
280}
281
282static gboolean account_watcher_refresh_cb(void *user_data) {
283 struct refresh_info *data = (struct refresh_info *)user_data;
284
285 char *key = g_strdup_printf("%d/%s", data->account_id, data->service_name);
286 AccountInfo *info = g_hash_table_lookup(data->watcher->services, key);
287 if (info != NULL) {
288 account_info_login(info);
289 }
290
291 return G_SOURCE_REMOVE;
292}
293
294void account_watcher_refresh(AccountWatcher *watcher, unsigned int account_id,
295 const char *service_name) {
296 struct refresh_info *data = g_new(struct refresh_info, 1);
297 data->watcher = watcher;
298 data->account_id = account_id;
299 data->service_name = g_strdup(service_name);
300 g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, account_watcher_refresh_cb,
301 data, (GDestroyNotify)refresh_info_free);
302}
3030
=== removed file 'accounts/account-watcher.h'
--- accounts/account-watcher.h 2016-08-02 14:34:52 +0000
+++ accounts/account-watcher.h 1970-01-01 00:00:00 +0000
@@ -1,47 +0,0 @@
1/*
2 Copyright 2014 Canonical Ltd.
3
4 This program is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License version 3, as published
6 by the Free Software Foundation.
7
8 This program is distributed in the hope that it will be useful, but
9 WITHOUT ANY WARRANTY; without even the implied warranties of
10 MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11 PURPOSE. See the GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License along
14 with this program. If not, see <http://www.gnu.org/licenses/>.
15*/
16#ifndef ACCOUNT_WATCHER_H
17#define ACCOUNT_WATCHER_H
18
19#include <glib.h>
20
21typedef struct _AccountWatcher AccountWatcher;
22
23typedef void (*AccountEnabledCallback)(AccountWatcher *watcher,
24 unsigned int account_id,
25 const char *service_type,
26 const char *service_name,
27 GError *error,
28 int enabled,
29 const char *client_id,
30 const char *client_secret,
31 const char *access_token,
32 const char *token_secret,
33 const char *user_name,
34 const char *secret,
35 void *user_data);
36
37AccountWatcher *account_watcher_new(AccountEnabledCallback callback,
38 void *user_data);
39void account_watcher_add_service(AccountWatcher *watcher,
40 char *serviceId);
41void account_watcher_run(AccountWatcher *watcher);
42
43void account_watcher_refresh(AccountWatcher *watcher,
44 unsigned int account_id,
45 const char *service_name);
46
47#endif
480
=== removed file 'accounts/accounts.c'
--- accounts/accounts.c 2016-08-02 14:34:52 +0000
+++ accounts/accounts.c 1970-01-01 00:00:00 +0000
@@ -1,26 +0,0 @@
1#include "_cgo_export.h"
2
3AccountWatcher *watch() {
4 /* Transfer service names to hash table */
5 if (FALSE) {
6 /* The Go callback doesn't quite match the
7 * AccountEnabledCallback function prototype, so we cast the
8 * argument in the account_watcher_new() call below.
9 *
10 * This is just a check to see that the function still has the
11 * prototype we expect.
12 */
13 void (*unused)(void *watcher,
14 unsigned int account_id,
15 char *service_type, char *service_name,
16 GError *error, int enabled,
17 char *client_id, char *client_secret,
18 char *access_token, char *token_secret,
19 char *user_name, char *secret,
20 void *user_data) = authCallback;
21 }
22
23 AccountWatcher *watcher = account_watcher_new(
24 (AccountEnabledCallback)authCallback, NULL);
25 return watcher;
26}
270
=== removed file 'accounts/accounts.go'
--- accounts/accounts.go 2016-08-02 14:34:52 +0000
+++ accounts/accounts.go 1970-01-01 00:00:00 +0000
@@ -1,130 +0,0 @@
1/*
2 Copyright 2014 Canonical Ltd.
3
4 This program is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License version 3, as published
6 by the Free Software Foundation.
7
8 This program is distributed in the hope that it will be useful, but
9 WITHOUT ANY WARRANTY; without even the implied warranties of
10 MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11 PURPOSE. See the GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License along
14 with this program. If not, see <http://www.gnu.org/licenses/>.
15*/
16
17package accounts
18
19/*
20#cgo pkg-config: glib-2.0 libaccounts-glib libsignon-glib
21#include <stdlib.h>
22#include <glib.h>
23#include "account-watcher.h"
24
25AccountWatcher *watch();
26*/
27import "C"
28import (
29 "errors"
30 "sync"
31 "unsafe"
32)
33
34type Watcher struct {
35 C <-chan AuthData
36 watcher *C.AccountWatcher
37}
38
39type AuthData struct {
40 AccountId uint
41 ServiceName string
42 ServiceType string
43 Error error
44 Enabled bool
45
46 ClientId string
47 ClientSecret string
48 AccessToken string
49 TokenSecret string
50 Secret string
51 UserName string
52}
53
54var (
55 authChannels = make(map[*C.AccountWatcher]chan<- AuthData)
56 authChannelsLock sync.Mutex
57)
58
59// NewWatcher creates a new account watcher
60func NewWatcher() *Watcher {
61 w := new(Watcher)
62 w.watcher = C.watch()
63
64 ch := make(chan AuthData)
65 w.C = ch
66 authChannelsLock.Lock()
67 authChannels[w.watcher] = ch
68 authChannelsLock.Unlock()
69
70 return w
71}
72
73func (w *Watcher) AddService(serviceId string) {
74 C.account_watcher_add_service(w.watcher, C.CString(serviceId))
75}
76
77// Walk through the enabled accounts, and get auth tokens for each of them.
78// The new access token will be delivered over the watcher's channel.
79func (w *Watcher) Run() {
80 C.account_watcher_run(w.watcher)
81}
82
83// Refresh requests that the token for the given account be refreshed.
84// The new access token will be delivered over the watcher's channel.
85func (w *Watcher) Refresh(accountId uint, serviceName string) {
86 C.account_watcher_refresh(w.watcher, C.uint(accountId), C.CString(serviceName))
87}
88
89//export authCallback
90func 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) {
91 // Ideally the first argument would be of type
92 // *C.AccountWatcher, but that fails with Go 1.2.
93 authChannelsLock.Lock()
94 ch := authChannels[(*C.AccountWatcher)(watcher)]
95 authChannelsLock.Unlock()
96 if ch == nil {
97 // Log the error
98 return
99 }
100
101 var data AuthData
102 data.AccountId = uint(accountId)
103 data.ServiceName = C.GoString(serviceName)
104 data.ServiceType = C.GoString(serviceType)
105 if error != nil {
106 data.Error = errors.New(C.GoString((*C.char)(error.message)))
107 }
108 if enabled != 0 {
109 data.Enabled = true
110 }
111 if clientId != nil {
112 data.ClientId = C.GoString(clientId)
113 }
114 if clientSecret != nil {
115 data.ClientSecret = C.GoString(clientSecret)
116 }
117 if accessToken != nil {
118 data.AccessToken = C.GoString(accessToken)
119 }
120 if tokenSecret != nil {
121 data.TokenSecret = C.GoString(tokenSecret)
122 }
123 if secret != nil {
124 data.Secret = C.GoString(secret)
125 }
126 if userName != nil {
127 data.UserName = C.GoString(userName)
128 }
129 ch <- data
130}
1310
=== added directory 'click-hook'
=== added file 'click-hook/account-polld.hook.in'
--- click-hook/account-polld.hook.in 1970-01-01 00:00:00 +0000
+++ click-hook/account-polld.hook.in 2016-09-21 07:16:22 +0000
@@ -0,0 +1,4 @@
1Pattern: ${home}/.local/share/account-polld/plugins/${id}.json
2User-Level: yes
3Hook-Name: account-polld
4Exec: $${hook_helper.path}/$${hook_helper.files}
05
=== added file 'click-hook/click-hook'
--- click-hook/click-hook 1970-01-01 00:00:00 +0000
+++ click-hook/click-hook 2016-09-21 07:16:22 +0000
@@ -0,0 +1,108 @@
1#!/usr/bin/python3
2# -*- python -*-
3"""Collect helpers hook data into a single json file"""
4
5import json
6import os
7import sys
8import time
9
10import xdg.BaseDirectory
11
12hook_ext = '.json'
13
14
15class HookProcessor:
16 def __init__(self):
17 self.xdg_data_home = xdg.BaseDirectory.xdg_data_home
18 self.xdg_data_dirs = xdg.BaseDirectory.xdg_data_dirs
19 self.plugins_data_path = os.path.join(self.xdg_data_home, 'account-polld',
20 'plugin_data.json')
21 self.plugins_data_path_tmp = os.path.join(self.xdg_data_home, 'account-polld',
22 '.plugin_data_%s.tmp')
23 os.makedirs(os.path.join(self.xdg_data_home, 'account-polld'), exist_ok=True)
24
25
26 def write_plugin_data(self):
27 plugin_data = {}
28 for path in self.xdg_data_dirs:
29 data = self.collect_plugins(path)
30 plugin_data.update(data)
31
32 # write the collected data to a temp file and rename the original once
33 # everything is on disk
34 try:
35 tmp_filename = self.plugins_data_path_tmp % (time.time(),)
36 with open(tmp_filename, 'w') as dest:
37 json.dump(plugin_data, dest)
38 dest.flush()
39 os.rename(tmp_filename, self.plugins_data_path)
40 except Exception as e:
41 print('Writing file %s failed: %s' % (self.plugins_data_path, e), file=sys.stderr)
42 return False
43 return True
44
45
46 def collect_plugins(self, base_path):
47 trusted = False if base_path == self.xdg_data_home else True
48 hooks_path = os.path.join(base_path, 'account-polld', 'plugins')
49 plugins_data = {}
50 if not os.path.isdir(hooks_path):
51 return plugins_data
52 for hook_fname in os.listdir(hooks_path):
53 if not hook_fname.endswith(hook_ext):
54 continue
55 try:
56 with open(os.path.join(hooks_path, hook_fname), 'r') as fd:
57 data = json.load(fd)
58 except Exception:
59 print('Unable to parse JSON from %s' % (hook_fname,), file=sys.stderr)
60 continue
61
62 helper_id = os.path.splitext(hook_fname)[0]
63 profile = 'unconfined' if trusted else helper_id
64 if helper_id.count('_') == 2:
65 helper_short_id = '_'.join(helper_id.split('_')[0:2])
66 else:
67 helper_short_id = helper_id
68
69 exec_path = data['exec']
70 if exec_path != "":
71 realpath = os.path.realpath(os.path.join(hooks_path,
72 hook_fname))
73 exec_path = os.path.join(os.path.dirname(realpath), exec_path)
74 app_id = data.get('app_id', None)
75 if app_id is None:
76 # no app_id, use the package name from the helper_id
77 app_id = helper_short_id
78 elif app_id.count('_') >= 2:
79 # remove the version from the app_id
80 app_id = '_'.join(app_id.split('_')[0:2])
81 if not trusted:
82 # check that the plugin comes from the same package as the app
83 plugin_package = helper_id.split('_')[0]
84 app_package = app_id.split('_')[0]
85 if plugin_package != app_package:
86 print('Skipping %s as it\'s unrelated to package %s' % (hook_fname, app_package), file=sys.stderr)
87 continue
88
89 parsed = {
90 'exec': exec_path,
91 'appId': app_id,
92 'profile': profile,
93 }
94 parsed['needsAuthData'] = data.get('needs_authentication_data', False)
95 if 'service_ids' in data:
96 parsed['services'] = data['service_ids']
97 if 'interval' in data:
98 parsed['interval'] = data['interval']
99 plugins_data[helper_short_id] = parsed
100
101 return plugins_data
102
103
104if __name__ == "__main__":
105 processor = HookProcessor()
106 ok = processor.write_plugin_data()
107
108 sys.exit(0 if ok else 1)
0109
=== added file 'click-hook/click-hook.pro'
--- click-hook/click-hook.pro 1970-01-01 00:00:00 +0000
+++ click-hook/click-hook.pro 2016-09-21 07:16:22 +0000
@@ -0,0 +1,20 @@
1include(../common-project-config.pri)
2
3TEMPLATE = aux
4TARGET = ""
5
6QMAKE_SUBSTITUTES += \
7 account-polld.hook.in
8
9OTHER_FILES += \
10 click-hook
11
12hook_helper.files = \
13 click-hook
14hook_helper.path = $${INSTALL_PREFIX}/lib/account-polld
15INSTALLS += hook_helper
16
17hooks.files = \
18 account-polld.hook
19hooks.path = $${INSTALL_PREFIX}/share/click/hooks
20INSTALLS += hooks
021
=== removed directory 'cmd'
=== removed directory 'cmd/account-polld'
=== removed file 'cmd/account-polld/account_service.go'
--- cmd/account-polld/account_service.go 2016-08-02 14:34:52 +0000
+++ cmd/account-polld/account_service.go 1970-01-01 00:00:00 +0000
@@ -1,186 +0,0 @@
1/*
2 Copyright 2014 Canonical Ltd.
3
4 This program is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License version 3, as published
6 by the Free Software Foundation.
7
8 This program is distributed in the hope that it will be useful, but
9 WITHOUT ANY WARRANTY; without even the implied warranties of
10 MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11 PURPOSE. See the GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License along
14 with this program. If not, see <http://www.gnu.org/licenses/>.
15*/
16
17package main
18
19import (
20 "errors"
21 "log"
22 "time"
23
24 "launchpad.net/account-polld/accounts"
25 "launchpad.net/account-polld/plugins"
26 "launchpad.net/ubuntu-push/click"
27)
28
29type AccountService struct {
30 watcher *accounts.Watcher
31 authData accounts.AuthData
32 plugin plugins.Plugin
33 interval time.Duration
34 postWatch chan *PostWatch
35 authChan chan accounts.AuthData
36 doneChan chan error
37 penaltyCount int
38 authFailureCount int
39}
40
41var (
42 pollTimeout = time.Duration(30 * time.Second)
43 bootstrapPollTimeout = time.Duration(4 * time.Minute)
44 maxCounter = 4
45 authTriesUntilPenalty = 3
46 authFailurePenalty = 10
47)
48
49var (
50 authError = errors.New("Skipped account")
51 clickNotInstalledError = errors.New("Click not installed")
52)
53
54func NewAccountService(watcher *accounts.Watcher, postWatch chan *PostWatch, plugin plugins.Plugin) *AccountService {
55 return &AccountService{
56 watcher: watcher,
57 plugin: plugin,
58 postWatch: postWatch,
59 authChan: make(chan accounts.AuthData, 1),
60 doneChan: make(chan error, 1),
61 }
62}
63
64func (a *AccountService) Delete() {
65 close(a.authChan)
66 close(a.doneChan)
67}
68
69// Poll() always needs to be called asynchronously as otherwise qtcontacs' GetAvatar()
70// will raise an error: "QSocketNotifier: Can only be used with threads started with QThread"
71func (a *AccountService) Poll(bootstrap bool) {
72 gotNewAuthData := false
73 if a.authData, gotNewAuthData = <-a.authChan; !gotNewAuthData {
74 log.Println("Account", a.authData.AccountId, "no longer enabled")
75 return
76 }
77
78 if a.penaltyCount > 0 {
79 log.Printf("Leaving poll for account %d as penalty count is %d", a.authData.AccountId, a.penaltyCount)
80 a.penaltyCount--
81 return
82 } else if !gotNewAuthData && a.authData.Error != nil {
83 // 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
84 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)")
85 a.authData.Error = nil
86 }
87
88 timeout := pollTimeout
89 if bootstrap {
90 timeout = bootstrapPollTimeout
91 }
92
93 log.Printf("Starting poll for account %d", a.authData.AccountId)
94 go a.poll()
95
96 select {
97 case <-time.After(timeout):
98 log.Println("Poll for account", a.authData.AccountId, "has timed out out after", timeout)
99 a.penaltyCount++
100 case err := <-a.doneChan:
101 if err == nil {
102 log.Println("Poll for account", a.authData.AccountId, "was successful")
103 a.authFailureCount = 0
104 a.penaltyCount = 0
105 } else {
106 if err != clickNotInstalledError && err != authError { // Do not log the error twice
107 log.Println("Poll for account", a.authData.AccountId, "has failed:", err)
108 }
109 if err == authError || err == plugins.ErrTokenExpired {
110 // Increase the authFailureCount counter, except for when we did a poll which
111 // raised a token expiry error when we did not get any new auth data this time.
112 if err != plugins.ErrTokenExpired || gotNewAuthData {
113 log.Println("Increasing the auth failure counter for account", a.authData.AccountId)
114 a.authFailureCount++
115 } else {
116 log.Println("Not increasing the auth failure counter for account", a.authData.AccountId, "as we do not have new auth data")
117 }
118 if a.authFailureCount >= authTriesUntilPenalty {
119 a.penaltyCount = authFailurePenalty
120 a.authFailureCount = 0
121 log.Println(authTriesUntilPenalty, "auth failures in a row for account", a.authData.AccountId, "-> skipping it for the next", a.penaltyCount, "poll cycles")
122 } else if err == plugins.ErrTokenExpired && !gotNewAuthData {
123 // If the error indicates that the authentication token has expired, request reauthentication
124 // and mark the data as disabled.
125 // Do not refresh immediately when we just got new (faulty) auth data as immediately trying
126 // again is probably not going to help. Instead, we wait for the next poll cycle.
127 a.watcher.Refresh(a.authData.AccountId, a.authData.ServiceName)
128 a.authData.Enabled = false
129 a.authData.Error = err
130 }
131 } else if a.penaltyCount < maxCounter {
132 a.authFailureCount = 0
133 a.penaltyCount++
134 }
135 }
136 }
137 log.Printf("Ending poll for account %d", a.authData.AccountId)
138}
139
140func (a *AccountService) poll() {
141 log.Println("Polling account", a.authData.AccountId)
142 if !isClickInstalled(a.plugin.ApplicationId()) {
143 log.Println(
144 "Skipping account", a.authData.AccountId, "as target click",
145 a.plugin.ApplicationId(), "is not installed")
146 a.doneChan <- clickNotInstalledError
147 return
148 }
149
150 if a.authData.Error != nil {
151 log.Println("Account", a.authData.AccountId, "failed to authenticate:", a.authData.Error)
152 a.doneChan <- authError
153 return
154 }
155
156 if bs, err := a.plugin.Poll(&a.authData); err != nil {
157 log.Print("Error while polling ", a.authData.AccountId, ": ", err)
158 a.doneChan <- err
159 } else {
160 for _, b := range bs {
161 log.Println("Account", a.authData.AccountId, "has", len(b.Messages), b.Tag, "updates to report")
162 }
163 a.postWatch <- &PostWatch{batches: bs, appId: a.plugin.ApplicationId()}
164 a.doneChan <- nil
165 }
166}
167
168func (a *AccountService) updateAuthData(authData accounts.AuthData) {
169 a.authChan <- authData
170}
171
172func isClickInstalled(appId plugins.ApplicationId) bool {
173 user, err := click.User()
174 if err != nil {
175 log.Println("User instance for click cannot be created to determine if click application", appId, "was installed")
176 return false
177 }
178
179 app, err := click.ParseAppId(string(appId))
180 if err != nil {
181 log.Println("Could not parse APP_ID for", appId)
182 return false
183 }
184
185 return user.Installed(app, false)
186}
1870
=== removed file 'cmd/account-polld/main.go'
--- cmd/account-polld/main.go 2016-08-02 14:34:52 +0000
+++ cmd/account-polld/main.go 1970-01-01 00:00:00 +0000
@@ -1,260 +0,0 @@
1/*
2 Copyright 2014 Canonical Ltd.
3
4 This program is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License version 3, as published
6 by the Free Software Foundation.
7
8 This program is distributed in the hope that it will be useful, but
9 WITHOUT ANY WARRANTY; without even the implied warranties of
10 MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11 PURPOSE. See the GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License along
14 with this program. If not, see <http://www.gnu.org/licenses/>.
15*/
16
17package main
18
19import (
20 "encoding/json"
21 "fmt"
22 "strings"
23 "sync"
24
25 "log"
26
27 "launchpad.net/account-polld/accounts"
28 "launchpad.net/account-polld/gettext"
29 "launchpad.net/account-polld/plugins"
30 "launchpad.net/account-polld/plugins/caldav"
31 "launchpad.net/account-polld/plugins/dekko"
32 "launchpad.net/account-polld/plugins/gcalendar"
33 "launchpad.net/account-polld/plugins/gmail"
34 "launchpad.net/account-polld/plugins/twitter"
35 "launchpad.net/account-polld/pollbus"
36 "launchpad.net/account-polld/qtcontact"
37 "launchpad.net/go-dbus/v1"
38)
39
40type PostWatch struct {
41 appId plugins.ApplicationId
42 batches []*plugins.PushMessageBatch
43}
44
45type AccountKey struct {
46 serviceId string
47 accountId uint
48}
49
50/* Use identifiers and API keys provided by the respective webapps which are the official
51 end points for the notifications */
52const (
53 SERVICENAME_DEKKO = "dekko.dekkoproject_dekko"
54 SERVICENAME_GMAIL = "com.ubuntu.developer.webapps.webapp-gmail_webapp-gmail"
55 SERVICENAME_TWITTER = "com.ubuntu.developer.webapps.webapp-twitter_webapp-twitter"
56 SERVICENAME_GCALENDAR = "google-caldav"
57 SERVICENAME_OCALENDAR = "owncloud-caldav"
58)
59
60const (
61 POSTAL_SERVICE = "com.ubuntu.Postal"
62 POSTAL_INTERFACE = "com.ubuntu.Postal"
63 POSTAL_OBJECT_PATH_PART = "/com/ubuntu/Postal/"
64)
65
66var mainLoopOnce sync.Once
67
68func init() {
69 startMainLoop()
70}
71
72func startMainLoop() {
73 mainLoopOnce.Do(func() {
74 go qtcontact.MainLoopStart()
75 })
76}
77
78func main() {
79 // TODO NewAccount called here is just for playing purposes.
80 postWatch := make(chan *PostWatch)
81
82 // Initialize i18n
83 gettext.SetLocale(gettext.LC_ALL, "")
84 gettext.Textdomain("account-polld")
85 gettext.BindTextdomain("account-polld", "/usr/share/locale")
86
87 bus, err := dbus.Connect(dbus.SessionBus)
88 if err != nil {
89 log.Fatal("Cannot connect to bus", err)
90 }
91
92 pollBus := pollbus.New(bus)
93 go postOffice(bus, postWatch)
94 go monitorAccounts(postWatch, pollBus)
95
96 if err := pollBus.Init(); err != nil {
97 log.Fatal("Issue while setting up the poll bus:", err)
98 }
99
100 done := make(chan bool)
101 <-done
102}
103
104func monitorAccounts(postWatch chan *PostWatch, pollBus *pollbus.PollBus) {
105 watcher := accounts.NewWatcher()
106 watcher.AddService(SERVICENAME_DEKKO)
107 watcher.AddService(SERVICENAME_GMAIL)
108 watcher.AddService(SERVICENAME_GCALENDAR)
109 watcher.AddService(SERVICENAME_TWITTER)
110
111 mgr := make(map[AccountKey]*AccountService)
112
113 var wg sync.WaitGroup
114
115 pullAccount := func(data accounts.AuthData) bool {
116 accountKey := AccountKey{data.ServiceName, data.AccountId}
117 if account, ok := mgr[accountKey]; ok {
118 if data.Enabled {
119 log.Println("New account data for existing account with id", data.AccountId)
120 account.penaltyCount = 0
121 account.updateAuthData(data)
122 wg.Add(1)
123 go func() {
124 defer wg.Done()
125 // Poll() needs to be called asynchronously as otherwise qtcontacs' GetAvatar() will
126 // raise an error: "QSocketNotifier: Can only be used with threads started with QThread"
127 account.Poll(false)
128 }()
129 // No wg.Wait() here as it would break GetAvatar() again.
130 // Instead we have a wg.Wait() before the PollChan polling below.
131 } else {
132 account.Delete()
133 delete(mgr, accountKey)
134 }
135 } else if data.Enabled {
136 var plugin plugins.Plugin
137 log.Println("Creating plugin for service: ", data.ServiceName)
138 switch data.ServiceName {
139 case SERVICENAME_DEKKO:
140 log.Println("Creating account with id", data.AccountId, "for", data.ServiceName)
141 plugin = dekko.New(data.AccountId)
142 case SERVICENAME_GMAIL:
143 log.Println("Creating account with id", data.AccountId, "for", data.ServiceName)
144 plugin = gmail.New(data.AccountId)
145 case SERVICENAME_GCALENDAR:
146 log.Println("Creating account with id", data.AccountId, "for", data.ServiceName)
147 plugin = gcalendar.New(data.AccountId)
148 case SERVICENAME_TWITTER:
149 // This is just stubbed until the plugin exists.
150 log.Println("Creating account with id", data.AccountId, "for", data.ServiceName)
151 plugin = twitter.New()
152 case SERVICENAME_OCALENDAR:
153 log.Println("Creating account with id", data.AccountId, "for", data.ServiceName)
154 plugin = caldav.New(data.AccountId)
155 default:
156 log.Println("Unhandled account with id", data.AccountId, "for", data.ServiceName)
157 return false
158 }
159 mgr[accountKey] = NewAccountService(watcher, postWatch, plugin)
160 mgr[accountKey].updateAuthData(data)
161 wg.Add(1)
162 go func() {
163 defer wg.Done()
164 // Poll() needs to be called asynchronously as otherwise qtcontacs' GetAvatar() will
165 // raise an error: "QSocketNotifier: Can only be used with threads started with QThread"
166 mgr[accountKey].Poll(true)
167 }()
168 // No wg.Wait() here as it would break GetAvatar() again.
169 // Instead we have a wg.Wait() before the PollChan polling below.
170 }
171 return true
172 }
173
174L:
175 for {
176 select {
177 case data := <-watcher.C:
178 if pullAccount(data) == false {
179 log.Println("pullAccount returned false, continuing")
180 continue L
181 }
182 case <-pollBus.PollChan:
183 wg.Wait() // Finish all running Poll() calls before potentially polling the same accounts again
184 watcher.Run()
185 wg.Wait()
186 pollBus.SignalDone()
187 }
188 }
189}
190
191func postOffice(bus *dbus.Connection, postWatch chan *PostWatch) {
192 for post := range postWatch {
193 obj := bus.Object(POSTAL_SERVICE, pushObjectPath(post.appId))
194
195 for _, batch := range post.batches {
196
197 notifs := batch.Messages
198 overflowing := len(notifs) > batch.Limit
199
200 for i, n := range notifs {
201 // Play sound and vibrate on first notif only.
202 if i > 0 {
203 n.Notification.Vibrate = false
204 n.Notification.Sound = ""
205 }
206
207 // We're overflowing, so no popups.
208 // See LP: #1527171
209 if overflowing {
210 n.Notification.Card.Popup = false
211 }
212 }
213
214 if overflowing {
215 n := batch.OverflowHandler(notifs)
216 n.Notification.Card.Persist = false
217 n.Notification.Vibrate = false
218 notifs = append(notifs, n)
219 }
220
221 for _, n := range notifs {
222 var pushMessage string
223 if out, err := json.Marshal(n); err == nil {
224 pushMessage = string(out)
225 } else {
226 log.Printf("Cannot marshall %#v to json: %s", n, err)
227 continue
228 }
229 if _, err := obj.Call(POSTAL_INTERFACE, "Post", post.appId, pushMessage); err != nil {
230 log.Println("Cannot call the Post Office:", err)
231 log.Println("Message missed posting:", pushMessage)
232 }
233 }
234 }
235 }
236}
237
238// pushObjectPath returns the object path of the ApplicationId
239// for Push Notifications with the Quoted Package Name in the form of
240// /com/ubuntu/PushNotifications/QUOTED_PKGNAME
241//
242// e.g.; if the APP_ID is com.ubuntu.music", the returned object path
243// would be "/com/ubuntu/PushNotifications/com_2eubuntu_2eubuntu_2emusic
244func pushObjectPath(id plugins.ApplicationId) dbus.ObjectPath {
245 idParts := strings.Split(string(id), "_")
246 if len(idParts) < 2 {
247 panic(fmt.Sprintf("APP_ID '%s' is not valid", id))
248 }
249
250 pkg := POSTAL_OBJECT_PATH_PART
251 for _, c := range idParts[0] {
252 switch c {
253 case '+', '.', '-', ':', '~', '_':
254 pkg += fmt.Sprintf("_%x", string(c))
255 default:
256 pkg += string(c)
257 }
258 }
259 return dbus.ObjectPath(pkg)
260}
2610
=== removed directory 'cmd/account-watcher-test'
=== removed file 'cmd/account-watcher-test/main.go'
--- cmd/account-watcher-test/main.go 2016-06-17 13:51:55 +0000
+++ cmd/account-watcher-test/main.go 1970-01-01 00:00:00 +0000
@@ -1,17 +0,0 @@
1package main
2
3import (
4 "fmt"
5
6 "launchpad.net/account-polld/accounts"
7)
8
9func main() {
10 for data := range accounts.NewWatcher().C {
11 if data.Error != nil {
12 fmt.Println("Failed to authenticate account", data.AccountId, ":", data.Error)
13 } else {
14 fmt.Printf("%#v\n", data)
15 }
16 }
17}
180
=== removed directory 'cmd/qtcontact-test'
=== removed file 'cmd/qtcontact-test/main.go'
--- cmd/qtcontact-test/main.go 2015-03-20 14:34:48 +0000
+++ cmd/qtcontact-test/main.go 1970-01-01 00:00:00 +0000
@@ -1,36 +0,0 @@
1/*
2 Copyright 2014 Canonical Ltd.
3
4 This program is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License version 3, as published
6 by the Free Software Foundation.
7
8 This program is distributed in the hope that it will be useful, but
9 WITHOUT ANY WARRANTY; without even the implied warranties of
10 MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11 PURPOSE. See the GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License along
14 with this program. If not, see <http://www.gnu.org/licenses/>.
15*/
16
17package main
18
19import (
20 "fmt"
21 "os"
22
23 "launchpad.net/account-polld/qtcontact"
24)
25
26func main() {
27 qtcontact.MainLoopStart()
28
29 if len(os.Args) != 2 {
30 fmt.Println("usage:", os.Args[0], "[email address]")
31 os.Exit(1)
32 }
33
34 path := qtcontact.GetAvatar(os.Args[1])
35 fmt.Println("Avatar found:", path)
36}
370
=== added file 'common-installs-config.pri'
--- common-installs-config.pri 1970-01-01 00:00:00 +0000
+++ common-installs-config.pri 2016-09-21 07:16:22 +0000
@@ -0,0 +1,43 @@
1#-----------------------------------------------------------------------------
2# Common installation configuration for all projects.
3#-----------------------------------------------------------------------------
4
5
6#-----------------------------------------------------------------------------
7# default installation target for applications
8#-----------------------------------------------------------------------------
9contains(TEMPLATE, app) {
10 target.path = $${INSTALL_PREFIX}/bin
11 INSTALLS += target
12 message("====")
13 message("==== INSTALLS += target")
14}
15
16
17#-----------------------------------------------------------------------------
18# default installation target for libraries
19#-----------------------------------------------------------------------------
20contains(TEMPLATE, lib) {
21 isEmpty(target.path) {
22 target.path = $${INSTALL_LIBDIR}
23 }
24 INSTALLS += target
25 message("====")
26 message("==== INSTALLS += target")
27}
28
29#-----------------------------------------------------------------------------
30# target for header files
31#-----------------------------------------------------------------------------
32!isEmpty(headers.files) {
33 headers.path = $${INSTALL_PREFIX}/include/$${TARGET}
34 INSTALLS += headers
35 message("====")
36 message("==== INSTALLS += headers")
37} else {
38 message("====")
39 message("==== NOTE: Remember to add your API headers into `headers.files' for installation!")
40}
41
42
43# End of File
044
=== added file 'common-pkgconfig.pri'
--- common-pkgconfig.pri 1970-01-01 00:00:00 +0000
+++ common-pkgconfig.pri 2016-09-21 07:16:22 +0000
@@ -0,0 +1,12 @@
1# Include this file after defining the pkgconfig.files variable
2
3!isEmpty(pkgconfig.files) {
4 QMAKE_SUBSTITUTES += $${pkgconfig.files}.in
5 pkgconfig.CONFIG = no_check_exist
6 pkgconfig.path = $${INSTALL_LIBDIR}/pkgconfig
7 QMAKE_EXTRA_TARGETS += pkgconfig
8
9 INSTALLS += pkgconfig
10
11 QMAKE_CLEAN += $${pkgconfig.files}
12}
013
=== added file 'common-project-config.pri'
--- common-project-config.pri 1970-01-01 00:00:00 +0000
+++ common-project-config.pri 2016-09-21 07:16:22 +0000
@@ -0,0 +1,36 @@
1#-----------------------------------------------------------------------------
2# Common configuration for all projects.
3#-----------------------------------------------------------------------------
4
5# we don't like warnings...
6QMAKE_CXXFLAGS += -Werror
7# Disable RTTI
8QMAKE_CXXFLAGS += -fno-exceptions -fno-rtti
9
10CONFIG += c++11
11
12!defined(TOP_SRC_DIR, var) {
13 TOP_SRC_DIR = $$PWD
14 TOP_BUILD_DIR = $${TOP_SRC_DIR}/$(BUILD_DIR)
15}
16
17include(coverage.pri)
18
19#-----------------------------------------------------------------------------
20# setup the installation prefix
21#-----------------------------------------------------------------------------
22INSTALL_PREFIX = /usr # default installation prefix
23
24# default prefix can be overriden by defining PREFIX when running qmake
25isEmpty(PREFIX) {
26 message("====")
27 message("==== NOTE: To override the installation path run: `qmake PREFIX=/custom/path'")
28 message("==== (current installation path is `$${INSTALL_PREFIX}')")
29} else {
30 INSTALL_PREFIX = $${PREFIX}
31 message("====")
32 message("==== install prefix set to `$${INSTALL_PREFIX}'")
33}
34
35I18N_DOMAIN="account-polld"
36PLUGIN_DATA_FILE="account-polld/plugin_data.json"
037
=== added file 'common-vars.pri'
--- common-vars.pri 1970-01-01 00:00:00 +0000
+++ common-vars.pri 2016-09-21 07:16:22 +0000
@@ -0,0 +1,18 @@
1#-----------------------------------------------------------------------------
2# Common variables for all projects.
3#-----------------------------------------------------------------------------
4
5
6#-----------------------------------------------------------------------------
7# Project name (used e.g. in include file and doc install path).
8# remember to update debian/* files if you changes this
9#-----------------------------------------------------------------------------
10PROJECT_NAME = account-polld
11
12#-----------------------------------------------------------------------------
13# Project version
14# remember to update debian/* files if you changes this
15#-----------------------------------------------------------------------------
16PROJECT_VERSION = 0.2
17
18# End of File
019
=== added file 'coverage.pri'
--- coverage.pri 1970-01-01 00:00:00 +0000
+++ coverage.pri 2016-09-21 07:16:22 +0000
@@ -0,0 +1,48 @@
1# Coverage
2CONFIG(coverage) {
3 OBJECTS_DIR =
4 MOC_DIR =
5
6 LIBS += -lgcov
7 QMAKE_CXXFLAGS += --coverage
8 QMAKE_LDFLAGS += --coverage
9
10 QMAKE_EXTRA_TARGETS += coverage cov
11 QMAKE_EXTRA_TARGETS += clean-gcno clean-gcda coverage-html \
12 generate-coverage-html clean-coverage-html coverage-gcovr \
13 generate-gcovr generate-coverage-gcovr clean-coverage-gcovr
14
15 clean-gcno.commands = \
16 "@echo Removing old coverage instrumentation"; \
17 "find -name '*.gcno' -print | xargs -r rm"
18
19 clean-gcda.commands = \
20 "@echo Removing old coverage results"; \
21 "find -name '*.gcda' -print | xargs -r rm"
22
23 coverage-html.depends = clean-gcda check generate-coverage-html
24
25 generate-coverage-html.commands = \
26 "@echo Collecting coverage data"; \
27 "lcov --directory $${TOP_SRC_DIR} --capture --output-file coverage.info --no-checksum --compat-libtool"; \
28 "lcov --extract coverage.info \"*/account-polld/*.cpp\" -o coverage.info"; \
29 "lcov --remove coverage.info \"moc_*.cpp\" --remove coverage.info \"tests/*.cpp\" -o coverage.info"; \
30 "LANG=C genhtml --prefix $${TOP_SRC_DIR} --output-directory coverage-html --title \"Code Coverage\" --legend --show-details coverage.info"
31
32 clean-coverage-html.depends = clean-gcda
33 clean-coverage-html.commands = \
34 "lcov --directory $${TOP_SRC_DIR} -z"; \
35 "rm -rf coverage.info coverage-html"
36
37 coverage-gcovr.depends = clean-gcda check generate-coverage-gcovr
38
39 generate-coverage-gcovr.commands = \
40 "@echo Generating coverage GCOVR report"; \
41 "gcovr -x -r $${TOP_SRC_DIR} -o $${TOP_SRC_DIR}/coverage.xml -e \".*/moc_.*\" -e \"tests/.*\" -e \".*\\.h\""
42
43 clean-coverage-gcovr.depends = clean-gcda
44 clean-coverage-gcovr.commands = \
45 "rm -rf $${TOP_SRC_DIR}/coverage.xml"
46
47 QMAKE_CLEAN += *.gcda *.gcno coverage.info coverage.xml
48}
049
=== modified file 'debian/control'
--- debian/control 2015-09-09 08:50:05 +0000
+++ debian/control 2016-09-21 07:16:22 +0000
@@ -3,22 +3,19 @@
3Priority: optional3Priority: optional
4Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>4Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
5Build-Depends: debhelper (>= 9),5Build-Depends: debhelper (>= 9),
6 dh-golang,
7 dh-translations,6 dh-translations,
8 golang-go,7 libaccounts-qt5-dev,
9 golang-go-dbus-dev,8 libqtdbusmock1-dev,
10 golang-go-xdg-dev,9 libqtdbustest1-dev,
11 golang-gocheck-dev,10 libsignon-qt5-dev,
12 golang-ubuntu-push-dev,11 pkg-config,
13 libaccounts-glib-dev,
14 libclick-0.4-dev,
15 libsignon-glib-dev,
16 qt5-default,12 qt5-default,
17 qtbase5-dev,13 qtbase5-dev,
18 qtpim5-dev,14 python3,
15 python3-xdg,
19Standards-Version: 3.9.516Standards-Version: 3.9.5
20Homepage: https://launchpad.net/account-polld17Homepage: https://launchpad.net/account-polld
21Vcs-Browser: http://bazaar.launchpad.net/~phablet-team/account-polld/trunk/files18Vcs-Browser: http://bazaar.launchpad.net/~online-accounts/account-polld/trunk/files
22Vcs-Bzr: lp:account-polld19Vcs-Bzr: lp:account-polld
2320
24Package: account-polld21Package: account-polld
@@ -27,10 +24,8 @@
27 ${misc:Depends},24 ${misc:Depends},
28 ${shlibs:Depends},25 ${shlibs:Depends},
29Built-Using: ${misc:Built-Using}26Built-Using: ${misc:Built-Using}
30Recommends: accountsservice,27Recommends: account-polld-plugins-go,
31Description: Poll daemon for notifications though the Ubuntu Push Client28Description: Poll daemon for notifications though the Ubuntu Push Client
32 This component polls twitter and gmail for updates and29 This component polls remote services for updates and communicates with the
33 communicates with the postal service provided by the ubuntu push client30 postal service provided by the ubuntu push client to expose notifications for
34 to expose notifications for the click webapps for the aforementioned31 the click webapps for the aforementioned services.
35 services.
36X-Ubuntu-Use-Langpack: yes
3732
=== modified file 'debian/rules'
--- debian/rules 2014-10-01 13:02:24 +0000
+++ debian/rules 2016-09-21 07:16:22 +0000
@@ -2,38 +2,7 @@
2# -*- makefile -*-2# -*- makefile -*-
33
4export DH_OPTIONS4export DH_OPTIONS
5export DH_GOPKG := launchpad.net/account-polld5
6export DH_GOLANG_INSTALL_ALL := 1
76
8%:7%:
9 dh $@ \8 dh $@
10 --buildsystem=golang \
11 --with=golang \
12 --with=translations \
13 --fail-missing
14
15override_dh_auto_install:
16 dh_auto_install -O--buildsystem=golang
17 rm \
18 ${CURDIR}/debian/account-polld/usr/bin/account-watcher-test
19 # all our libs are private
20 rm -r \
21 ${CURDIR}/debian/account-polld/usr/share/gocode
22 # setup online accounts service files
23 mkdir -p \
24 ${CURDIR}/debian/account-polld/usr/share/applications
25 cp ${CURDIR}/data/account-polld.desktop \
26 ${CURDIR}/debian/account-polld/usr/share/applications/
27 # translations
28 appname=account-polld; \
29 for pofile in po/*.po; do \
30 pofilename="$${pofile##*/}"; \
31 langcode="$${pofilename%.*}"; \
32 localedir="debian/$$appname/usr/share/locale/$$langcode/LC_MESSAGES"; \
33 mkdir -p $$localedir; \
34 mofile="$$localedir/$$appname.mo"; \
35 msgfmt -o $$mofile $$pofile; \
36 done
37
38override_dh_strip:
39 echo "Skipping strip (LP: #1318027)"
409
=== removed directory 'gettext'
=== removed file 'gettext/LICENSE'
--- gettext/LICENSE 2014-07-29 18:02:58 +0000
+++ gettext/LICENSE 1970-01-01 00:00:00 +0000
@@ -1,20 +0,0 @@
1Copyright (c) 2012-2013 José Carlos Nieto, http://xiam.menteslibres.org/
2
3Permission is hereby granted, free of charge, to any person obtaining
4a copy of this software and associated documentation files (the
5"Software"), to deal in the Software without restriction, including
6without limitation the rights to use, copy, modify, merge, publish,
7distribute, sublicense, and/or sell copies of the Software, and to
8permit persons to whom the Software is furnished to do so, subject to
9the following conditions:
10
11The above copyright notice and this permission notice shall be
12included in all copies or substantial portions of the Software.
13
14THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
210
=== removed file 'gettext/README.md'
--- gettext/README.md 2014-07-29 18:02:58 +0000
+++ gettext/README.md 1970-01-01 00:00:00 +0000
@@ -1,94 +0,0 @@
1# gosexy/gettext
2
3Go bindings for [GNU gettext][1], an internationalization and localization
4library for writing multilingual systems.
5
6## Requeriments
7
8The GNU C library. If you're using GNU/Linux, FreeBSD or OSX you should already
9have it.
10
11## Installation
12
13Use `go get` to download and install the binding:
14
15```sh
16go get github.com/gosexy/gettext
17```
18
19## Usage
20
21```go
22package main
23
24import (
25 "github.com/gosexy/gettext"
26 "fmt"
27 "os"
28)
29
30func main() {
31 gettext.BindTextdomain("example", ".")
32 gettext.Textdomain("example")
33
34 os.Setenv("LANGUAGE", "es_MX.utf8")
35
36 gettext.SetLocale(gettext.LC_ALL, "")
37
38 fmt.Println(gettext.Gettext("Hello, world!"))
39}
40```
41
42You can use `os.Setenv` to set the `LANGUAGE` environment variable or set it
43on a terminal:
44
45```sh
46export LANGUAGE="es_MX.utf8"
47./gettext-program
48```
49
50Note that `xgettext` does not officially support Go syntax yet, however, you
51can generate a valid `.pot` file by forcing `xgettest` to use the C++
52syntax:
53
54```sh
55xgettext -d example -s gettext_test.go -o example.pot -L c++ -i \
56--keyword=NGettext:1,2 --keyword=Gettext
57```
58
59This will generate a `example.pot` file.
60
61After translating the `.pot` file, you must generate `.po` and `.mo` files and
62remember to set the UTF-8 charset.
63
64```sh
65msginit -l es_MX -o example.po -i example.pot
66msgfmt -c -v -o example.mo example.po
67```
68
69Finally, move the `.mo` file to an appropriate location.
70
71```sh
72mv example.mo examples/es_MX.utf8/LC_MESSAGES/example.mo
73```
74
75## Documentation
76
77You can read `gosexy/gettext` documentation from a terminal
78
79```sh
80go doc github.com/gosexy/gettext
81```
82
83Or you can [browse it](http://godoc.org/github.com/gosexy/gettext) online.
84
85The original gettext documentation could be very useful as well:
86
87```sh
88man 3 gettext
89```
90
91Here's another [good tutorial][2] on using gettext.
92
93[1]: http://www.gnu.org/software/gettext/
94[2]: http://oriya.sarovar.org/docs/gettext_single.html
950
=== removed file 'gettext/gettext.go'
--- gettext/gettext.go 2014-07-29 18:02:58 +0000
+++ gettext/gettext.go 1970-01-01 00:00:00 +0000
@@ -1,207 +0,0 @@
1/*
2 Copyright (c) 2012 José Carlos Nieto, http://xiam.menteslibres.org/
3
4 Permission is hereby granted, free of charge, to any person obtaining
5 a copy of this software and associated documentation files (the
6 "Software"), to deal in the Software without restriction, including
7 without limitation the rights to use, copy, modify, merge, publish,
8 distribute, sublicense, and/or sell copies of the Software, and to
9 permit persons to whom the Software is furnished to do so, subject to
10 the following conditions:
11
12 The above copyright notice and this permission notice shall be
13 included in all copies or substantial portions of the Software.
14
15 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19 LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20 OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22*/
23
24package gettext
25
26/*
27
28#include <libintl.h>
29#include <locale.h>
30#include <stdlib.h>
31*/
32import "C"
33
34import (
35 "fmt"
36 "strings"
37 "unsafe"
38)
39
40var (
41 // For all of the locale.
42 LC_ALL = uint(C.LC_ALL)
43
44 // For regular expression matching (it determines the meaning of range
45 // expressions and equivalence classes) and string collation.
46 LC_COLATE = uint(C.LC_ALL)
47
48 // For regular expression matching, character classification, conversion,
49 // case-sensitive comparison, and wide character functions.
50 LC_CTYPE = uint(C.LC_CTYPE)
51
52 // For localizable natural-language messages.
53 LC_MESSAGES = uint(C.LC_MESSAGES)
54
55 // For monetary formatting.
56 LC_MONETARY = uint(C.LC_MONETARY)
57
58 // For number formatting (such as the decimal point and the thousands
59 // separator).
60 LC_NUMERIC = uint(C.LC_NUMERIC)
61
62 // For time and date formatting.
63 LC_TIME = uint(C.LC_TIME)
64)
65
66// Sets or queries the program's current locale.
67func SetLocale(category uint, locale string) string {
68 clocale := C.CString(locale)
69
70 res := C.GoString(C.setlocale(C.int(category), clocale))
71
72 C.free(unsafe.Pointer(clocale))
73 return res
74}
75
76// Sets directory containing message catalogs.
77func BindTextdomain(domainname string, dirname string) string {
78 cdirname := C.CString(dirname)
79 cdomainname := C.CString(domainname)
80
81 res := C.GoString(C.bindtextdomain(cdomainname, cdirname))
82
83 C.free(unsafe.Pointer(cdirname))
84 C.free(unsafe.Pointer(cdomainname))
85 return res
86}
87
88// Sets the output codeset for message catalogs for domain domainname.
89func BindTextdomainCodeset(domainname string, codeset string) string {
90 cdomainname := C.CString(domainname)
91 ccodeset := C.CString(codeset)
92
93 res := C.GoString(C.bind_textdomain_codeset(cdomainname, ccodeset))
94
95 C.free(unsafe.Pointer(cdomainname))
96 C.free(unsafe.Pointer(ccodeset))
97 return res
98}
99
100// Sets or retrieves the current message domain.
101func Textdomain(domainname string) string {
102 cdomainname := C.CString(domainname)
103
104 res := C.GoString(C.textdomain(cdomainname))
105
106 C.free(unsafe.Pointer(cdomainname))
107 return res
108}
109
110// Attempt to translate a text string into the user's native language, by
111// looking up the translation in a message catalog.
112func Gettext(msgid string) string {
113 cmsgid := C.CString(msgid)
114
115 res := C.GoString(C.gettext(cmsgid))
116
117 C.free(unsafe.Pointer(cmsgid))
118 return res
119}
120
121// Like Gettext(), but looking up the message in the specified domain.
122func DGettext(domain string, msgid string) string {
123 cdomain := C.CString(domain)
124 cmsgid := C.CString(msgid)
125
126 res := C.GoString(C.dgettext(cdomain, cmsgid))
127
128 C.free(unsafe.Pointer(cdomain))
129 C.free(unsafe.Pointer(cmsgid))
130 return res
131}
132
133// Like Gettext(), but looking up the message in the specified domain and
134// category.
135func DCGettext(domain string, msgid string, category uint) string {
136 cdomain := C.CString(domain)
137 cmsgid := C.CString(msgid)
138
139 res := C.GoString(C.dcgettext(cdomain, cmsgid, C.int(category)))
140
141 C.free(unsafe.Pointer(cdomain))
142 C.free(unsafe.Pointer(cmsgid))
143 return res
144}
145
146// Attempt to translate a text string into the user's native language, by
147// looking up the appropriate plural form of the translation in a message
148// catalog.
149func NGettext(msgid string, msgid_plural string, n uint64) string {
150 cmsgid := C.CString(msgid)
151 cmsgid_plural := C.CString(msgid_plural)
152
153 res := C.GoString(C.ngettext(cmsgid, cmsgid_plural, C.ulong(n)))
154
155 C.free(unsafe.Pointer(cmsgid))
156 C.free(unsafe.Pointer(cmsgid_plural))
157
158 return res
159}
160
161// Like fmt.Sprintf() but without %!(EXTRA) errors.
162func Sprintf(format string, a ...interface{}) string {
163 expects := strings.Count(format, "%") - strings.Count(format, "%%")
164
165 if expects > 0 {
166 arguments := make([]interface{}, expects)
167 for i := 0; i < expects; i++ {
168 if len(a) > i {
169 arguments[i] = a[i]
170 }
171 }
172 return fmt.Sprintf(format, arguments...)
173 }
174
175 return format
176}
177
178// Like NGettext(), but looking up the message in the specified domain.
179func DNGettext(domainname string, msgid string, msgid_plural string, n uint64) string {
180 cdomainname := C.CString(domainname)
181 cmsgid := C.CString(msgid)
182 cmsgid_plural := C.CString(msgid_plural)
183
184 res := C.GoString(C.dngettext(cdomainname, cmsgid, cmsgid_plural, C.ulong(n)))
185
186 C.free(unsafe.Pointer(cdomainname))
187 C.free(unsafe.Pointer(cmsgid))
188 C.free(unsafe.Pointer(cmsgid_plural))
189
190 return res
191}
192
193// Like NGettext(), but looking up the message in the specified domain and
194// category.
195func DCNGettext(domainname string, msgid string, msgid_plural string, n uint64, category uint) string {
196 cdomainname := C.CString(domainname)
197 cmsgid := C.CString(msgid)
198 cmsgid_plural := C.CString(msgid_plural)
199
200 res := C.GoString(C.dcngettext(cdomainname, cmsgid, cmsgid_plural, C.ulong(n), C.int(category)))
201
202 C.free(unsafe.Pointer(cdomainname))
203 C.free(unsafe.Pointer(cmsgid))
204 C.free(unsafe.Pointer(cmsgid_plural))
205
206 return res
207}
2080
=== removed directory 'plugins'
=== removed directory 'plugins/caldav'
=== removed file 'plugins/caldav/api.go'
--- plugins/caldav/api.go 2016-07-12 19:40:07 +0000
+++ plugins/caldav/api.go 1970-01-01 00:00:00 +0000
@@ -1,54 +0,0 @@
1/*
2 Copyright 2014 Canonical Ltd.
3
4 This program is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License version 3, as published
6 by the Free Software Foundation.
7
8 This program is distributed in the hope that it will be useful, but
9 WITHOUT ANY WARRANTY; without even the implied warranties of
10 MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11 PURPOSE. See the GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License along
14 with this program. If not, see <http://www.gnu.org/licenses/>.
15*/
16
17package caldav
18
19import (
20 "fmt"
21)
22
23// eventList holds a response to call to Calendar.events: list
24// defined in https://developers.google.com/google-apps/calendar/v3/reference/events/list#response
25type eventList struct {
26 // Messages holds a list of message.
27 Events []event `json:"items"`
28}
29
30// event holds the event data response for a Calendar.event.
31// The full definition of a message is defined in
32// https://developers.google.com/google-apps/calendar/v3/reference/events#resource-representations
33type event struct {
34 // Id is the immutable ID of the message.
35 Etag string `json:"etag"`
36 // ThreadId is the ID of the thread the message belongs to.
37 Summary string `json:"summary"`
38}
39
40func (e event) String() string {
41 return fmt.Sprintf("Id: %s, snippet: '%s'\n", e.Etag, e.Summary)
42}
43
44type errorResp struct {
45 Err struct {
46 Code uint64 `json:"code"`
47 Message string `json:"message"`
48 Errors []struct {
49 Domain string `json:"domain"`
50 Reason string `json:"reason"`
51 Message string `json:"message"`
52 } `json:"errors"`
53 } `json:"error"`
54}
550
=== removed file 'plugins/caldav/caldav.go'
--- plugins/caldav/caldav.go 2016-08-03 11:59:28 +0000
+++ plugins/caldav/caldav.go 1970-01-01 00:00:00 +0000
@@ -1,201 +0,0 @@
1/*
2 Copyright 2016 Canonical Ltd.
3
4 This program is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License version 3, as published
6 by the Free Software Foundation.
7
8 This program is distributed in the hope that it will be useful, but
9 WITHOUT ANY WARRANTY; without even the implied warranties of
10 MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11 PURPOSE. See the GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License along
14 with this program. If not, see <http://www.gnu.org/licenses/>.
15*/
16
17package caldav
18
19import (
20 "bytes"
21 "fmt"
22 "io/ioutil"
23 "log"
24 "net/http"
25 "net/url"
26 "os"
27 "strings"
28 "time"
29
30 "launchpad.net/account-polld/accounts"
31 "launchpad.net/account-polld/plugins"
32 "launchpad.net/account-polld/syncmonitor"
33)
34
35const (
36 APP_ID = "com.ubuntu.calendar_calendar"
37 pluginName = "caldav"
38)
39
40type CalDavPlugin struct {
41 accountId uint
42}
43
44func New(accountId uint) *CalDavPlugin {
45 return &CalDavPlugin{accountId: accountId}
46}
47
48func (p *CalDavPlugin) ApplicationId() plugins.ApplicationId {
49 return plugins.ApplicationId(APP_ID)
50}
51
52func (p *CalDavPlugin) Poll(authData *accounts.AuthData) ([]*plugins.PushMessageBatch, error) {
53 // This envvar check is to ease testing.
54 if token := os.Getenv("ACCOUNT_POLLD_TOKEN_CALDAV"); token != "" {
55 log.Print("Using token from: ACCOUNT_POLLD_TOKEN_CALDAV env var")
56 authData.AccessToken = token
57 }
58
59 log.Print("Check calendar changes for account:", p.accountId)
60
61 syncMonitor := syncmonitor.NewSyncMonitor()
62 if syncMonitor == nil {
63 log.Print("Sync monitor not available yet.")
64 return nil, nil
65 }
66
67 state, err := syncMonitor.State()
68 if err != nil {
69 log.Print("Fail to retrieve sync monitor state ", err)
70 return nil, nil
71 }
72 if state != "idle" {
73 log.Print("Sync monitor is not on 'idle' state, try later!")
74 return nil, nil
75 }
76
77 calendars, err := syncMonitor.ListCalendarsByAccount(p.accountId)
78 if err != nil {
79 log.Print("Calendar plugin ", p.accountId, ": cannot load calendars: ", err)
80 return nil, nil
81 }
82
83 var calendarsToSync []string
84 log.Print("Number of calendars for account:", p.accountId, " size:", len(calendars))
85
86 for id, calendar := range calendars {
87 lastSyncDate, err := syncMonitor.LastSyncDate(p.accountId, id)
88 if err != nil {
89 log.Print("\tcalendar: ", id, ", cannot load previous sync date: ", err, ". Try next time.")
90 continue
91 } else {
92 log.Print("\tcalendar: ", id, " Url: ", calendar, " last sync date: ", lastSyncDate)
93 }
94
95 var needSync bool
96 needSync = (len(lastSyncDate) == 0)
97
98 if !needSync {
99 resp, err := p.requestChanges(authData, calendar, lastSyncDate)
100 if err != nil {
101 log.Print("\tERROR: Fail to query for changes: ", err)
102 continue
103 }
104
105 needSync, err = p.containEvents(resp)
106 if err != nil {
107 log.Print("\tERROR: Fail to parse changes: ", err)
108 if err == plugins.ErrTokenExpired {
109 log.Print("\t\tAbort poll")
110 return nil, err
111 } else {
112 continue
113 }
114 }
115 }
116
117 if needSync {
118 log.Print("\tCalendar needs sync: ", id)
119 calendarsToSync = append(calendarsToSync, id)
120 } else {
121 log.Print("\tFound no calendar updates for account: ", p.accountId, " calendar: ", id)
122 }
123 }
124
125 if len(calendarsToSync) > 0 {
126 log.Print("Request account sync")
127 err = syncMonitor.SyncAccount(p.accountId, calendarsToSync)
128 if err != nil {
129 log.Print("ERROR: Fail to start account sync ", p.accountId, " message: ", err)
130 }
131 }
132
133 return nil, nil
134}
135
136func (p *CalDavPlugin) containEvents(resp *http.Response) (bool, error) {
137 defer resp.Body.Close()
138 log.Print("RESPONSE CODE ----:", resp.StatusCode)
139
140 if resp.StatusCode != 207 {
141 var errResp errorResp
142 log.Print("Invalid response:", errResp.Err.Code)
143 return false, nil
144 } else {
145 data, err := ioutil.ReadAll(resp.Body)
146 if err != nil {
147 return false, err
148 }
149 fmt.Printf("DATA: %s", data)
150 return strings.Contains(string(data), "BEGIN:VEVENT"), nil
151 }
152
153 return false, nil
154}
155
156func (p *CalDavPlugin) requestChanges(authData *accounts.AuthData, calendar string, lastSyncDate string) (*http.Response, error) {
157 u, err := url.Parse(calendar)
158 if err != nil {
159 return nil, err
160 }
161 startDate, err := time.Parse(time.RFC3339, lastSyncDate)
162 if err != nil {
163 log.Print("Fail to parse date: ", lastSyncDate)
164 return nil, err
165 }
166
167 // Start date will be one minute before last sync
168 startDate = startDate.Add(time.Duration(-1) * time.Minute)
169
170 // End Date will be one year in the future from now
171 endDate := time.Now().AddDate(1, 0, 0).UTC()
172
173 log.Print("Calendar Url:", calendar)
174
175 query := "<c:calendar-query xmlns:d=\"DAV:\" xmlns:c=\"urn:ietf:params:xml:ns:caldav\">\n"
176 query += "<d:prop>\n"
177 query += "<d:getetag />\n"
178 query += "<c:calendar-data />\n"
179 query += "</d:prop>\n"
180 query += "<c:filter>\n"
181 query += "<c:comp-filter name=\"VCALENDAR\">\n"
182 query += "<c:comp-filter name=\"VEVENT\">\n"
183 query += "<c:prop-filter name=\"LAST-MODIFIED\">\n"
184 query += "<c:time-range start=\"" + startDate.Format("20060102T150405Z") + "\" end=\"" + endDate.Format("20060102T150405Z") + "\"/>\n"
185 query += "</c:prop-filter>\n"
186 query += "</c:comp-filter>\n"
187 query += "</c:comp-filter>\n"
188 query += "</c:filter>\n"
189 query += "</c:calendar-query>\n"
190 log.Print("Query: ", query)
191 req, err := http.NewRequest("REPORT", u.String(), bytes.NewBufferString(query))
192 if err != nil {
193 return nil, err
194 }
195 req.Header.Set("Depth", "1")
196 req.Header.Set("Prefer", "return-minimal")
197 req.Header.Set("Content-Type", "application/xml; charset=utf-8")
198 req.SetBasicAuth(authData.UserName, authData.Secret)
199
200 return http.DefaultClient.Do(req)
201}
2020
=== removed directory 'plugins/dekko'
=== removed file 'plugins/dekko/api.go'
--- plugins/dekko/api.go 2016-06-15 14:41:33 +0000
+++ plugins/dekko/api.go 1970-01-01 00:00:00 +0000
@@ -1,127 +0,0 @@
1/*
2 Copyright 2014 Canonical Ltd.
3
4 This program is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License version 3, as published
6 by the Free Software Foundation.
7
8 This program is distributed in the hope that it will be useful, but
9 WITHOUT ANY WARRANTY; without even the implied warranties of
10 MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11 PURPOSE. See the GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License along
14 with this program. If not, see <http://www.gnu.org/licenses/>.
15*/
16
17package dekko
18
19import (
20 "fmt"
21 "time"
22
23 "launchpad.net/account-polld/plugins"
24)
25
26const gmailTime = "Mon, 2 Jan 2006 15:04:05 -0700"
27
28type pushes map[string]*plugins.PushMessage
29type headers map[string]string
30
31// messageList holds a response to call to Users.messages: list
32// defined in https://developers.google.com/gmail/api/v1/reference/users/messages/list
33type messageList struct {
34 // Messages holds a list of message.
35 Messages []message `json:"messages"`
36 // NextPageToken is used to retrieve the next page of results in the list.
37 NextPageToken string `json:"nextPageToken"`
38 // ResultSizeEstimage is the estimated total number of results.
39 ResultSizeEstimage uint64 `json:"resultSizeEstimate"`
40}
41
42// message holds a partial response for a Users.messages.
43// The full definition of a message is defined in
44// https://developers.google.com/gmail/api/v1/reference/users/messages#resource
45type message struct {
46 // Id is the immutable ID of the message.
47 Id string `json:"id"`
48 // ThreadId is the ID of the thread the message belongs to.
49 ThreadId string `json:"threadId"`
50 // HistoryId is the ID of the last history record that modified
51 // this message.
52 HistoryId string `json:"historyId"`
53 // Snippet is a short part of the message text. This text is
54 // used for the push message summary.
55 Snippet string `json:"snippet"`
56 // Payload represents the message payload.
57 Payload payload `json:"payload"`
58}
59
60func (m message) String() string {
61 return fmt.Sprintf("Id: %d, snippet: '%s'\n", m.Id, m.Snippet[:10])
62}
63
64// ById implements sort.Interface for []message based on
65// the Id field.
66type byId []message
67
68func (m byId) Len() int { return len(m) }
69func (m byId) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
70func (m byId) Less(i, j int) bool { return m[i].Id < m[j].Id }
71
72// payload represents the message payload.
73type payload struct {
74 Headers []messageHeader `json:"headers"`
75}
76
77func (p *payload) mapHeaders() headers {
78 headers := make(map[string]string)
79 for _, hdr := range p.Headers {
80 headers[hdr.Name] = hdr.Value
81 }
82 return headers
83}
84
85func (hdr headers) getTimestamp() time.Time {
86 timestamp, ok := hdr[hdrDATE]
87 if !ok {
88 return time.Now()
89 }
90
91 if t, err := time.Parse(gmailTime, timestamp); err == nil {
92 return t
93 }
94 return time.Now()
95}
96
97func (hdr headers) getEpoch() int64 {
98 return hdr.getTimestamp().Unix()
99}
100
101// messageHeader represents the message headers.
102type messageHeader struct {
103 Name string `json:"name"`
104 Value string `json:"value"`
105}
106
107type errorResp struct {
108 Err struct {
109 Code uint64 `json:"code"`
110 Message string `json:"message"`
111 Errors []struct {
112 Domain string `json:"domain"`
113 Reason string `json:"reason"`
114 Message string `json:"message"`
115 } `json:"errors"`
116 } `json:"error"`
117}
118
119func (err *errorResp) Error() string {
120 return fmt.Sprint("backend response:", err.Err.Message)
121}
122
123const (
124 hdrDATE = "Date"
125 hdrSUBJECT = "Subject"
126 hdrFROM = "From"
127)
1280
=== removed file 'plugins/dekko/dekko.go'
--- plugins/dekko/dekko.go 2016-07-22 09:46:36 +0000
+++ plugins/dekko/dekko.go 1970-01-01 00:00:00 +0000
@@ -1,346 +0,0 @@
1/*
2 Copyright 2014 Canonical Ltd.
3
4 This program is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License version 3, as published
6 by the Free Software Foundation.
7
8 This program is distributed in the hope that it will be useful, but
9 WITHOUT ANY WARRANTY; without even the implied warranties of
10 MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11 PURPOSE. See the GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License along
14 with this program. If not, see <http://www.gnu.org/licenses/>.
15*/
16
17package dekko
18
19import (
20 "encoding/json"
21 "fmt"
22 "net/http"
23 "net/mail"
24 "net/url"
25 "os"
26 "regexp"
27 "sort"
28 "strings"
29 "time"
30
31 "log"
32
33 "launchpad.net/account-polld/accounts"
34 "launchpad.net/account-polld/gettext"
35 "launchpad.net/account-polld/plugins"
36 "launchpad.net/account-polld/qtcontact"
37)
38
39const (
40 APP_ID = "dekko.dekkoproject_dekko"
41 dekkoDispatchUrl = "dekko://notify/%d/%s/%s"
42 // If there's more than 10 emails in one batch, we don't show 10 notification
43 // bubbles, but instead show one summary. We always show all notifications in the
44 // indicator.
45 individualNotificationsLimit = 10
46 pluginName = "dekko"
47)
48
49type reportedIdMap map[string]time.Time
50
51var baseUrl, _ = url.Parse("https://www.googleapis.com/gmail/v1/users/me/")
52
53// timeDelta defines how old messages can be to be reported.
54var timeDelta = time.Duration(time.Hour * 24)
55
56// trackDelta defines how old messages can be before removed from tracking
57var trackDelta = time.Duration(time.Hour * 24 * 7)
58
59// relativeTimeDelta is the same as timeDelta
60var relativeTimeDelta string = "1d"
61
62// regexp for identifying non-ascii characters
63var nonAsciiChars, _ = regexp.Compile("[^\x00-\x7F]")
64
65type GmailPlugin struct {
66 // reportedIds holds the messages that have already been notified. This
67 // approach is taken against timestamps as it avoids needing to call
68 // get on the message.
69 reportedIds reportedIdMap
70 accountId uint
71}
72
73func idsFromPersist(accountId uint) (ids reportedIdMap, err error) {
74 err = plugins.FromPersist(pluginName, accountId, &ids)
75 if err != nil {
76 return nil, err
77 }
78 // discard old ids
79 timestamp := time.Now()
80 for k, v := range ids {
81 delta := timestamp.Sub(v)
82 if delta > trackDelta {
83 log.Print("gmail plugin ", accountId, ": deleting ", k, " as ", delta, " is greater than ", trackDelta)
84 delete(ids, k)
85 }
86 }
87 return ids, nil
88}
89
90func (ids reportedIdMap) persist(accountId uint) (err error) {
91 err = plugins.Persist(pluginName, accountId, ids)
92 if err != nil {
93 log.Print("gmail plugin ", accountId, ": failed to save state: ", err)
94 }
95 return nil
96}
97
98func New(accountId uint) *GmailPlugin {
99 reportedIds, err := idsFromPersist(accountId)
100 if err != nil {
101 log.Print("gmail plugin ", accountId, ": cannot load previous state from storage: ", err)
102 } else {
103 log.Print("gmail plugin ", accountId, ": last state loaded from storage")
104 }
105 return &GmailPlugin{reportedIds: reportedIds, accountId: accountId}
106}
107
108func (p *GmailPlugin) ApplicationId() plugins.ApplicationId {
109 return plugins.ApplicationId(APP_ID)
110}
111
112func (p *GmailPlugin) Poll(authData *accounts.AuthData) ([]*plugins.PushMessageBatch, error) {
113 // This envvar check is to ease testing.
114 if token := os.Getenv("ACCOUNT_POLLD_TOKEN_GMAIL"); token != "" {
115 authData.AccessToken = token
116 }
117
118 resp, err := p.requestMessageList(authData.AccessToken)
119 if err != nil {
120 return nil, err
121 }
122 messages, err := p.parseMessageListResponse(resp)
123 if err != nil {
124 return nil, err
125 }
126
127 // TODO use the batching API defined in https://developers.google.com/gmail/api/guides/batch
128 for i := range messages {
129 resp, err := p.requestMessage(messages[i].Id, authData.AccessToken)
130 if err != nil {
131 return nil, err
132 }
133 messages[i], err = p.parseMessageResponse(resp)
134 if err != nil {
135 return nil, err
136 }
137 }
138 notif, err := p.createNotifications(messages)
139 if err != nil {
140 return nil, err
141 }
142 return []*plugins.PushMessageBatch{
143 &plugins.PushMessageBatch{
144 Messages: notif,
145 Limit: individualNotificationsLimit,
146 OverflowHandler: p.handleOverflow,
147 Tag: "dekko",
148 }}, nil
149
150}
151
152func (p *GmailPlugin) reported(id string) bool {
153 _, ok := p.reportedIds[id]
154 return ok
155}
156
157func (p *GmailPlugin) createNotifications(messages []message) ([]*plugins.PushMessage, error) {
158 timestamp := time.Now()
159 pushMsgMap := make(pushes)
160
161 for _, msg := range messages {
162 hdr := msg.Payload.mapHeaders()
163
164 from := hdr[hdrFROM]
165 var avatarPath string
166
167 emailAddress, err := mail.ParseAddress(from)
168 if err != nil {
169 // If the email address contains non-ascii characters, we get an
170 // error so we're going to try again, this time mangling the name
171 // by removing all non-ascii characters. We only care about the email
172 // address here anyway.
173 // XXX: We can't check the error message due to [1]: the error
174 // message is different in go < 1.3 and > 1.5.
175 // [1] https://github.com/golang/go/issues/12492
176 mangledAddr := nonAsciiChars.ReplaceAllString(from, "")
177 mangledEmail, mangledParseError := mail.ParseAddress(mangledAddr)
178 if mangledParseError == nil {
179 emailAddress = mangledEmail
180 }
181 } else if emailAddress.Name != "" {
182 // We only want the Name if the first ParseAddress
183 // call was successful. I.e. we do not want the name
184 // from a mangled email address.
185 from = emailAddress.Name
186 }
187
188 if emailAddress != nil {
189 avatarPath = qtcontact.GetAvatar(emailAddress.Address)
190 // If icon path starts with a path separator, assume local file path,
191 // encode it and prepend file scheme defined in RFC 1738.
192 if strings.HasPrefix(avatarPath, string(os.PathSeparator)) {
193 avatarPath = url.QueryEscape(avatarPath)
194 avatarPath = "file://" + avatarPath
195 }
196 }
197
198 msgStamp := hdr.getTimestamp()
199
200 if _, ok := pushMsgMap[msg.ThreadId]; ok {
201 // TRANSLATORS: the %s is an appended "from" corresponding to an specific email thread
202 pushMsgMap[msg.ThreadId].Notification.Card.Summary += fmt.Sprintf(gettext.Gettext(", %s"), from)
203 } else if timestamp.Sub(msgStamp) < timeDelta {
204 // TRANSLATORS: the %s is the "from" header corresponding to a specific email
205 summary := fmt.Sprintf(gettext.Gettext("%s"), from)
206 // TRANSLATORS: the first %s refers to the email "subject", the second %s refers "from"
207 body := fmt.Sprintf(gettext.Gettext("%s\n%s"), hdr[hdrSUBJECT], msg.Snippet)
208 // fmt with label personal and threadId
209 action := fmt.Sprintf(dekkoDispatchUrl, p.accountId, "INBOX", msg.Id)
210 epoch := hdr.getEpoch()
211 pushMsgMap[msg.ThreadId] = plugins.NewStandardPushMessage(summary, body, action, avatarPath, epoch)
212 } else {
213 log.Print("gmail plugin ", p.accountId, ": skipping message id ", msg.Id, " with date ", msgStamp, " older than ", timeDelta)
214 }
215 }
216 pushMsg := make([]*plugins.PushMessage, 0, len(pushMsgMap))
217 for _, v := range pushMsgMap {
218 pushMsg = append(pushMsg, v)
219 }
220 return pushMsg, nil
221
222}
223func (p *GmailPlugin) handleOverflow(pushMsg []*plugins.PushMessage) *plugins.PushMessage {
224 // TODO it would probably be better to grab the estimate that google returns in the message list.
225 approxUnreadMessages := len(pushMsg)
226
227 // TRANSLATORS: the %d refers to the number of new email messages.
228 summary := fmt.Sprintf(gettext.Gettext("You have %d new messages"), approxUnreadMessages)
229
230 body := ""
231
232 // fmt with label personal and no threadId
233 action := fmt.Sprintf(dekkoDispatchUrl, p.accountId, "INBOX")
234 epoch := time.Now().Unix()
235
236 return plugins.NewStandardPushMessage(summary, body, action, "", epoch)
237}
238
239func (p *GmailPlugin) parseMessageListResponse(resp *http.Response) ([]message, error) {
240 defer resp.Body.Close()
241 decoder := json.NewDecoder(resp.Body)
242
243 if resp.StatusCode != http.StatusOK {
244 var errResp errorResp
245 if err := decoder.Decode(&errResp); err != nil {
246 return nil, err
247 }
248 if errResp.Err.Code == 401 {
249 return nil, plugins.ErrTokenExpired
250 }
251 return nil, &errResp
252 }
253
254 var messages messageList
255 if err := decoder.Decode(&messages); err != nil {
256 return nil, err
257 }
258
259 filteredMsg := p.messageListFilter(messages.Messages)
260
261 return filteredMsg, nil
262}
263
264// messageListFilter returns a subset of unread messages where the subset
265// depends on not being in reportedIds. Before returning, reportedIds is
266// updated with the new list of unread messages.
267func (p *GmailPlugin) messageListFilter(messages []message) []message {
268 sort.Sort(byId(messages))
269 var reportMsg []message
270 var ids = make(reportedIdMap)
271
272 for _, msg := range messages {
273 if !p.reported(msg.Id) {
274 reportMsg = append(reportMsg, msg)
275 }
276 ids[msg.Id] = time.Now()
277 }
278 p.reportedIds = ids
279 p.reportedIds.persist(p.accountId)
280 return reportMsg
281}
282
283func (p *GmailPlugin) parseMessageResponse(resp *http.Response) (message, error) {
284 defer resp.Body.Close()
285 decoder := json.NewDecoder(resp.Body)
286
287 if resp.StatusCode != http.StatusOK {
288 var errResp errorResp
289 if err := decoder.Decode(&errResp); err != nil {
290 return message{}, err
291 }
292 return message{}, &errResp
293 }
294
295 var msg message
296 if err := decoder.Decode(&msg); err != nil {
297 return message{}, err
298 }
299
300 return msg, nil
301}
302
303func (p *GmailPlugin) requestMessage(id, accessToken string) (*http.Response, error) {
304 u, err := baseUrl.Parse("messages/" + id)
305 if err != nil {
306 return nil, err
307 }
308
309 query := u.Query()
310 // only request specific fields
311 query.Add("fields", "snippet,threadId,id,payload/headers")
312 // get the full message to get From and Subject from headers
313 query.Add("format", "full")
314 u.RawQuery = query.Encode()
315
316 req, err := http.NewRequest("GET", u.String(), nil)
317 if err != nil {
318 return nil, err
319 }
320 req.Header.Set("Authorization", "Bearer "+accessToken)
321
322 return http.DefaultClient.Do(req)
323}
324
325func (p *GmailPlugin) requestMessageList(accessToken string) (*http.Response, error) {
326 u, err := baseUrl.Parse("messages")
327 if err != nil {
328 return nil, err
329 }
330
331 query := u.Query()
332
333 // get all unread inbox emails received after
334 // the last time we checked. If this is the first
335 // time we check, get unread emails after timeDelta
336 query.Add("q", fmt.Sprintf("is:unread in:inbox newer_than:%s", relativeTimeDelta))
337 u.RawQuery = query.Encode()
338
339 req, err := http.NewRequest("GET", u.String(), nil)
340 if err != nil {
341 return nil, err
342 }
343 req.Header.Set("Authorization", "Bearer "+accessToken)
344
345 return http.DefaultClient.Do(req)
346}
3470
=== removed directory 'plugins/gcalendar'
=== removed file 'plugins/gcalendar/api.go'
--- plugins/gcalendar/api.go 2016-04-14 14:58:10 +0000
+++ plugins/gcalendar/api.go 1970-01-01 00:00:00 +0000
@@ -1,54 +0,0 @@
1/*
2 Copyright 2014 Canonical Ltd.
3
4 This program is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License version 3, as published
6 by the Free Software Foundation.
7
8 This program is distributed in the hope that it will be useful, but
9 WITHOUT ANY WARRANTY; without even the implied warranties of
10 MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11 PURPOSE. See the GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License along
14 with this program. If not, see <http://www.gnu.org/licenses/>.
15*/
16
17package gcalendar
18
19import (
20 "fmt"
21)
22
23// eventList holds a response to call to Calendar.events: list
24// defined in https://developers.google.com/google-apps/calendar/v3/reference/events/list#response
25type eventList struct {
26 // Messages holds a list of message.
27 Events []event `json:"items"`
28}
29
30// event holds the event data response for a Calendar.event.
31// The full definition of a message is defined in
32// https://developers.google.com/google-apps/calendar/v3/reference/events#resource-representations
33type event struct {
34 // Id is the immutable ID of the message.
35 Etag string `json:"etag"`
36 // ThreadId is the ID of the thread the message belongs to.
37 Summary string `json:"summary"`
38}
39
40func (e event) String() string {
41 return fmt.Sprintf("Id: %s, snippet: '%s'\n", e.Etag, e.Summary)
42}
43
44type errorResp struct {
45 Err struct {
46 Code uint64 `json:"code"`
47 Message string `json:"message"`
48 Errors []struct {
49 Domain string `json:"domain"`
50 Reason string `json:"reason"`
51 Message string `json:"message"`
52 } `json:"errors"`
53 } `json:"error"`
54}
550
=== removed file 'plugins/gcalendar/gcalendar.go'
--- plugins/gcalendar/gcalendar.go 2016-08-02 14:16:26 +0000
+++ plugins/gcalendar/gcalendar.go 1970-01-01 00:00:00 +0000
@@ -1,189 +0,0 @@
1/*
2 Copyright 2016 Canonical Ltd.
3
4 This program is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License version 3, as published
6 by the Free Software Foundation.
7
8 This program is distributed in the hope that it will be useful, but
9 WITHOUT ANY WARRANTY; without even the implied warranties of
10 MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11 PURPOSE. See the GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License along
14 with this program. If not, see <http://www.gnu.org/licenses/>.
15*/
16
17package gcalendar
18
19import (
20 "encoding/json"
21 "log"
22 "net/http"
23 "net/url"
24 "os"
25
26 "launchpad.net/account-polld/accounts"
27 "launchpad.net/account-polld/plugins"
28 "launchpad.net/account-polld/syncmonitor"
29)
30
31const (
32 APP_ID = "com.ubuntu.calendar_calendar"
33 pluginName = "gcalendar"
34)
35
36var baseUrl, _ = url.Parse("https://www.googleapis.com/calendar/v3/calendars/")
37
38type GCalendarPlugin struct {
39 accountId uint
40}
41
42func New(accountId uint) *GCalendarPlugin {
43 return &GCalendarPlugin{accountId: accountId}
44}
45
46func (p *GCalendarPlugin) ApplicationId() plugins.ApplicationId {
47 return plugins.ApplicationId(APP_ID)
48}
49
50func (p *GCalendarPlugin) Poll(authData *accounts.AuthData) ([]*plugins.PushMessageBatch, error) {
51 // This envvar check is to ease testing.
52 if token := os.Getenv("ACCOUNT_POLLD_TOKEN_GCALENDAR"); token != "" {
53 log.Print("calendar: Using token from: ACCOUNT_POLLD_TOKEN_GCALENDAR env var")
54 authData.AccessToken = token
55 }
56
57 log.Print("calendar: Check calendar changes for account:", p.accountId)
58
59 syncMonitor := syncmonitor.NewSyncMonitor()
60 if syncMonitor == nil {
61 log.Print("calendar: Sync monitor not available yet.")
62 return nil, nil
63 }
64
65 state, err := syncMonitor.State()
66 if err != nil {
67 log.Print("calendar: Fail to retrieve sync monitor state ", err)
68 return nil, nil
69 }
70 if state != "idle" {
71 log.Print("calendar: Sync monitor is not on 'idle' state, try later!")
72 return nil, nil
73 }
74
75 calendars, err := syncMonitor.ListCalendarsByAccount(p.accountId)
76 if err != nil {
77 log.Print("calendar: Calendar plugin ", p.accountId, ": cannot load calendars: ", err)
78 return nil, nil
79 }
80
81 var calendarsToSync []string
82 log.Print("calendar: Number of calendars for account:", p.accountId, " size:", len(calendars))
83
84 for id, calendar := range calendars {
85 lastSyncDate, err := syncMonitor.LastSyncDate(p.accountId, id)
86 if err != nil {
87 log.Print("\tcalendar: ", calendar, ", cannot load previous sync date: ", err, ". Try next time.")
88 continue
89 } else {
90 log.Print("\tcalendar: ", calendar, " Id: ", id, ": last sync date: ", lastSyncDate)
91 }
92
93 var needSync bool
94 needSync = (len(lastSyncDate) == 0)
95
96 if !needSync {
97 resp, err := p.requestChanges(authData.AccessToken, id, lastSyncDate)
98 if err != nil {
99 log.Print("\tcalendar: ERROR: Fail to query for changes: ", err)
100 continue
101 }
102
103 messages, err := p.parseChangesResponse(resp)
104 if err != nil {
105 log.Print("\tcalendar: ERROR: Fail to parse changes: ", err)
106 if err == plugins.ErrTokenExpired {
107 log.Print("\t\tcalendar: Abort poll")
108 return nil, err
109 } else {
110 continue
111 }
112 }
113 needSync = (len(messages) > 0)
114 }
115
116 if needSync {
117 log.Print("\tcalendar: Calendar needs sync: ", calendar)
118 calendarsToSync = append(calendarsToSync, id)
119 } else {
120 log.Print("\tcalendar: Found no calendar updates for account: ", p.accountId, " calendar: ", calendar)
121 }
122 }
123
124 if len(calendarsToSync) > 0 {
125 log.Print("calendar: Request account sync")
126 err = syncMonitor.SyncAccount(p.accountId, calendarsToSync)
127 if err != nil {
128 log.Print("calendar: ERROR: Fail to start account sync ", p.accountId, " message: ", err)
129 }
130 }
131
132 return nil, nil
133}
134
135func (p *GCalendarPlugin) parseChangesResponse(resp *http.Response) ([]event, error) {
136 defer resp.Body.Close()
137 decoder := json.NewDecoder(resp.Body)
138
139 if resp.StatusCode != http.StatusOK {
140 var errResp errorResp
141 log.Print("calendar: Invalid response:", errResp.Err.Code)
142 if err := decoder.Decode(&errResp); err != nil {
143 return nil, err
144 }
145 if errResp.Err.Code == 401 {
146 return nil, plugins.ErrTokenExpired
147 }
148 return nil, nil
149 }
150
151 var events eventList
152 if err := decoder.Decode(&events); err != nil {
153 log.Print("calendar: Fail to decode")
154 return nil, err
155 }
156
157 for _, ev := range events.Events {
158 log.Print("calendar: Found event: ", ev.Etag, ev.Summary)
159 }
160
161 return events.Events, nil
162}
163
164func (p *GCalendarPlugin) requestChanges(accessToken string, calendar string, lastSyncDate string) (*http.Response, error) {
165 u, err := baseUrl.Parse("")
166 if err != nil {
167 return nil, err
168 }
169 u.Path += calendar + "/events"
170
171 //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}
172 query := u.Query()
173 query.Add("showDeleted", "true")
174 query.Add("singleEvents", "true")
175 query.Add("fields", "description,items(summary,etag)")
176 query.Add("maxResults", "1")
177 if len(lastSyncDate) > 0 {
178 query.Add("updatedMin", lastSyncDate)
179 }
180 u.RawQuery = query.Encode()
181
182 req, err := http.NewRequest("GET", u.String(), nil)
183 if err != nil {
184 return nil, err
185 }
186 req.Header.Set("Authorization", "Bearer "+accessToken)
187
188 return http.DefaultClient.Do(req)
189}
1900
=== removed directory 'plugins/gmail'
=== removed file 'plugins/gmail/api.go'
--- plugins/gmail/api.go 2015-03-20 14:34:48 +0000
+++ plugins/gmail/api.go 1970-01-01 00:00:00 +0000
@@ -1,127 +0,0 @@
1/*
2 Copyright 2014 Canonical Ltd.
3
4 This program is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License version 3, as published
6 by the Free Software Foundation.
7
8 This program is distributed in the hope that it will be useful, but
9 WITHOUT ANY WARRANTY; without even the implied warranties of
10 MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11 PURPOSE. See the GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License along
14 with this program. If not, see <http://www.gnu.org/licenses/>.
15*/
16
17package gmail
18
19import (
20 "fmt"
21 "time"
22
23 "launchpad.net/account-polld/plugins"
24)
25
26const gmailTime = "Mon, 2 Jan 2006 15:04:05 -0700"
27
28type pushes map[string]*plugins.PushMessage
29type headers map[string]string
30
31// messageList holds a response to call to Users.messages: list
32// defined in https://developers.google.com/gmail/api/v1/reference/users/messages/list
33type messageList struct {
34 // Messages holds a list of message.
35 Messages []message `json:"messages"`
36 // NextPageToken is used to retrieve the next page of results in the list.
37 NextPageToken string `json:"nextPageToken"`
38 // ResultSizeEstimage is the estimated total number of results.
39 ResultSizeEstimage uint64 `json:"resultSizeEstimate"`
40}
41
42// message holds a partial response for a Users.messages.
43// The full definition of a message is defined in
44// https://developers.google.com/gmail/api/v1/reference/users/messages#resource
45type message struct {
46 // Id is the immutable ID of the message.
47 Id string `json:"id"`
48 // ThreadId is the ID of the thread the message belongs to.
49 ThreadId string `json:"threadId"`
50 // HistoryId is the ID of the last history record that modified
51 // this message.
52 HistoryId string `json:"historyId"`
53 // Snippet is a short part of the message text. This text is
54 // used for the push message summary.
55 Snippet string `json:"snippet"`
56 // Payload represents the message payload.
57 Payload payload `json:"payload"`
58}
59
60func (m message) String() string {
61 return fmt.Sprintf("Id: %d, snippet: '%s'\n", m.Id, m.Snippet[:10])
62}
63
64// ById implements sort.Interface for []message based on
65// the Id field.
66type byId []message
67
68func (m byId) Len() int { return len(m) }
69func (m byId) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
70func (m byId) Less(i, j int) bool { return m[i].Id < m[j].Id }
71
72// payload represents the message payload.
73type payload struct {
74 Headers []messageHeader `json:"headers"`
75}
76
77func (p *payload) mapHeaders() headers {
78 headers := make(map[string]string)
79 for _, hdr := range p.Headers {
80 headers[hdr.Name] = hdr.Value
81 }
82 return headers
83}
84
85func (hdr headers) getTimestamp() time.Time {
86 timestamp, ok := hdr[hdrDATE]
87 if !ok {
88 return time.Now()
89 }
90
91 if t, err := time.Parse(gmailTime, timestamp); err == nil {
92 return t
93 }
94 return time.Now()
95}
96
97func (hdr headers) getEpoch() int64 {
98 return hdr.getTimestamp().Unix()
99}
100
101// messageHeader represents the message headers.
102type messageHeader struct {
103 Name string `json:"name"`
104 Value string `json:"value"`
105}
106
107type errorResp struct {
108 Err struct {
109 Code uint64 `json:"code"`
110 Message string `json:"message"`
111 Errors []struct {
112 Domain string `json:"domain"`
113 Reason string `json:"reason"`
114 Message string `json:"message"`
115 } `json:"errors"`
116 } `json:"error"`
117}
118
119func (err *errorResp) Error() string {
120 return fmt.Sprint("backend response:", err.Err.Message)
121}
122
123const (
124 hdrDATE = "Date"
125 hdrSUBJECT = "Subject"
126 hdrFROM = "From"
127)
1280
=== removed file 'plugins/gmail/gmail.go'
--- plugins/gmail/gmail.go 2016-04-20 11:10:17 +0000
+++ plugins/gmail/gmail.go 1970-01-01 00:00:00 +0000
@@ -1,346 +0,0 @@
1/*
2 Copyright 2014 Canonical Ltd.
3
4 This program is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License version 3, as published
6 by the Free Software Foundation.
7
8 This program is distributed in the hope that it will be useful, but
9 WITHOUT ANY WARRANTY; without even the implied warranties of
10 MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11 PURPOSE. See the GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License along
14 with this program. If not, see <http://www.gnu.org/licenses/>.
15*/
16
17package gmail
18
19import (
20 "encoding/json"
21 "fmt"
22 "net/http"
23 "net/mail"
24 "net/url"
25 "os"
26 "regexp"
27 "sort"
28 "strings"
29 "time"
30
31 "log"
32
33 "launchpad.net/account-polld/accounts"
34 "launchpad.net/account-polld/gettext"
35 "launchpad.net/account-polld/plugins"
36 "launchpad.net/account-polld/qtcontact"
37)
38
39const (
40 APP_ID = "com.ubuntu.developer.webapps.webapp-gmail_webapp-gmail"
41 gmailDispatchUrl = "https://mail.google.com/mail/mu/mp/#cv/priority/^smartlabel_%s/%s"
42 // If there's more than 10 emails in one batch, we don't show 10 notification
43 // bubbles, but instead show one summary. We always show all notifications in the
44 // indicator.
45 individualNotificationsLimit = 10
46 pluginName = "gmail"
47)
48
49type reportedIdMap map[string]time.Time
50
51var baseUrl, _ = url.Parse("https://www.googleapis.com/gmail/v1/users/me/")
52
53// timeDelta defines how old messages can be to be reported.
54var timeDelta = time.Duration(time.Hour * 24)
55
56// trackDelta defines how old messages can be before removed from tracking
57var trackDelta = time.Duration(time.Hour * 24 * 7)
58
59// relativeTimeDelta is the same as timeDelta
60var relativeTimeDelta string = "1d"
61
62// regexp for identifying non-ascii characters
63var nonAsciiChars, _ = regexp.Compile("[^\x00-\x7F]")
64
65type GmailPlugin struct {
66 // reportedIds holds the messages that have already been notified. This
67 // approach is taken against timestamps as it avoids needing to call
68 // get on the message.
69 reportedIds reportedIdMap
70 accountId uint
71}
72
73func idsFromPersist(accountId uint) (ids reportedIdMap, err error) {
74 err = plugins.FromPersist(pluginName, accountId, &ids)
75 if err != nil {
76 return nil, err
77 }
78 // discard old ids
79 timestamp := time.Now()
80 for k, v := range ids {
81 delta := timestamp.Sub(v)
82 if delta > trackDelta {
83 log.Print("gmail plugin ", accountId, ": deleting ", k, " as ", delta, " is greater than ", trackDelta)
84 delete(ids, k)
85 }
86 }
87 return ids, nil
88}
89
90func (ids reportedIdMap) persist(accountId uint) (err error) {
91 err = plugins.Persist(pluginName, accountId, ids)
92 if err != nil {
93 log.Print("gmail plugin ", accountId, ": failed to save state: ", err)
94 }
95 return nil
96}
97
98func New(accountId uint) *GmailPlugin {
99 reportedIds, err := idsFromPersist(accountId)
100 if err != nil {
101 log.Print("gmail plugin ", accountId, ": cannot load previous state from storage: ", err)
102 } else {
103 log.Print("gmail plugin ", accountId, ": last state loaded from storage")
104 }
105 return &GmailPlugin{reportedIds: reportedIds, accountId: accountId}
106}
107
108func (p *GmailPlugin) ApplicationId() plugins.ApplicationId {
109 return plugins.ApplicationId(APP_ID)
110}
111
112func (p *GmailPlugin) Poll(authData *accounts.AuthData) ([]*plugins.PushMessageBatch, error) {
113 // This envvar check is to ease testing.
114 if token := os.Getenv("ACCOUNT_POLLD_TOKEN_GMAIL"); token != "" {
115 authData.AccessToken = token
116 }
117
118 resp, err := p.requestMessageList(authData.AccessToken)
119 if err != nil {
120 return nil, err
121 }
122 messages, err := p.parseMessageListResponse(resp)
123 if err != nil {
124 return nil, err
125 }
126
127 // TODO use the batching API defined in https://developers.google.com/gmail/api/guides/batch
128 for i := range messages {
129 resp, err := p.requestMessage(messages[i].Id, authData.AccessToken)
130 if err != nil {
131 return nil, err
132 }
133 messages[i], err = p.parseMessageResponse(resp)
134 if err != nil {
135 return nil, err
136 }
137 }
138 notif, err := p.createNotifications(messages)
139 if err != nil {
140 return nil, err
141 }
142 return []*plugins.PushMessageBatch{
143 &plugins.PushMessageBatch{
144 Messages: notif,
145 Limit: individualNotificationsLimit,
146 OverflowHandler: p.handleOverflow,
147 Tag: "gmail",
148 }}, nil
149
150}
151
152func (p *GmailPlugin) reported(id string) bool {
153 _, ok := p.reportedIds[id]
154 return ok
155}
156
157func (p *GmailPlugin) createNotifications(messages []message) ([]*plugins.PushMessage, error) {
158 timestamp := time.Now()
159 pushMsgMap := make(pushes)
160
161 for _, msg := range messages {
162 hdr := msg.Payload.mapHeaders()
163
164 from := hdr[hdrFROM]
165 var avatarPath string
166
167 emailAddress, err := mail.ParseAddress(from)
168 if err != nil {
169 // If the email address contains non-ascii characters, we get an
170 // error so we're going to try again, this time mangling the name
171 // by removing all non-ascii characters. We only care about the email
172 // address here anyway.
173 // XXX: We can't check the error message due to [1]: the error
174 // message is different in go < 1.3 and > 1.5.
175 // [1] https://github.com/golang/go/issues/12492
176 mangledAddr := nonAsciiChars.ReplaceAllString(from, "")
177 mangledEmail, mangledParseError := mail.ParseAddress(mangledAddr)
178 if mangledParseError == nil {
179 emailAddress = mangledEmail
180 }
181 } else if emailAddress.Name != "" {
182 // We only want the Name if the first ParseAddress
183 // call was successful. I.e. we do not want the name
184 // from a mangled email address.
185 from = emailAddress.Name
186 }
187
188 if emailAddress != nil {
189 avatarPath = qtcontact.GetAvatar(emailAddress.Address)
190 // If icon path starts with a path separator, assume local file path,
191 // encode it and prepend file scheme defined in RFC 1738.
192 if strings.HasPrefix(avatarPath, string(os.PathSeparator)) {
193 avatarPath = url.QueryEscape(avatarPath)
194 avatarPath = "file://" + avatarPath
195 }
196 }
197
198 msgStamp := hdr.getTimestamp()
199
200 if _, ok := pushMsgMap[msg.ThreadId]; ok {
201 // TRANSLATORS: the %s is an appended "from" corresponding to an specific email thread
202 pushMsgMap[msg.ThreadId].Notification.Card.Summary += fmt.Sprintf(gettext.Gettext(", %s"), from)
203 } else if timestamp.Sub(msgStamp) < timeDelta {
204 // TRANSLATORS: the %s is the "from" header corresponding to a specific email
205 summary := fmt.Sprintf(gettext.Gettext("%s"), from)
206 // TRANSLATORS: the first %s refers to the email "subject", the second %s refers "from"
207 body := fmt.Sprintf(gettext.Gettext("%s\n%s"), hdr[hdrSUBJECT], msg.Snippet)
208 // fmt with label personal and threadId
209 action := fmt.Sprintf(gmailDispatchUrl, "personal", msg.ThreadId)
210 epoch := hdr.getEpoch()
211 pushMsgMap[msg.ThreadId] = plugins.NewStandardPushMessage(summary, body, action, avatarPath, epoch)
212 } else {
213 log.Print("gmail plugin ", p.accountId, ": skipping message id ", msg.Id, " with date ", msgStamp, " older than ", timeDelta)
214 }
215 }
216 pushMsg := make([]*plugins.PushMessage, 0, len(pushMsgMap))
217 for _, v := range pushMsgMap {
218 pushMsg = append(pushMsg, v)
219 }
220 return pushMsg, nil
221
222}
223func (p *GmailPlugin) handleOverflow(pushMsg []*plugins.PushMessage) *plugins.PushMessage {
224 // TODO it would probably be better to grab the estimate that google returns in the message list.
225 approxUnreadMessages := len(pushMsg)
226
227 // TRANSLATORS: the %d refers to the number of new email messages.
228 summary := fmt.Sprintf(gettext.Gettext("You have %d new messages"), approxUnreadMessages)
229
230 body := ""
231
232 // fmt with label personal and no threadId
233 action := fmt.Sprintf(gmailDispatchUrl, "personal")
234 epoch := time.Now().Unix()
235
236 return plugins.NewStandardPushMessage(summary, body, action, "", epoch)
237}
238
239func (p *GmailPlugin) parseMessageListResponse(resp *http.Response) ([]message, error) {
240 defer resp.Body.Close()
241 decoder := json.NewDecoder(resp.Body)
242
243 if resp.StatusCode != http.StatusOK {
244 var errResp errorResp
245 if err := decoder.Decode(&errResp); err != nil {
246 return nil, err
247 }
248 if errResp.Err.Code == 401 {
249 return nil, plugins.ErrTokenExpired
250 }
251 return nil, &errResp
252 }
253
254 var messages messageList
255 if err := decoder.Decode(&messages); err != nil {
256 return nil, err
257 }
258
259 filteredMsg := p.messageListFilter(messages.Messages)
260
261 return filteredMsg, nil
262}
263
264// messageListFilter returns a subset of unread messages where the subset
265// depends on not being in reportedIds. Before returning, reportedIds is
266// updated with the new list of unread messages.
267func (p *GmailPlugin) messageListFilter(messages []message) []message {
268 sort.Sort(byId(messages))
269 var reportMsg []message
270 var ids = make(reportedIdMap)
271
272 for _, msg := range messages {
273 if !p.reported(msg.Id) {
274 reportMsg = append(reportMsg, msg)
275 }
276 ids[msg.Id] = time.Now()
277 }
278 p.reportedIds = ids
279 p.reportedIds.persist(p.accountId)
280 return reportMsg
281}
282
283func (p *GmailPlugin) parseMessageResponse(resp *http.Response) (message, error) {
284 defer resp.Body.Close()
285 decoder := json.NewDecoder(resp.Body)
286
287 if resp.StatusCode != http.StatusOK {
288 var errResp errorResp
289 if err := decoder.Decode(&errResp); err != nil {
290 return message{}, err
291 }
292 return message{}, &errResp
293 }
294
295 var msg message
296 if err := decoder.Decode(&msg); err != nil {
297 return message{}, err
298 }
299
300 return msg, nil
301}
302
303func (p *GmailPlugin) requestMessage(id, accessToken string) (*http.Response, error) {
304 u, err := baseUrl.Parse("messages/" + id)
305 if err != nil {
306 return nil, err
307 }
308
309 query := u.Query()
310 // only request specific fields
311 query.Add("fields", "snippet,threadId,id,payload/headers")
312 // get the full message to get From and Subject from headers
313 query.Add("format", "full")
314 u.RawQuery = query.Encode()
315
316 req, err := http.NewRequest("GET", u.String(), nil)
317 if err != nil {
318 return nil, err
319 }
320 req.Header.Set("Authorization", "Bearer "+accessToken)
321
322 return http.DefaultClient.Do(req)
323}
324
325func (p *GmailPlugin) requestMessageList(accessToken string) (*http.Response, error) {
326 u, err := baseUrl.Parse("messages")
327 if err != nil {
328 return nil, err
329 }
330
331 query := u.Query()
332
333 // get all unread inbox emails received after
334 // the last time we checked. If this is the first
335 // time we check, get unread emails after timeDelta
336 query.Add("q", fmt.Sprintf("is:unread in:inbox newer_than:%s", relativeTimeDelta))
337 u.RawQuery = query.Encode()
338
339 req, err := http.NewRequest("GET", u.String(), nil)
340 if err != nil {
341 return nil, err
342 }
343 req.Header.Set("Authorization", "Bearer "+accessToken)
344
345 return http.DefaultClient.Do(req)
346}
3470
=== removed file 'plugins/plugins.go'
--- plugins/plugins.go 2016-02-08 19:21:31 +0000
+++ plugins/plugins.go 1970-01-01 00:00:00 +0000
@@ -1,239 +0,0 @@
1/*
2 Copyright 2014 Canonical Ltd.
3
4 This program is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License version 3, as published
6 by the Free Software Foundation.
7
8 This program is distributed in the hope that it will be useful, but
9 WITHOUT ANY WARRANTY; without even the implied warranties of
10 MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11 PURPOSE. See the GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License along
14 with this program. If not, see <http://www.gnu.org/licenses/>.
15*/
16
17package plugins
18
19import (
20 "bufio"
21 "encoding/json"
22 "errors"
23 "fmt"
24 "os"
25 "path/filepath"
26 "reflect"
27
28 "launchpad.net/account-polld/accounts"
29 "launchpad.net/go-xdg/v0"
30)
31
32func init() {
33 cmdName = filepath.Base(os.Args[0])
34}
35
36// Plugin is an interface which the plugins will adhere to for the poll
37// daemon to interact with.
38//
39// Poll interacts with the backend service with the means the plugin defines
40// and returns a list of Notifications to send to the Push service. If an
41// error occurs and is returned the daemon can decide to throttle the service.
42//
43// ApplicationId returns the APP_ID of the delivery target for Post Office.
44type Plugin interface {
45 ApplicationId() ApplicationId
46 Poll(*accounts.AuthData) ([]*PushMessageBatch, error)
47}
48
49// AuthTokens is a map with tokens the plugins are to use to make requests.
50type AuthTokens map[string]interface{}
51
52// ApplicationId represents the application id to direct posts to.
53// e.g.: com.ubuntu.diaspora_diaspora or com.ubuntu.diaspora_diaspora_1.0
54type ApplicationId string
55
56// NewStandardPushMessage creates a base Notification with common
57// components (members) setup.
58func NewStandardPushMessage(summary, body, action, icon string, epoch int64) *PushMessage {
59 pm := &PushMessage{
60 Notification: Notification{
61 Card: &Card{
62 Summary: summary,
63 Body: body,
64 Actions: []string{action},
65 Icon: icon,
66 Timestamp: epoch,
67 Popup: true,
68 Persist: true,
69 },
70 Sound: DefaultSound(),
71 Vibrate: true,
72 Tag: cmdName,
73 },
74 }
75 return pm
76}
77
78// PushMessageBatch represents a logical grouping of PushMessages that
79// have a limit on the number of their notifications that want to be
80// presented to the user at the same time, and a way to handle the
81// overflow. All Notifications that are part of a Batch share the same
82// tag (Tag). ${Tag}-overflow is the overflow notification tag.
83//
84// TODO: support notifications sharing just the prefix (so the app can
85// tell them apart by tag).
86type PushMessageBatch struct {
87 Messages []*PushMessage
88 Limit int
89 OverflowHandler func([]*PushMessage) *PushMessage
90 Tag string
91}
92
93// PushMessage represents a data structure to be sent over to the
94// Post Office. It consists of a Notification and a Message.
95type PushMessage struct {
96 // Message represents a JSON object that is passed as-is to the
97 // application
98 Message string `json:"message,omitempty"`
99 // Notification (optional) describes the user-facing notifications
100 // triggered by this push message.
101 Notification Notification `json:"notification,omitempty"`
102}
103
104// Notification (optional) describes the user-facing notifications
105// triggered by this push message.
106type Notification struct {
107 // Sound (optional) is the path to a sound file which can or
108 // cannot be played depending on user preferences.
109 Sound string `json:"sound,omitempty"`
110 // Card represents a specific bubble to give to the user
111 Card *Card `json:"card,omitempty"`
112 // Vibrate is the haptic feedback part of a notification.
113 Vibrate bool `json:"vibrate,omitempty"`
114 // EmblemCounter represents and application counter hint
115 // related to the notification.
116 EmblemCounter *EmblemCounter `json:"emblem-counter,omitempty"`
117 // Tag represents a tag to identify persistent notifications
118 Tag string `json:"tag,omitempty"`
119}
120
121// Card is part of a notification and represents the user visible hints for
122// a specific notification.
123type Card struct {
124 // Summary is a required title. The card will not be presented if this is missing.
125 Summary string `json:"summary"`
126 // Body is the longer text.
127 Body string `json:"body,omitempty"`
128 // Whether to show a bubble. Users can disable this, and can easily miss
129 // them, so don’t rely on it exclusively.
130 Popup bool `json:"popup,omitempty"`
131 // Actions provides actions for the bubble's snap decissions.
132 Actions []string `json:"actions,omitempty"`
133 // Icon is a path to an icon to display with the notification bubble.
134 Icon string `json:"icon,omitempty"`
135 // Whether to show in notification centre.
136 Persist bool `json:"persist,omitempty"`
137 // Seconds since the unix epoch, useful for persistent cards.
138 Timestamp int64 `json:"Timestamp,omitempty"`
139}
140
141// EmblemCounter is part of a notification and represents the application visual
142// hints related to a notification.
143type EmblemCounter struct {
144 // Count is a number to be displayed over the application’s icon in the
145 // launcher.
146 Count uint `json:"count"`
147 // Visible determines if the counter is visible or not.
148 Visible bool `json:"visible"`
149}
150
151// The constanst defined here determine the polling aggressivenes with the following criteria
152// MAXIMUM: calls, health warning
153// HIGH: SMS, chat message, new email
154// DEFAULT: social media updates
155// LOW: software updates, junk email
156const (
157 PRIORITY_MAXIMUM = 0
158 PRIORITY_HIGH
159 PRIORITY_DEFAULT
160 PRIORITY_LOW
161)
162
163const (
164 PLUGIN_EMAIL = 0
165 PLUGIN_SOCIAL
166)
167
168// ErrTokenExpired is the error returned by a plugin to indicate that
169// the web service reported that the authentication token has expired.
170var ErrTokenExpired = errors.New("Token expired")
171
172var cmdName string
173
174var XdgDataFind = xdg.Data.Find
175var XdgDataEnsure = xdg.Data.Ensure
176
177// Persist stores the plugins data in a common location to a json file
178// from which it can recover later
179func Persist(pluginName string, accountId uint, data interface{}) (err error) {
180 var p string
181 defer func() {
182 if err != nil && p != "" {
183 os.Remove(p)
184 }
185 }()
186 p, err = XdgDataEnsure(filepath.Join(cmdName, fmt.Sprintf("%s-%d.json", pluginName, accountId)))
187 if err != nil {
188 return err
189 }
190 file, err := os.Create(p)
191 if err != nil {
192 return err
193 }
194 defer file.Close()
195 w := bufio.NewWriter(file)
196 defer w.Flush()
197 jsonWriter := json.NewEncoder(w)
198 if err := jsonWriter.Encode(data); err != nil {
199 return err
200 }
201 return nil
202}
203
204// FromPersist restores the plugins data from a common location which
205// was stored in a json file
206func FromPersist(pluginName string, accountId uint, data interface{}) (err error) {
207 if reflect.ValueOf(data).Kind() != reflect.Ptr {
208 return errors.New("decode target is not a pointer")
209 }
210 var p string
211 defer func() {
212 if err != nil {
213 if p != "" {
214 os.Remove(p)
215 }
216 }
217 }()
218 p, err = XdgDataFind(filepath.Join(cmdName, fmt.Sprintf("%s-%d.json", pluginName, accountId)))
219 if err != nil {
220 return err
221 }
222 file, err := os.Open(p)
223 if err != nil {
224 return err
225 }
226 defer file.Close()
227 jsonReader := json.NewDecoder(file)
228 if err := jsonReader.Decode(&data); err != nil {
229 return err
230 }
231
232 return nil
233}
234
235// DefaultSound returns the path to the default sound for a Notification
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches