-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmain.py
230 lines (188 loc) · 7.23 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
from heedy import Plugin
from aiohttp import web, BasicAuth, ClientSession
import logging
import urllib.parse
import json
from datetime import datetime, date, time, timedelta
from syncer import Syncer
import asyncio
logging.basicConfig(level=logging.DEBUG)
p = Plugin()
routes = web.RouteTableDef()
server_url = p.config["config"]["url"]
addr = p.config["config"]["addr"]
if addr.startswith(":"):
addr = "localhost" + addr
port = p.config["config"]["addr"].split(":")[1]
l = logging.getLogger("fitbit")
def redirector(x):
if server_url.startswith("https://"):
return f"{server_url}/api/fitbit/{x}/auth"
return f"http://{addr}/api/fitbit/{x}/auth"
# Initialize the client session on server start, because otherwise aiohttp complains
s = None
async def getApp(request):
h = request.headers
if h["X-Heedy-As"] == "public" or "/" in h["X-Heedy-As"]:
raise web.HTTPForbidden(text="You do not have access to this resource")
appid = request.match_info["app"]
try:
a = await p.apps[appid]
appvals = await a.read()
if appvals["plugin"] != "fitbit:fitbit":
l.error(f"The app {appid} is not managed by fitbit")
raise "fail"
if appvals["owner"] != h["X-Heedy-As"] and h["X-Heedy-As"] != "heedy":
l.error(f"Only the owner of {appid} can run fitbit commands on it")
raise "fail"
return appid, a
except:
raise web.HTTPForbidden(text="You do not have access to this resource")
async def notify_register(app):
l.debug(f"Creating setup notification for {app}")
redir = redirector(app)
msg = f"To give heedy access to your data, you need to register an application with fitbit. The application must be of 'personal' type, and must use the following callback URL: `{redir}`"
msg += "\n\nAfter registering an application with fitbit, copy its details into your fitbit app's settings."
await p.notify(
"setup",
"Link your fitbit account to heedy",
app=app,
_global=True,
description=msg,
actions=[
{
"title": "Register App",
"href": "https://dev.fitbit.com/apps/new",
"new_window": True,
},
{
"title": "Settings",
"icon": "fas fa-cog",
"href": f"#/apps/{app}/settings",
},
],
dismissible=False,
)
@routes.post("/app_create")
async def app_create(request):
evt = await request.json()
l.debug(f"App created: {evt}")
# Try creating the notification 10 times, in case the database is still committing the app
for i in range(10):
try:
await notify_register(evt["app"])
return web.Response(text="ok")
except:
l.warning("Failed to notify on app create")
await asyncio.sleep(0.1)
return web.Response(text="ok")
@routes.post("/app_settings_update")
async def app_settings_update(request):
evt = await request.json()
l.debug(f"Settings updated: {evt}")
a = await p.apps[evt["app"]]
# Read the app, getting the necessary settings
# await a.notify("setup", "Setting up...", description="", actions=[])
settings = await a.settings
desc = "Now that heedy has fitbit app credentials, you need to give it access to your data."
if redirector(evt["app"]).startswith("http://localhost"):
desc += f"\n\n**NOTE:** Due to http restrictions on fitbit's servers, you must press the Authorize button from the computer running heedy (i.e. heedy must be accessible at `http://localhost:{port}`). If running heedy on a remote server, you will need to forward port `{port}` to `localhost:{port}` for authorization to succeed."
await a.notify(
"setup",
"Authorize Access",
description=desc,
actions=[
{
"title": "Authorize",
"href": settings["authorization_uri"]
+ "?"
+ urllib.parse.urlencode(
{
"response_type": "code",
"client_id": settings["client_id"],
"redirect_uri": redirector(evt["app"]),
"scope": "activity heartrate location nutrition profile settings sleep social weight",
}
),
}
],
)
return web.Response(text="ok")
@routes.get("/api/fitbit/{app}/auth")
async def auth_callback(request):
appid, a = await getApp(request)
code = request.rel_url.query["code"]
settings = await a.settings
response = await s.post(
settings["refresh_uri"],
data={
"grant_type": "authorization_code",
"code": code,
"redirect_uri": redirector(appid),
},
auth=BasicAuth(settings["client_id"], settings["client_secret"]),
)
resjson = await response.json()
await a.kv.set(**resjson)
l.info(f"Successfully authenticated {appid}")
await a.notifications.delete("setup")
await a.notify(
"sync",
"Synchronizing...",
description="Heedy is syncing your historical fitbit data. Due to fitbit's download limits, this might take several days. This message will disappear once synchronization is complete.",
actions=[],
)
await Syncer.sync(s, a, appid)
# redirect the user back to heedy
if server_url.startswith("https://"):
raise web.HTTPFound(location=f"{server_url}/#/apps/{appid}")
# Makes it work also when ssh port forwarding the server
raise web.HTTPFound(location=f"http://{addr}/#/apps/{appid}")
@routes.get("/api/fitbit/{app}/sync")
async def sync(request):
appid, a = await getApp(request)
l.debug(f"Sync requested for {appid}")
await a.notify(
"sync",
"Synchronizing...",
description="Heedy is syncing your fitbit data. This might take a while...",
actions=[],
)
await Syncer.sync(s, a, appid)
return web.Response(text="ok")
async def run_sync():
l.debug("Starting sync of all fitbit accounts...")
applist = await p.apps(plugin="fitbit:fitbit")
for a in applist:
appid = a["id"]
# Make sure the app has an access token ready
if (await a.kv["access_token"]) is None:
# If not, notify to create one
await notify_register(appid)
else:
await Syncer.sync(s, a, appid)
async def syncloop():
l.debug("Waiting 10 seconds before syncing")
await asyncio.sleep(10)
while True:
try:
await run_sync()
except Exception as e:
l.error(e)
wait_until = p.config["config"]["plugin"]["fitbit"]["config"]["sync_every"]
l.debug(f"Waiting {wait_until} seconds until next auto-sync initiated")
await asyncio.sleep(wait_until)
async def startup(app):
global s
s = ClientSession()
asyncio.create_task(syncloop())
async def cleanup(app):
await s.close()
await p.session.close()
app = web.Application()
app.add_routes(routes)
app.on_startup.append(startup)
app.on_cleanup.append(cleanup)
# Runs the server over a unix domain socket. The socket is automatically placed in the data folder,
# and not the plugin folder.
web.run_app(app, path=f"{p.name}.sock")