iobroker.parcel 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -27
- package/admin/index_m.html +10 -4
- package/io-package.json +3 -2
- package/main.js +123 -113
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -24,33 +24,6 @@ DHL:
|
|
|
24
24
|
* SMS/EMail Code erhalten
|
|
25
25
|
* In die Instanzeinstellungen eingeben und speichern
|
|
26
26
|
|
|
27
|
-
## Amazon Vorbedingungen
|
|
28
|
-
|
|
29
|
-
Es müssen auf Linuxsysteme Pakete installiert werden.
|
|
30
|
-
Nach jeder Variante den Adapter neustarten um zu sehen ob der Login funktioniert.
|
|
31
|
-
|
|
32
|
-
Variante #1 minimal
|
|
33
|
-
|
|
34
|
-
```
|
|
35
|
-
sudo apt-get install -y libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm-dev libxkbcommon-dev libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm-dev libpango-1.0-0
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
Variante #2 falls Variante #1 nicht funktioniert
|
|
39
|
-
|
|
40
|
-
```
|
|
41
|
-
sudo apt-get install -yq \
|
|
42
|
-
gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
|
|
43
|
-
libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \
|
|
44
|
-
libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 \
|
|
45
|
-
libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates \
|
|
46
|
-
fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
Variante #3 **Nur Falls Variante #2 nicht funktioniert**
|
|
50
|
-
|
|
51
|
-
```
|
|
52
|
-
sudo apt-get install -y chromium-browser
|
|
53
|
-
```
|
|
54
27
|
|
|
55
28
|
## Skripte
|
|
56
29
|
|
package/admin/index_m.html
CHANGED
|
@@ -88,19 +88,25 @@
|
|
|
88
88
|
</div>
|
|
89
89
|
</div>
|
|
90
90
|
<div class="row">
|
|
91
|
-
<div class="col">Amazon
|
|
92
|
-
<a href="https://github.com/TA2k/ioBroker.parcel/blob/master/README.md#amazon-vorbedingungen" target="_blank">Linux Zusatzpakete installieren</a>
|
|
91
|
+
<div class="col">Amazon</div>
|
|
93
92
|
</div>
|
|
94
93
|
<div class="row">
|
|
95
94
|
<div class="col s6 input-field">
|
|
96
95
|
<input type="text" class="value" id="amzusername" />
|
|
97
|
-
<label for="amzusername" class="translate">
|
|
96
|
+
<label for="amzusername" class="translate">Amazon Email</label>
|
|
98
97
|
</div>
|
|
99
98
|
</div>
|
|
99
|
+
|
|
100
100
|
<div class="row">
|
|
101
101
|
<div class="col s6 input-field">
|
|
102
102
|
<input type="password" class="value" id="amzpassword" />
|
|
103
|
-
<label for="amzpassword" class="translate">
|
|
103
|
+
<label for="amzpassword" class="translate">Amazon Password</label>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
<div class="row">
|
|
107
|
+
<div class="col s6 input-field">
|
|
108
|
+
<input type="text" class="value" id="amzotp" />
|
|
109
|
+
<label for="amzotp" class="translate">Amazon OTP Token falls 2FA aktiviert ist</label>
|
|
104
110
|
</div>
|
|
105
111
|
</div>
|
|
106
112
|
<div class="row">
|
package/io-package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "parcel",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.3",
|
|
5
5
|
"news": {
|
|
6
|
-
"0.0.
|
|
6
|
+
"0.0.3": {
|
|
7
7
|
"en": "initial release",
|
|
8
8
|
"de": "Erstveröffentlichung",
|
|
9
9
|
"ru": "Начальная версия",
|
|
@@ -99,6 +99,7 @@
|
|
|
99
99
|
"native": {
|
|
100
100
|
"amzusername": "",
|
|
101
101
|
"amzpassword": "",
|
|
102
|
+
"amzotp": "",
|
|
102
103
|
"t17username": "",
|
|
103
104
|
"t17password": "",
|
|
104
105
|
"dpdusername": "",
|
package/main.js
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
// you need to create an adapter
|
|
9
9
|
const utils = require("@iobroker/adapter-core");
|
|
10
10
|
const axios = require("axios");
|
|
11
|
-
const puppeteer = require("puppeteer");
|
|
12
11
|
const qs = require("qs");
|
|
13
12
|
const Json2iob = require("./lib/json2iob");
|
|
14
13
|
const tough = require("tough-cookie");
|
|
@@ -77,89 +76,7 @@ class Parcel extends utils.Adapter {
|
|
|
77
76
|
this.setState("info.connection", true, true);
|
|
78
77
|
}
|
|
79
78
|
if (this.config.amzusername && this.config.amzpassword) {
|
|
80
|
-
this.
|
|
81
|
-
this.browser = await puppeteer
|
|
82
|
-
.launch({
|
|
83
|
-
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
|
84
|
-
})
|
|
85
|
-
.catch((e) => {
|
|
86
|
-
if (e.message && e.message.indexOf("Unterminated quoted string") !== -1) {
|
|
87
|
-
this.log.warn("Puppeteer native browser is not ARM compatible. Try to start local chromium browser installed via sudo apt-get install chromium-browser");
|
|
88
|
-
} else {
|
|
89
|
-
this.log.error(e);
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
//try local instance
|
|
93
|
-
if (!this.browser) {
|
|
94
|
-
this.log.info("Try to start local instance of chromium");
|
|
95
|
-
this.browser = await puppeteer
|
|
96
|
-
.launch({
|
|
97
|
-
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
|
98
|
-
executablePath: "/usr/bin/chromium-browser",
|
|
99
|
-
})
|
|
100
|
-
.catch((e) => {
|
|
101
|
-
this.log.error(e);
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
if (!this.browser) {
|
|
105
|
-
this.log.error("Can't start puppeteer please execute on your ioBroker command line");
|
|
106
|
-
this.log.error(
|
|
107
|
-
"sudo apt-get install -y libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm-dev libxkbcommon-dev libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm-dev libpango-1.0-0"
|
|
108
|
-
);
|
|
109
|
-
this.log.error("More infos: https://github.com/TA2k/ioBroker.parcel/blob/master/README.md#amazon-vorbedingungen");
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
this.page = await this.browser.newPage().catch((e) => this.log.error(e));
|
|
113
|
-
await this.page.goto("https://www.amazon.de/gp/css/order-history?ref_=nav_orders_first").catch((e) => this.log.error(e));
|
|
114
|
-
await this.page
|
|
115
|
-
.evaluate((config) => {
|
|
116
|
-
const email = document.querySelector("#ap_email");
|
|
117
|
-
email.value = config.amzusername;
|
|
118
|
-
const next = document.querySelector(".a-button-input");
|
|
119
|
-
next.click();
|
|
120
|
-
}, this.config)
|
|
121
|
-
.catch((e) => this.log.error(e));
|
|
122
|
-
await this.page.waitForSelector("#ap_password").catch((e) => this.log.error(e));
|
|
123
|
-
await this.page
|
|
124
|
-
.evaluate((config) => {
|
|
125
|
-
const email = document.querySelector("#ap_password");
|
|
126
|
-
email.value = config.amzpassword;
|
|
127
|
-
const remember = document.querySelector("input[name*='rememberMe']");
|
|
128
|
-
remember.click();
|
|
129
|
-
const next = document.querySelector("#signInSubmit");
|
|
130
|
-
next.click();
|
|
131
|
-
}, this.config)
|
|
132
|
-
.catch((e) => this.log.error(e));
|
|
133
|
-
|
|
134
|
-
const success = await this.page
|
|
135
|
-
.waitForSelector("#yourOrdersContent")
|
|
136
|
-
.then(() => true)
|
|
137
|
-
.catch(async (e) => {
|
|
138
|
-
this.log.debug(await this.page.content());
|
|
139
|
-
|
|
140
|
-
this.log.error(e);
|
|
141
|
-
|
|
142
|
-
this.log.error("Amazon login failed. Please check your credentials and login manually");
|
|
143
|
-
const errorHandle = await this.page.$(".a-alert-content .a-list-item");
|
|
144
|
-
if (errorHandle) {
|
|
145
|
-
this.log.error(await errorHandle.evaluate((node) => node.innerText));
|
|
146
|
-
}
|
|
147
|
-
return false;
|
|
148
|
-
});
|
|
149
|
-
if (!success) {
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
await this.setObjectNotExistsAsync("amazon", {
|
|
154
|
-
type: "device",
|
|
155
|
-
common: {
|
|
156
|
-
name: "Amazon Tracking",
|
|
157
|
-
},
|
|
158
|
-
native: {},
|
|
159
|
-
});
|
|
160
|
-
this.log.info("Login to Amazon successful");
|
|
161
|
-
this.sessions["amz"] = true;
|
|
162
|
-
this.setState("info.connection", true, true);
|
|
79
|
+
await this.loginAmz();
|
|
163
80
|
}
|
|
164
81
|
this.updateInterval = null;
|
|
165
82
|
this.reLoginTimeout = null;
|
|
@@ -340,7 +257,7 @@ class Parcel extends utils.Adapter {
|
|
|
340
257
|
}
|
|
341
258
|
}
|
|
342
259
|
|
|
343
|
-
async
|
|
260
|
+
async loginAmz() {
|
|
344
261
|
const body = await this.requestClient({
|
|
345
262
|
method: "get",
|
|
346
263
|
url: "https://www.amazon.de/ap/signin?openid.return_to=https://www.amazon.de/ap/maplanding&openid.oa2.code_challenge_method=S256&openid.assoc_handle=amzn_mshop_ios_v2_de&openid.identity=http://specs.openid.net/auth/2.0/identifier_select&pageId=amzn_mshop_ios_v2_de&accountStatusPolicy=P1&openid.claimed_id=http://specs.openid.net/auth/2.0/identifier_select&openid.mode=checkid_setup&openid.ns.oa2=http://www.amazon.com/ap/ext/oauth/2&openid.oa2.client_id=device:32467234687368746238704723437432432&openid.oa2.code_challenge=IeFTKnKcmHEPij50cdHHCq6ZVMbFYJMQQtbrMvKbgz0&openid.ns.pape=http://specs.openid.net/extensions/pape/1.0&openid.oa2.scope=device_auth_access&openid.ns=http://specs.openid.net/auth/2.0&openid.pape.max_auth_age=0&openid.oa2.response_type=code",
|
|
@@ -383,21 +300,67 @@ class Parcel extends utils.Adapter {
|
|
|
383
300
|
})
|
|
384
301
|
.then(async (res) => {
|
|
385
302
|
this.log.debug(JSON.stringify(res.data));
|
|
303
|
+
if (res.data.indexOf("auth-mfa-otpcode") !== -1) {
|
|
304
|
+
this.log.info("Found MFA token login");
|
|
305
|
+
const form = this.extractHidden(res.data);
|
|
306
|
+
form.otpCode = this.config.amzotp;
|
|
307
|
+
form.rememberDevice = true;
|
|
308
|
+
|
|
309
|
+
await this.requestClient({
|
|
310
|
+
method: "post",
|
|
311
|
+
url: "https://www.amazon.de/ap/signin",
|
|
312
|
+
headers: {
|
|
313
|
+
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
314
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
315
|
+
origin: "https://www.amazon.de",
|
|
316
|
+
"accept-language": "de-de",
|
|
317
|
+
"user-agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_8 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",
|
|
318
|
+
referer: "https://www.amazon.de/ap/signin",
|
|
319
|
+
},
|
|
320
|
+
data: qs.stringify(form),
|
|
321
|
+
jar: this.cookieJar,
|
|
322
|
+
withCredentials: true,
|
|
323
|
+
})
|
|
324
|
+
.then(async (res) => {
|
|
325
|
+
this.log.debug(JSON.stringify(res.data));
|
|
326
|
+
this.log.error("Login to Amazon failed, please login to Amazon and check your credentials");
|
|
327
|
+
this.setState("info.connection", false, true);
|
|
328
|
+
})
|
|
329
|
+
.catch(async (error) => {
|
|
330
|
+
if (error.response) {
|
|
331
|
+
if (error.response.status === 404) {
|
|
332
|
+
this.log.info("Login to Amazon successful");
|
|
333
|
+
this.sessions["amz"] = true;
|
|
334
|
+
this.setState("info.connection", true, true);
|
|
335
|
+
this.setState("auth.cookie", JSON.stringify(this.cookieJar.toJSON()), true);
|
|
336
|
+
await this.setObjectNotExistsAsync("amazon", {
|
|
337
|
+
type: "device",
|
|
338
|
+
common: {
|
|
339
|
+
name: "Amazon Tracking",
|
|
340
|
+
},
|
|
341
|
+
native: {},
|
|
342
|
+
});
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
this.setState("info.connection", false, true);
|
|
346
|
+
this.log.error(JSON.stringify(error.response.data));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
this.log.error(error);
|
|
350
|
+
});
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
386
353
|
if (res.data.indexOf("Amazon Anmelden") !== -1) {
|
|
387
354
|
this.log.error("Login to Amazon failed, please login to Amazon and check your credentials");
|
|
388
355
|
return;
|
|
389
356
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
name: "Amazon Tracking",
|
|
398
|
-
},
|
|
399
|
-
native: {},
|
|
400
|
-
});
|
|
357
|
+
if (res.data.indexOf("Zurücksetzen des Passworts erforderlich") !== -1) {
|
|
358
|
+
this.log.error("Zurücksetzen des Passworts erforderlich");
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
this.log.error("Login to Amazon failed, please login to Amazon and check your credentials");
|
|
362
|
+
this.setState("info.connection", false, true);
|
|
363
|
+
return;
|
|
401
364
|
})
|
|
402
365
|
.catch(async (error) => {
|
|
403
366
|
if (error.response) {
|
|
@@ -862,15 +825,28 @@ class Parcel extends utils.Adapter {
|
|
|
862
825
|
this.log.debug("Get Amazon Packages");
|
|
863
826
|
const amzResult = { sendungen: [] };
|
|
864
827
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
.
|
|
828
|
+
const orders = await this.requestClient({
|
|
829
|
+
method: "get",
|
|
830
|
+
url: "https://www.amazon.de/gp/css/order-history?ref_=nav_orders_first&disableCsd=missing-library",
|
|
831
|
+
headers: {
|
|
832
|
+
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
833
|
+
|
|
834
|
+
"accept-language": "de-de",
|
|
835
|
+
},
|
|
836
|
+
jar: this.cookieJar,
|
|
837
|
+
withCredentials: true,
|
|
838
|
+
})
|
|
839
|
+
.then(async (res) => {
|
|
840
|
+
//this.log.debug(JSON.stringify(res.data));
|
|
841
|
+
|
|
842
|
+
const dom = new JSDOM(res.data);
|
|
843
|
+
const document = dom.window.document;
|
|
868
844
|
const elements = [];
|
|
869
845
|
const orders = document.querySelectorAll(".a-box.shipment");
|
|
870
846
|
|
|
871
847
|
for (const order of orders) {
|
|
872
848
|
const descHandle = order.querySelector(".a-fixed-right-grid-col.a-col-left .a-row div:first-child .a-fixed-left-grid-col.a-col-right div:first-child .a-link-normal");
|
|
873
|
-
const desc = descHandle ? descHandle.
|
|
849
|
+
const desc = descHandle ? descHandle.textContent.replace(/\n */g, "") : "";
|
|
874
850
|
const url = order.querySelector(".track-package-button a") ? order.querySelector(".track-package-button a").getAttribute("href") : "";
|
|
875
851
|
if (url) {
|
|
876
852
|
elements.push({ desc: desc, url: url });
|
|
@@ -878,28 +854,62 @@ class Parcel extends utils.Adapter {
|
|
|
878
854
|
}
|
|
879
855
|
return elements;
|
|
880
856
|
})
|
|
881
|
-
.catch((
|
|
882
|
-
|
|
857
|
+
.catch((error) => {
|
|
858
|
+
this.log.error(error);
|
|
859
|
+
if (error.response) {
|
|
860
|
+
this.log.error(JSON.stringify(error.response.data));
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
this.log.debug("Found " + orders.length + " Amazon Orders");
|
|
883
864
|
for (const order of orders) {
|
|
884
865
|
if (order.url.indexOf("http") === -1) {
|
|
885
866
|
order.url = "https://www.amazon.de" + order.url;
|
|
886
867
|
}
|
|
887
868
|
this.log.debug(order.url);
|
|
888
|
-
|
|
889
|
-
const element = await this.
|
|
890
|
-
|
|
869
|
+
order.url = order.url + "&disableCsd=missing-library";
|
|
870
|
+
const element = await this.requestClient({
|
|
871
|
+
method: "get",
|
|
872
|
+
url: order.url,
|
|
873
|
+
headers: {
|
|
874
|
+
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
875
|
+
"accept-language": "de-de",
|
|
876
|
+
},
|
|
877
|
+
jar: this.cookieJar,
|
|
878
|
+
withCredentials: true,
|
|
879
|
+
})
|
|
880
|
+
.then(async (res) => {
|
|
881
|
+
// this.log.debug(JSON.stringify(res.data));
|
|
882
|
+
const dom = new JSDOM(res.data);
|
|
883
|
+
const document = dom.window.document;
|
|
891
884
|
const statusHandle = document.querySelector(".milestone-primaryMessage.alpha") || document.querySelector(".milestone-primaryMessage") || null;
|
|
892
|
-
const additionalStatus = document.querySelector("#primaryStatus") ? document.querySelector("#primaryStatus").
|
|
893
|
-
const secondaryStatus = document.querySelector("#secondaryStatus") ? document.querySelector("#secondaryStatus").
|
|
894
|
-
let status = statusHandle ? statusHandle.
|
|
895
|
-
|
|
885
|
+
const additionalStatus = document.querySelector("#primaryStatus") ? document.querySelector("#primaryStatus").textContent.replace(/\n */g, "") : "";
|
|
886
|
+
const secondaryStatus = document.querySelector("#secondaryStatus") ? document.querySelector("#secondaryStatus").textContent.replace(/\n */g, "") : "";
|
|
887
|
+
let status = statusHandle ? statusHandle.textContent.replace(/\n */g, "") : "";
|
|
888
|
+
if (!status) {
|
|
889
|
+
status = additionalStatus;
|
|
890
|
+
}
|
|
891
|
+
if (additionalStatus && status !== additionalStatus) {
|
|
892
|
+
status = status + " " + additionalStatus;
|
|
893
|
+
}
|
|
894
|
+
if (secondaryStatus) {
|
|
895
|
+
status = status + " " + secondaryStatus;
|
|
896
|
+
}
|
|
897
|
+
|
|
896
898
|
return {
|
|
897
|
-
id: document.querySelector(".carrierRelatedInfo-trackingId-text")
|
|
898
|
-
|
|
899
|
+
id: document.querySelector(".carrierRelatedInfo-trackingId-text")
|
|
900
|
+
? document.querySelector(".carrierRelatedInfo-trackingId-text").textContent.replace("Trackingnummer ", "")
|
|
901
|
+
: "",
|
|
902
|
+
name: document.querySelector(".carrierRelatedInfo-mfn-providerTitle") ? document.querySelector(".carrierRelatedInfo-mfn-providerTitle").textContent.replace(/\\n */g, "") : "",
|
|
899
903
|
status: status,
|
|
900
904
|
};
|
|
901
905
|
})
|
|
902
|
-
.catch((
|
|
906
|
+
.catch((error) => {
|
|
907
|
+
this.log.error(error);
|
|
908
|
+
if (error.response) {
|
|
909
|
+
this.log.error(JSON.stringify(error.response.data));
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
|
|
903
913
|
if (element) {
|
|
904
914
|
const orderId = qs.parse(order.url).orderId;
|
|
905
915
|
element.name = order.desc;
|
|
@@ -915,8 +925,8 @@ class Parcel extends utils.Adapter {
|
|
|
915
925
|
}
|
|
916
926
|
|
|
917
927
|
this.json2iob.parse("amazon", amzResult, { forceIndex: true });
|
|
918
|
-
|
|
919
928
|
this.mergeProviderJson("amz", amzResult);
|
|
929
|
+
await this.setStateAsync("auth.cookie", JSON.stringify(this.cookieJar.toJSON()), true);
|
|
920
930
|
}
|
|
921
931
|
async refreshToken() {
|
|
922
932
|
if (Object.keys(this.sessions).length === 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.parcel",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Parcel tracking",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "TA2k",
|
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
"http-cookie-agent": "^1.0.3",
|
|
29
29
|
"jsdom": "^19.0.0",
|
|
30
30
|
"json-bigint": "^1.0.0",
|
|
31
|
-
"puppeteer": "^13.1.3",
|
|
32
31
|
"qs": "^6.10.3",
|
|
33
32
|
"tough-cookie": "^4.0.0"
|
|
34
33
|
},
|
|
@@ -74,4 +73,4 @@
|
|
|
74
73
|
"url": "https://github.com/TA2k/ioBroker.parcel/issues"
|
|
75
74
|
},
|
|
76
75
|
"readmeFilename": "README.md"
|
|
77
|
-
}
|
|
76
|
+
}
|