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