-
Notifications
You must be signed in to change notification settings - Fork 4
/
impfWidget.js
377 lines (316 loc) · 12.6 KB
/
impfWidget.js
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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
/*
Impftermin Widget
v 1.4.1 Workaround durch JavaScript eval innerhalb eines WebViews (Thanks to @Redna)
This Scriptable Widget will show you if there are any "Vermittlungscode" for vaccination appointments available.
The data is pulled from the impfterminservice.de api, which is neither publicly available nor documented.
Therefore everything may break.
The newest version, issues, etc. of this widget can be found here: https://github.com/not-a-feature/impfWidget
The framework/skeleton of this script was created by marco79cgn for the toiletpaper-widget
(https://gist.github.com/marco79cgn/23ce08fd8711ee893a3be12d4543f2d2)
To uses this widget go to https://003-iz.impfterminservice.de/assets/static/impfzentren.json and search for
your local center. Copy the whole text in between the two curly brackets and paste it below in the settings (Starting at line 55).
If you want a notification change the NOTIFICATION_LEVEL to
0: no notification
1: only if vaccines are available
2: every time the widget refreshes
If you want to know if there are appointments specifically for a vaccine,
set DISPLAY_VACCINES_AS_ONE to false. This requires a medium size-widget (2x1)
If you want to exclude specific vaccines, set them to inside the VACCINES variable to false.
Thats it. You can now run this script.
Copy the source, open the scriptabel app, add the source there.
go the home screen, add scriptable widget
Aknowlodgements:
- @Redna, for providing a workaround to bypass the botprotection.
-------------------------------------------------------------------------------
LICENSE:
Copyright (C) 2021 by Jules Kreuer - @not_a_feature
This piece of software is published unter the GNU General Public License v3.0
TLDR:
| Permissions | Conditions | Limitations |
| ---------------- | ---------------------------- | ----------- |
| ✓ Commercial use | Disclose source | ✕ Liability |
| ✓ Distribution | License and copyright notice | ✕ Warranty |
| ✓ Modification | Same license | |
| ✓ Patent use | State changes | |
| ✓ Private use | | |
Go to https://github.com/not-a-feature/impfWidget/blob/main/LICENSE to see the full version.
-------------------------------------------------------------------------------
*/
//-----------------------------------------------------------------------------
// Settings
// Replace this with the data of you local center
const CENTER = {
"Zentrumsname": "Paul Horn Arena",
"PLZ": "72072",
"Ort": "Tübingen",
"Bundesland": "Baden-Württemberg",
"URL": "https://003-iz.impfterminservice.de/",
"Adresse": "Europastraße 50"
};
// adjust to your desired level
const NOTIFICATION_LEVEL = 1;
// Set to false, if a detailed view is wanted.
// Attention! This requires a medium size-widget (2x1)
const DISPLAY_VACCINES_AS_ONE = true;
// Advanced Setting
// Fetch status of following vaccines, set to false to ignore this vaccine
const VACCINES = [{"name": "BioNTech", "ID": "L920", "allowed": true},
{"name": "mRNA", "ID": "L921", "allowed": true},
{"name": "AstraZeneca", "ID": "L922", "allowed": true},
{"name": "J&J", "ID": "L923", "allowed": true}];
// END Setting
//-----------------------------------------------------------------------------
const vaccineTextFontSize = 13;
const appointmentsTextFontSize = 22;
const detailTextFontSize = 17;
const textColorRed = new Color("#E50000");
const textColorGreen = new Color("#00CD66");
const widget = new ListWidget();
widget.url = CENTER["URL"] + "impftermine/service?plz=" + CENTER["PLZ"];
const openAppointments = await fetchOpenAppointments();
await createNotification();
await createWidget();
if (!config.runsInWidget) {
if (DISPLAY_VACCINES_AS_ONE) {
await widget.presentSmall();
}
else {
await widget.presentMedium();
}
}
Script.setWidget(widget);
Script.complete();
/* create Widget
case: smallWidget (DISPLAY_VACCINES_AS_ONE == true)
topRow: | leftColumn | rightColumn |
| | IMPFUNGEN |
| icon | Keine/Termine |
bottomRow: | Location |
case: mediumWidget (DISPLAY_VACCINES_AS_ONE == false)
topRow: | leftColumn | rightColumn | detailColumn |
| | IMPFUNGEN | BioNTech |
| icon | Keine/Termine | Moderna... |
bottomRow: | Location |
*/
/*
Create widget using current information
*/
async function createWidget() {
widget.setPadding(10, 10, 10, 10);
const icon = await getImage('vaccine');
let topRow = widget.addStack();
topRow.layoutHorizontally();
let leftColumn = topRow.addStack();
leftColumn.layoutVertically();
leftColumn.addSpacer(vaccineTextFontSize);
const iconImg = leftColumn.addImage(icon);
iconImg.imageSize = new Size(40, 40);
topRow.addSpacer(vaccineTextFontSize);
let rightColumn = topRow.addStack();
rightColumn.layoutVertically();
const vaccineText = rightColumn.addText("IMPFUNGEN");
vaccineText.font = Font.mediumRoundedSystemFont(vaccineTextFontSize);
let openAppointmentsText;
let textColor = textColorRed;
if (openAppointments.hasOwnProperty("error")) {
if (Object.keys(openAppointments.error).length == 0) {
openAppointmentsText = "⚠️ Keine Antwort " + openAppointments["error"];
} else {
openAppointmentsText = "⚠️ " + openAppointments["error"];
}
}
else if (Object.values(openAppointments).includes(true)) {
openAppointmentsText = "Freie";
textColor = textColorGreen;
}
else {
openAppointmentsText = "Keine";
}
let openAppointmentsTextObj = rightColumn.addText(openAppointmentsText);
let generalAppointmentsTextObj = rightColumn.addText("Termine");
openAppointmentsTextObj.font = Font.mediumRoundedSystemFont(appointmentsTextFontSize);
openAppointmentsTextObj.textColor = textColor;
generalAppointmentsTextObj.font = Font.mediumRoundedSystemFont(appointmentsTextFontSize);
generalAppointmentsTextObj.textColor = textColor;
if(!DISPLAY_VACCINES_AS_ONE) {
topRow.addSpacer(8);
let detailColumn = topRow.addStack()
detailColumn.layoutVertically();
openAppointmentsDetail = {}
Object.keys(openAppointments).forEach((key, index) => {
openAppointmentsDetail[key] = detailColumn.addText(key);
openAppointmentsDetail[key].font = Font.mediumRoundedSystemFont(detailTextFontSize);
if (openAppointments[key]) {
openAppointmentsDetail[key].textColor = textColorGreen;
}
else {
openAppointmentsDetail[key].textColor = textColorRed;
}
})
}
widget.addSpacer(4);
const bottomRow = widget.addStack();
bottomRow.layoutVertically();
// Replacing long names with their abbrehivations
let shortName = CENTER["Zentrumsname"];
shortName = shortName.replace("Zentrales Impfzentrum (ZIZ)", "ZIZ");
shortName = shortName.replace("Zentrales Impfzentrum", "ZIZ");
shortName = shortName.replace("Impfzentrum Landkreis", "KIZ");
shortName = shortName.replace("Landkreis", "LK");
shortName = shortName.replace("Kreisimpfzentrum", "KIZ");
shortName = shortName.replace("Impfzentrum Kreis", "KIZ");
const street = bottomRow.addText(shortName);
street.font = Font.regularSystemFont(11);
const zipCity = bottomRow.addText(CENTER["Adresse"] + ", " + CENTER["Ort"]);
zipCity.font = Font.regularSystemFont(11);
}
/*
Create notification if turned on
*/
async function createNotification() {
if (NOTIFICATION_LEVEL > 0) {
const notify = new Notification();
notify.sound = "default";
notify.title = "ImpfWidget";
notify.openURL = CENTER["URL"];
if (Object.values(openAppointments).includes(true)) {
notify.body = "💉 Freie Termine - " + CENTER["Ort"];
notify.schedule();
return;
}
else if (openAppointments.hasOwnProperty("error") && NOTIFICATION_LEVEL == 2) {
notify.body = "⚠️ Keine Antwort " + openAppointments["error"];
notify.schedule();
return;
}
else if (NOTIFICATION_LEVEL == 2) {
notify.body = "🦠 Keine Termine";
notify.schedule();
return;
}
}
}
/*
Fetches open appointments
Returns object e.g:
{"BioNTech": true, "Monderna": false}
or {"Error": "Error message"}
*/
async function fetchOpenAppointments() {
let landingUrl = CENTER["URL"] + "/impftermine/service?plz=" + CENTER["PLZ"];
let url = CENTER["URL"] + "rest/suche/termincheck?plz=" + CENTER["PLZ"] + "&leistungsmerkmale=";
let result = {};
console.log(VACCINES);
// Case if all vaccines are displayed as one
if (DISPLAY_VACCINES_AS_ONE) {
let urlAppendix = [];
for (var i = 0; i < VACCINES.length; i++) {
if (VACCINES[i]["allowed"]) {
urlAppendix.push(VACCINES[i]["ID"]);
}
}
if (urlAppendix == []) {
return {"error": "No vaccines selected."};
}
url = url + urlAppendix.join(",")
let body = await webViewRequest(landingUrl, url);
console.log(body);
if (Object.keys(body).length === 0) {
await debugNotify("Empty Body");
body = await webViewRequest(landingUrl, url);
}
for (var i = 0; i < VACCINES.length; i++) {
if (!body["termineVorhanden"] && !body.error) {
result[VACCINES[i]["name"]] = false;
}
else if (body["termineVorhanden"]) {
result[VACCINES[i]["name"]] = true;
}
else {
return {"error": body.msg};
}
}
}
// Case if all vaccines are displayed one by one
else {
for (var i = 0; i < VACCINES.length; i++) {
if (VACCINES[i]["allowed"]) {
console.log("Checking Vaccine: " + VACCINES[i]["name"]);
let req = new Request(url + VACCINES[i]["ID"]);
let body = await req.loadString();
if (!body["termineVorhanden"] && !body.error) {
result[VACCINES[i]["name"]] = false;
}
else if (body["termineVorhanden"]) {
result[VACCINES[i]["name"]] = true;
}
else {
return {"error": body.msg};
}
}
}
}
return result;
}
// get images from local filestore or download them once
async function getImage(image) {
let fm = FileManager.local();
let dir = fm.documentsDirectory();
let path = fm.joinPath(dir, image);
if (fm.fileExists(path)) {
return fm.readImage(path);
} else {
// download once, save in local storage
let imageUrl;
switch (image) {
case 'vaccine':
imageUrl = "https://api.juleskreuer.eu/syringe-solid.png";
break;
default:
console.log(`Sorry, couldn't find ${image}.`);
}
let iconImage = await loadImage(imageUrl);
fm.writeImage(path, iconImage);
return iconImage;
}
}
// helper function to download an image from a given url
async function loadImage(imgUrl) {
const req = new Request(imgUrl);
return await req.loadImage();
}
async function webViewRequest(landingUrl, requestUrl) {
let evalJS = `
let request = new XMLHttpRequest();
request.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
let jsonResponse = JSON.parse(this.responseText);
completion(jsonResponse)
}
else if (this.readyState == 4 && this.status != 200) {
console.log("Error", this.status);
completion(
{"error": true,
"msg": this.responseText}
);
}
}
request.open("GET", "${requestUrl}");
request.send();
`;
const web = new WebView();
await web.loadURL(landingUrl);
await web.waitForLoad();
const result = await web.evaluateJavaScript(evalJS, true);
await debugNotify("Eval result: " + JSON.stringify(result));
return result;
}
async function debugNotify(message) {
if (NOTIFICATION_LEVEL < 2)
return;
const notify = new Notification();
notify.sound = "default";
notify.title = "ImpfWidget";
notify.body = message;
notify.schedule();
}