Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Der-Henning
GitHub Repository: Der-Henning/tgtg
Path: blob/main/tgtg_scanner/models/config.py
725 views
1
from __future__ import annotations
2
3
import codecs
4
import configparser
5
import json
6
import logging
7
from abc import ABC, abstractmethod
8
from dataclasses import dataclass, field
9
from os import environ
10
from pathlib import Path
11
from typing import IO, Any, Union
12
13
import humanize
14
15
from tgtg_scanner.errors import ConfigurationError
16
from tgtg_scanner.models.cron import Cron
17
from tgtg_scanner.tgtg.tgtg_client import BASE_URL
18
19
log = logging.getLogger("tgtg")
20
21
CONFIG_FILE_HEADER = """## TGTG Scanner Configuration
22
## --------------------------
23
## This is the configuration file for the TGTG Scanner.
24
## You can find more information about the configuration on the project page:
25
## https://github.com/Der-Henning/tgtg/wiki/Configuration
26
27
"""
28
29
DEPRECATION_NOTICE = "{} is deprecated and will be removed in a future release. Please use {} instead."
30
31
32
@dataclass
33
class BaseConfig(ABC):
34
"""Base configuration"""
35
36
@abstractmethod
37
def _read_ini(self, parser: configparser.ConfigParser):
38
pass
39
40
@abstractmethod
41
def _read_env(self):
42
pass
43
44
@staticmethod
45
def _decode(value: str) -> str:
46
return codecs.escape_decode(bytes(value, "utf-8"))[0].decode("utf-8") # type: ignore[attr-defined]
47
48
def _ini_get(self, parser: configparser.ConfigParser, section: str, key: str, attr: str):
49
value = parser.get(section, key, fallback=None)
50
if value is not None:
51
setattr(self, attr, self._decode(value))
52
53
def _ini_get_boolean(self, parser: configparser.ConfigParser, section: str, key: str, attr: str):
54
try:
55
value = parser.getboolean(section, key, fallback=None)
56
except ValueError as err:
57
raise ConfigurationError(f"Invalid boolean value for {section}.{key} - {err}") from err
58
if value is not None:
59
setattr(self, attr, value)
60
61
def _ini_get_int(self, parser: configparser.ConfigParser, section: str, key: str, attr: str):
62
try:
63
value = parser.getint(section, key, fallback=None)
64
except ValueError as err:
65
raise ConfigurationError(f"Invalid integer value for {section}.{key} - {err}") from err
66
if value is not None:
67
setattr(self, attr, value)
68
69
def _ini_get_list(self, parser: configparser.ConfigParser, section: str, key: str, attr: str):
70
value = parser.get(section, key, fallback=None)
71
if value is not None:
72
setattr(self, attr, [self._decode(val.strip()) for val in value.split(",")])
73
74
def _ini_get_dict(self, parser: configparser.ConfigParser, section: str, key: str, attr: str):
75
value = parser.get(section, key, fallback=None)
76
if value is not None:
77
try:
78
setattr(self, attr, json.loads(value))
79
except json.JSONDecodeError as err:
80
raise ConfigurationError(f"Invalid JSON value for {section}.{key} - {err}") from err
81
82
def _ini_get_cron(self, parser: configparser.ConfigParser, section: str, key: str, attr: str):
83
value = parser.get(section, key, fallback=None)
84
if value is not None:
85
try:
86
setattr(self, attr, Cron(value))
87
except ValueError as err:
88
raise ConfigurationError(f"Invalid cron value for {section}.{key} - {err}") from err
89
90
def _env_get(self, key: str, attr: str):
91
value = environ.get(key, None)
92
if value is not None:
93
setattr(self, attr, self._decode(value))
94
95
def _env_get_boolean(self, key: str, attr: str):
96
value = environ.get(key, None)
97
if value is not None:
98
setattr(self, attr, value.lower() in {"true", "1", "t", "y", "yes"})
99
100
def _env_get_int(self, key: str, attr: str):
101
value = environ.get(key, None)
102
if value is not None:
103
try:
104
setattr(self, attr, int(value))
105
except ValueError as err:
106
raise ConfigurationError(f"Invalid integer value for {key} - {err}") from err
107
108
def _env_get_list(self, key: str, attr: str):
109
value = environ.get(key, None)
110
if value is not None:
111
setattr(self, attr, [self._decode(val.strip()) for val in value.split(",")])
112
113
def _env_get_dict(self, key: str, attr: str):
114
value = environ.get(key, None)
115
if value is not None:
116
try:
117
setattr(self, attr, json.loads(value))
118
except json.JSONDecodeError as err:
119
raise ConfigurationError(f"Invalid JSON value for {key} - {err}") from err
120
121
def _env_get_cron(self, key: str, attr: str):
122
value = environ.get(key, None)
123
if value is not None:
124
try:
125
setattr(self, attr, Cron(value))
126
except ValueError as err:
127
raise ConfigurationError(f"Invalid cron value for {key} - {err}") from err
128
129
130
@dataclass
131
class NotifierConfig(BaseConfig):
132
"""Base Notifier configuration"""
133
134
enabled: bool = False
135
cron: Cron = field(default_factory=Cron)
136
137
138
@dataclass
139
class AppriseConfig(NotifierConfig):
140
"""Apprise Notifier configuration"""
141
142
url: Union[str, None] = None
143
title: str = "New Magic Bags"
144
body: str = "${{display_name}} - new amount: ${{items_available}} - ${{link}}"
145
146
def _read_ini(self, parser: configparser.ConfigParser):
147
self._ini_get_boolean(parser, "APPRISE", "Enabled", "enabled")
148
self._ini_get_cron(parser, "APPRISE", "Cron", "cron")
149
self._ini_get(parser, "APPRISE", "URL", "url")
150
self._ini_get(parser, "APPRISE", "Title", "title")
151
self._ini_get(parser, "APPRISE", "Body", "body")
152
153
def _read_env(self):
154
self._env_get_boolean("APPRISE", "enabled")
155
self._env_get_cron("APPRISE_CRON", "cron")
156
self._env_get("APPRISE_URL", "url")
157
self._env_get("APPRISE_TITLE", "title")
158
self._env_get("APPRISE_BODY", "body")
159
160
161
@dataclass
162
class TelegramConfig(NotifierConfig):
163
"""Telegram Notifier configuration"""
164
165
token: Union[str, None] = None
166
chat_ids: list[str] = field(default_factory=list)
167
disable_commands: bool = False
168
only_reservations: bool = False
169
timeout: int = 60
170
body: str = (
171
"*${{display_name}}*\n*Available*: ${{items_available}}\n*Price*: ${{price}} ${{currency}}\n*Pickup*: ${{pickupdate}}"
172
)
173
image: Union[str, None] = None
174
175
def _read_ini(self, parser: configparser.ConfigParser):
176
self._ini_get_boolean(parser, "TELEGRAM", "Enabled", "enabled")
177
self._ini_get_cron(parser, "TELEGRAM", "Cron", "cron")
178
self._ini_get(parser, "TELEGRAM", "Token", "token")
179
if parser.has_option("TELEGRAM", "chat_ids"):
180
log.warning(DEPRECATION_NOTICE.format("[TELEGRAM] chat_ids", "ChatIDs"))
181
self._ini_get_list(parser, "TELEGRAM", "chat_ids", "chat_ids") # legacy support
182
self._ini_get_list(parser, "TELEGRAM", "ChatIDs", "chat_ids")
183
self._ini_get_boolean(parser, "TELEGRAM", "DisableCommands", "disable_commands")
184
self._ini_get_boolean(parser, "TELEGRAM", "OnlyReservations", "only_reservations")
185
self._ini_get_int(parser, "TELEGRAM", "Timeout", "timeout")
186
self._ini_get(parser, "TELEGRAM", "Body", "body")
187
self._ini_get(parser, "TELEGRAM", "Image", "image")
188
189
def _read_env(self):
190
self._env_get_boolean("TELEGRAM", "enabled")
191
self._env_get_cron("TELEGRAM_CRON", "cron")
192
self._env_get("TELEGRAM_TOKEN", "token")
193
self._env_get_list("TELEGRAM_CHAT_IDS", "chat_ids")
194
self._env_get_boolean("TELEGRAM_DISABLE_COMMANDS", "disable_commands")
195
self._env_get_boolean("TELEGRAM_ONLY_RESERVATIONS", "only_reservations")
196
self._env_get_int("TELEGRAM_TIMEOUT", "timeout")
197
self._env_get("TELEGRAM_BODY", "body")
198
self._env_get("TELEGRAM_IMAGE", "image")
199
200
201
@dataclass
202
class PushSaferConfig(NotifierConfig):
203
"""PushSafer Notifier configuration"""
204
205
key: Union[str, None] = None
206
device_ids: list[str] = field(default_factory=list)
207
208
@property
209
def device_id(self) -> str | None:
210
return self.device_ids[0] if self.device_ids else None
211
212
def _read_ini(self, parser: configparser.ConfigParser):
213
self._ini_get_boolean(parser, "PUSHSAFER", "Enabled", "enabled")
214
self._ini_get_cron(parser, "PUSHSAFER", "Cron", "cron")
215
self._ini_get(parser, "PUSHSAFER", "Key", "key")
216
self._ini_get_list(parser, "PUSHSAFER", "DeviceID", "device_ids") # Legacy support
217
self._ini_get_list(parser, "PUSHSAFER", "DeviceIDs", "device_ids")
218
219
def _read_env(self):
220
self._env_get_boolean("PUSHSAFER", "enabled")
221
self._env_get_cron("PUSHSAFER_CRON", "cron")
222
self._env_get("PUSHSAFER_KEY", "key")
223
self._env_get_list("PUSHSAFER_DEVICE_ID", "device_ids")
224
self._env_get_list("PUSHSAFER_DEVICE_IDS", "device_ids")
225
226
227
@dataclass
228
class ConsoleConfig(NotifierConfig):
229
"""Console Notifier configuration"""
230
231
body: str = "${{display_name}} - new amount: ${{items_available}} - ${{link}}"
232
233
def _read_ini(self, parser: configparser.ConfigParser):
234
self._ini_get_boolean(parser, "CONSOLE", "Enabled", "enabled")
235
self._ini_get_cron(parser, "CONSOLE", "Cron", "cron")
236
self._ini_get(parser, "CONSOLE", "Body", "body")
237
238
def _read_env(self):
239
self._env_get_boolean("CONSOLE", "enabled")
240
self._env_get_cron("CONSOLE_CRON", "cron")
241
self._env_get("CONSOLE_BODY", "body")
242
243
244
@dataclass
245
class SMTPConfig(NotifierConfig):
246
"""SMTP Notifier configuration"""
247
248
host: Union[str, None] = None
249
port: Union[int, None] = None
250
username: Union[str, None] = None
251
password: Union[str, None] = None
252
use_tls: bool = False
253
use_ssl: bool = False
254
timeout: int = 60
255
sender: Union[str, None] = None
256
recipients: list[str] = field(default_factory=list)
257
recipients_per_item: Union[str, None] = None
258
subject: str = "New Magic Bags"
259
body: str = "<b>${{display_name}}</b> </br>New Amount: ${{items_available}}"
260
261
def _read_ini(self, parser: configparser.ConfigParser):
262
self._ini_get_boolean(parser, "SMTP", "Enabled", "enabled")
263
self._ini_get_cron(parser, "SMTP", "Cron", "cron")
264
self._ini_get(parser, "SMTP", "Host", "host")
265
self._ini_get_int(parser, "SMTP", "Port", "port")
266
self._ini_get(parser, "SMTP", "Username", "username")
267
self._ini_get(parser, "SMTP", "Password", "password")
268
self._ini_get_boolean(parser, "SMTP", "TLS", "use_tls")
269
self._ini_get_boolean(parser, "SMTP", "SSL", "use_ssl")
270
self._ini_get_int(parser, "SMTP", "Timeout", "timeout")
271
self._ini_get(parser, "SMTP", "Sender", "sender")
272
if parser.has_option("SMTP", "Recipient"):
273
log.warning(DEPRECATION_NOTICE.format("[SMTP] Recipient", "Recipients"))
274
self._ini_get_list(parser, "SMTP", "Recipient", "recipients") # legacy support
275
self._ini_get_list(parser, "SMTP", "Recipients", "recipients")
276
self._ini_get(parser, "SMTP", "RecipientsPerItem", "recipients_per_item")
277
self._ini_get(parser, "SMTP", "Subject", "subject")
278
self._ini_get(parser, "SMTP", "Body", "body")
279
280
def _read_env(self):
281
self._env_get_boolean("SMTP", "enabled")
282
self._env_get_cron("SMTP_CRON", "cron")
283
self._env_get("SMTP_HOST", "host")
284
self._env_get_int("SMTP_PORT", "port")
285
self._env_get("SMTP_USERNAME", "username")
286
self._env_get("SMTP_PASSWORD", "password")
287
self._env_get_boolean("SMTP_TLS", "use_tls")
288
self._env_get_boolean("SMTP_SSL", "use_ssl")
289
self._env_get_int("SMTP_TIMEOUT", "timeout")
290
self._env_get("SMTP_SENDER", "sender")
291
if environ.get("SMTP_RECIPIENT", None):
292
log.warning(DEPRECATION_NOTICE.format("SMTP_RECIPIENT", "SMTP_RECIPIENTS"))
293
self._env_get_list("SMTP_RECIPIENT", "recipients") # legacy support
294
self._env_get_list("SMTP_RECIPIENTS", "recipients")
295
self._env_get("SMTP_RECIPIENTS_PER_ITEM", "recipients_per_item")
296
self._env_get("SMTP_SUBJECT", "subject")
297
self._env_get("SMTP_BODY", "body")
298
299
300
@dataclass
301
class IFTTTConfig(NotifierConfig):
302
"""IFTTT Notifier configuration"""
303
304
event: str = "tgtg_notification"
305
key: Union[str, None] = None
306
body: str = '{"value1": "${{display_name}}", "value2": ${{items_available}}, "value3": "${{link}}"}'
307
timeout: int = 60
308
309
def _read_ini(self, parser: configparser.ConfigParser):
310
self._ini_get_boolean(parser, "IFTTT", "Enabled", "enabled")
311
self._ini_get_cron(parser, "IFTTT", "Cron", "cron")
312
self._ini_get(parser, "IFTTT", "Event", "event")
313
self._ini_get(parser, "IFTTT", "Key", "key")
314
self._ini_get(parser, "IFTTT", "Body", "body")
315
self._ini_get_int(parser, "IFTTT", "Timeout", "timeout")
316
317
def _read_env(self):
318
self._env_get_boolean("IFTTT", "enabled")
319
self._env_get_cron("IFTTT_CRON", "cron")
320
self._env_get("IFTTT_EVENT", "event")
321
self._env_get("IFTTT_KEY", "key")
322
self._env_get("IFTTT_BODY", "body")
323
self._env_get_int("IFTTT_TIMEOUT", "timeout")
324
325
326
@dataclass
327
class NtfyConfig(NotifierConfig):
328
"""Ntfy Notifier configuration"""
329
330
server: str = "https://ntfy.sh"
331
topic: Union[str, None] = None
332
title: str = "New Magic Bags"
333
message: str = "${{display_name}} - New Amount: ${{items_available}} - ${{link}}"
334
body: Union[str, None] = None
335
priority: str = "default"
336
tags: str = "shopping,tgtg"
337
click: str = "${{link}}"
338
username: Union[str, None] = None
339
password: Union[str, None] = None
340
token: Union[str, None] = None
341
timeout: int = 60
342
343
def _read_ini(self, parser: configparser.ConfigParser):
344
self._ini_get_boolean(parser, "NTFY", "Enabled", "enabled")
345
self._ini_get_cron(parser, "NTFY", "Cron", "cron")
346
self._ini_get(parser, "NTFY", "Server", "server")
347
self._ini_get(parser, "NTFY", "Topic", "topic")
348
self._ini_get(parser, "NTFY", "Title", "title")
349
self._ini_get(parser, "NTFY", "Message", "message")
350
self._ini_get(parser, "NTFY", "Body", "body")
351
self._ini_get(parser, "NTFY", "Priority", "priority")
352
self._ini_get(parser, "NTFY", "Tags", "tags")
353
self._ini_get(parser, "NTFY", "Click", "click")
354
self._ini_get(parser, "NTFY", "Username", "username")
355
self._ini_get(parser, "NTFY", "Password", "password")
356
self._ini_get(parser, "NTFY", "Token", "token")
357
self._ini_get_int(parser, "NTFY", "Timeout", "timeout")
358
359
def _read_env(self):
360
self._env_get_boolean("NTFY", "enabled")
361
self._env_get_cron("NTFY_CRON", "cron")
362
self._env_get("NTFY_SERVER", "server")
363
self._env_get("NTFY_TOPIC", "topic")
364
self._env_get("NTFY_TITLE", "title")
365
self._env_get("NTFY_MESSAGE", "message")
366
self._env_get("NTFY_BODY", "body")
367
self._env_get("NTFY_PRIORITY", "priority")
368
self._env_get("NTFY_TAGS", "tags")
369
self._env_get("NTFY_CLICK", "click")
370
self._env_get("NTFY_USERNAME", "username")
371
self._env_get("NTFY_PASSWORD", "password")
372
self._env_get("NTFY_TOKEN", "token")
373
self._env_get_int("NTFY_TIMEOUT", "timeout")
374
375
376
@dataclass
377
class WebhookConfig(NotifierConfig):
378
"""Webhook Notifier configuration"""
379
380
url: Union[str, None] = None
381
method: str = "POST"
382
headers: dict[str, str | bytes] = field(default_factory=dict)
383
body: str = ""
384
type: str = "text/plain"
385
timeout: int = 60
386
username: Union[str, None] = None
387
password: Union[str, None] = None
388
389
def _read_ini(self, parser: configparser.ConfigParser):
390
self._ini_get_boolean(parser, "WEBHOOK", "Enabled", "enabled")
391
self._ini_get_cron(parser, "WEBHOOK", "Cron", "cron")
392
self._ini_get(parser, "WEBHOOK", "URL", "url")
393
self._ini_get(parser, "WEBHOOK", "Method", "method")
394
self._ini_get_dict(parser, "WEBHOOK", "Headers", "headers")
395
self._ini_get(parser, "WEBHOOK", "Body", "body")
396
self._ini_get(parser, "WEBHOOK", "Type", "type")
397
self._ini_get(parser, "WEBHOOK", "Username", "username")
398
self._ini_get(parser, "WEBHOOK", "Password", "password")
399
self._ini_get_int(parser, "WEBHOOK", "Timeout", "timeout")
400
401
def _read_env(self):
402
self._env_get_boolean("WEBHOOK", "enabled")
403
self._env_get_cron("WEBHOOK_CRON", "cron")
404
self._env_get("WEBHOOK_URL", "url")
405
self._env_get("WEBHOOK_METHOD", "method")
406
self._env_get_dict("WEBHOOK_HEADERS", "headers")
407
self._env_get("WEBHOOK_BODY", "body")
408
self._env_get("WEBHOOK_TYPE", "type")
409
self._env_get("WEBHOOK_USERNAME", "username")
410
self._env_get("WEBHOOK_PASSWORD", "password")
411
self._env_get_int("WEBHOOK_TIMEOUT", "timeout")
412
413
414
@dataclass
415
class ScriptConfig(NotifierConfig):
416
"""Script Notifier configuration"""
417
418
command: Union[str, None] = None
419
420
def _read_ini(self, parser: configparser.ConfigParser):
421
self._ini_get_boolean(parser, "SCRIPT", "Enabled", "enabled")
422
self._ini_get_cron(parser, "SCRIPT", "Cron", "cron")
423
self._ini_get(parser, "SCRIPT", "Command", "command")
424
425
def _read_env(self):
426
self._env_get_boolean("SCRIPT", "enabled")
427
self._env_get_cron("SCRIPT_CRON", "cron")
428
self._env_get("SCRIPT_COMMAND", "command")
429
430
431
@dataclass
432
class DiscordConfig(NotifierConfig):
433
"""Discord configuration"""
434
435
enabled: bool = False
436
prefix: Union[str, None] = "!"
437
token: Union[str, None] = None
438
channel: int = 0
439
body: str = (
440
"*${{display_name}}*\n*Available*: ${{items_available}}\n*Price*: ${{price}} ${{currency}}\n*Pickup*: ${{pickupdate}}"
441
)
442
disable_commands: bool = False
443
444
def _read_ini(self, parser: configparser.ConfigParser):
445
self._ini_get_boolean(parser, "DISCORD", "Enabled", "enabled")
446
self._ini_get(parser, "DISCORD", "Prefix", "prefix")
447
self._ini_get(parser, "DISCORD", "Token", "token")
448
self._ini_get_int(parser, "DISCORD", "Channel", "channel")
449
self._ini_get(parser, "DISCORD", "Body", "body")
450
self._ini_get_boolean(parser, "DISCORD", "DisableCommands", "disable_commands")
451
self._ini_get_cron(parser, "DISCORD", "Cron", "cron")
452
453
def _read_env(self):
454
self._env_get_boolean("DISCORD", "enabled")
455
self._env_get("DISCORD_PREFIX", "prefix")
456
self._env_get("DISCORD_TOKEN", "token")
457
self._env_get_int("DISCORD_CHANNEL", "channel")
458
self._env_get("DISCORD_BODY", "body")
459
self._env_get_boolean("DISCORD_DISABLE_COMMANDS", "disable_commands")
460
self._env_get_cron("DISCORD_CRON", "cron")
461
462
463
@dataclass
464
class TgtgConfig(BaseConfig):
465
"""Tgtg configuration"""
466
467
username: Union[str, None] = None
468
access_token: Union[str, None] = None
469
refresh_token: Union[str, None] = None
470
datadome: Union[str, None] = None
471
timeout: int = 60
472
access_token_lifetime: int = 14400
473
max_polling_tries: int = 24
474
polling_wait_time: int = 5
475
base_url: str = BASE_URL
476
477
def _read_ini(self, parser: configparser.ConfigParser):
478
self._ini_get(parser, "TGTG", "Username", "username")
479
self._ini_get(parser, "TGTG", "AccessToken", "access_token")
480
self._ini_get(parser, "TGTG", "RefreshToken", "refresh_token")
481
self._ini_get(parser, "TGTG", "Datadome", "datadome")
482
self._ini_get_int(parser, "TGTG", "Timeout", "timeout")
483
self._ini_get_int(parser, "TGTG", "AccessTokenLifetime", "access_token_lifetime")
484
self._ini_get_int(parser, "TGTG", "MaxPollingTries", "max_polling_tries")
485
self._ini_get_int(parser, "TGTG", "PollingWaitTime", "polling_wait_time")
486
487
def _read_env(self):
488
self._env_get("TGTG_USERNAME", "username")
489
self._env_get("TGTG_ACCESS_TOKEN", "access_token")
490
self._env_get("TGTG_REFRESH_TOKEN", "refresh_token")
491
self._env_get("TGTG_DATADOME", "datadome")
492
self._env_get_int("TGTG_TIMEOUT", "timeout")
493
self._env_get_int("TGTG_ACCESS_TOKEN_LIFETIME", "access_token_lifetime")
494
self._env_get_int("TGTG_MAX_POLLING_TRIES", "max_polling_tries")
495
self._env_get_int("TGTG_POLLING_WAIT_TIME", "polling_wait_time")
496
497
498
@dataclass
499
class LocationConfig(BaseConfig):
500
"""Location configuration"""
501
502
enabled: bool = False
503
google_maps_api_key: Union[str, None] = None
504
origin_address: Union[str, None] = None
505
506
def _read_ini(self, parser: configparser.ConfigParser):
507
self._ini_get_boolean(parser, "LOCATION", "Enabled", "enabled")
508
if parser.has_option("LOCATION", "Google_Maps_API_Key"):
509
log.warning(DEPRECATION_NOTICE.format("[LOCATION] Google_Maps_API_Key", "GoogleMapsAPIKey"))
510
self._ini_get(parser, "LOCATION", "Google_Maps_API_Key", "google_maps_api_key") # legacy support
511
self._ini_get(parser, "LOCATION", "GoogleMapsAPIKey", "google_maps_api_key")
512
if parser.has_option("LOCATION", "Address"):
513
log.warning(DEPRECATION_NOTICE.format("[LOCATION] Address", "OriginAddress"))
514
self._ini_get(parser, "LOCATION", "Address", "origin_address") # legacy support
515
self._ini_get(parser, "LOCATION", "OriginAddress", "origin_address")
516
517
def _read_env(self):
518
self._env_get_boolean("LOCATION", "enabled")
519
self._env_get("LOCATION_GOOGLE_MAPS_API_KEY", "google_maps_api_key")
520
if environ.get("LOCATION_ADDRESS", None):
521
log.warning(DEPRECATION_NOTICE.format("LOCATION_ADDRESS", "LOCATION_ORIGIN_ADDRESS"))
522
self._env_get("LOCATION_ADDRESS", "origin_address") # legacy support
523
self._env_get("LOCATION_ORIGIN_ADDRESS", "origin_address")
524
525
526
@dataclass
527
class Config(BaseConfig):
528
"""Main configuration"""
529
530
file: Union[str, None] = None
531
item_ids: list[str] = field(default_factory=list)
532
sleep_time: int = 60
533
schedule_cron: Cron = field(default_factory=Cron)
534
debug: bool = False
535
locale: str = "en_US"
536
metrics: bool = False
537
metrics_port: int = 8000
538
disable_tests: bool = False
539
quiet: bool = False
540
docker: bool = False
541
activity: bool = True
542
tgtg: TgtgConfig = field(default_factory=TgtgConfig)
543
location: LocationConfig = field(default_factory=LocationConfig)
544
token_path: Union[str, None] = None
545
apprise: AppriseConfig = field(default_factory=AppriseConfig)
546
telegram: TelegramConfig = field(default_factory=TelegramConfig)
547
pushsafer: PushSaferConfig = field(default_factory=PushSaferConfig)
548
console: ConsoleConfig = field(default_factory=ConsoleConfig)
549
smtp: SMTPConfig = field(default_factory=SMTPConfig)
550
ifttt: IFTTTConfig = field(default_factory=IFTTTConfig)
551
ntfy: NtfyConfig = field(default_factory=NtfyConfig)
552
webhook: WebhookConfig = field(default_factory=WebhookConfig)
553
script: ScriptConfig = field(default_factory=ScriptConfig)
554
discord: DiscordConfig = field(default_factory=DiscordConfig)
555
556
def __post_init__(self):
557
if self.file:
558
config_file = Path(self.file)
559
if not config_file.is_file():
560
raise ConfigurationError(f"Configuration file '{config_file.absolute()}' is not a file!")
561
config_file = Path(self.file)
562
parser = configparser.ConfigParser()
563
parser.read(config_file, encoding="utf-8")
564
self._read_ini(parser)
565
self.tgtg._read_ini(parser)
566
self.location._read_ini(parser)
567
self.apprise._read_ini(parser)
568
self.telegram._read_ini(parser)
569
self.pushsafer._read_ini(parser)
570
self.console._read_ini(parser)
571
self.smtp._read_ini(parser)
572
self.ifttt._read_ini(parser)
573
self.ntfy._read_ini(parser)
574
self.webhook._read_ini(parser)
575
self.script._read_ini(parser)
576
self.discord._read_ini(parser)
577
578
log.info("Loaded config from %s", config_file.absolute())
579
else:
580
self._read_env()
581
self.tgtg._read_env()
582
self.location._read_env()
583
self.apprise._read_env()
584
self.telegram._read_env()
585
self.pushsafer._read_env()
586
self.console._read_env()
587
self.smtp._read_env()
588
self.ifttt._read_env()
589
self.ntfy._read_env()
590
self.webhook._read_env()
591
self.script._read_env()
592
self.discord._read_env()
593
594
log.info("Loaded config from environment variables")
595
596
self.token_path = environ.get("TGTG_TOKEN_PATH", None)
597
self._load_tokens()
598
self.set_locale()
599
600
def set_locale(self) -> None:
601
if self.locale and not self.locale.startswith("en"):
602
try:
603
log.debug("Activating locale %s", self.locale)
604
humanize.i18n.activate(self.locale)
605
except FileNotFoundError as err:
606
raise ConfigurationError(f"Invalid locale '{self.locale}' - {err}") from err
607
608
def _read_ini(self, parser: configparser.ConfigParser):
609
self._ini_get_list(parser, "MAIN", "ItemIDs", "item_ids")
610
self._ini_get_int(parser, "MAIN", "SleepTime", "sleep_time")
611
self._ini_get_cron(parser, "MAIN", "ScheduleCron", "schedule_cron")
612
self._ini_get_boolean(parser, "MAIN", "Debug", "debug")
613
self._ini_get(parser, "MAIN", "Locale", "locale")
614
self._ini_get_boolean(parser, "MAIN", "Metrics", "metrics")
615
self._ini_get_int(parser, "MAIN", "MetricsPort", "metrics_port")
616
self._ini_get_boolean(parser, "MAIN", "DisableTests", "disable_tests")
617
self._ini_get_boolean(parser, "MAIN", "Quiet", "quiet")
618
self._ini_get_boolean(parser, "MAIN", "Docker", "docker")
619
self._ini_get_boolean(parser, "MAIN", "Activity", "activity")
620
621
def _read_env(self):
622
self._env_get_list("ITEM_IDS", "item_ids")
623
self._env_get_int("SLEEP_TIME", "sleep_time")
624
self._env_get_cron("SCHEDULE_CRON", "schedule_cron")
625
self._env_get_boolean("DEBUG", "debug")
626
self._env_get("LOCALE", "locale")
627
self._env_get_boolean("METRICS", "metrics")
628
self._env_get_int("METRICS_PORT", "metrics_port")
629
self._env_get_boolean("DISABLE_TESTS", "disable_tests")
630
self._env_get_boolean("QUIET", "quiet")
631
self._env_get_boolean("DOCKER", "docker")
632
self._env_get_boolean("ACTIVITY", "activity")
633
634
def _open(self, file: str, mode: str) -> IO[Any]:
635
if self.token_path is None:
636
raise ConfigurationError("Token path is not set.")
637
return open(Path(self.token_path, file), mode, encoding="utf-8")
638
639
def _load_tokens(self) -> None:
640
"""
641
Reads tokens from token files
642
"""
643
if self.token_path is not None:
644
try:
645
with self._open("accessToken", "r") as file:
646
self.tgtg.access_token = file.read()
647
with self._open("refreshToken", "r") as file:
648
self.tgtg.refresh_token = file.read()
649
with self._open("datadome", "r") as file:
650
self.tgtg.datadome = file.read()
651
except FileNotFoundError:
652
log.warning("No token files in token path.")
653
except EnvironmentError as err:
654
log.error("Error loading Tokens - %s", err)
655
656
def save_tokens(self, access_token: str, refresh_token: str, datadome: str) -> None:
657
"""
658
Saves TGTG Access Tokens to config.ini
659
if provided or as files to token_path.
660
"""
661
if self.file is not None:
662
try:
663
config_file = Path(self.file)
664
config = configparser.ConfigParser()
665
config.optionxform = str # type: ignore
666
config.read(config_file, encoding="utf-8")
667
if "TGTG" not in config.sections():
668
config.add_section("TGTG")
669
config.set("TGTG", "AccessToken", access_token)
670
config.set("TGTG", "RefreshToken", refresh_token)
671
config.set("TGTG", "Datadome", datadome)
672
with open(config_file, "w", encoding="utf-8") as configfile:
673
configfile.write(CONFIG_FILE_HEADER)
674
config.write(configfile)
675
except EnvironmentError as err:
676
log.error("error saving credentials to config.ini! - %s", err)
677
if self.token_path is not None:
678
try:
679
with self._open("accessToken", "w") as file:
680
file.write(access_token)
681
with self._open("refreshToken", "w") as file:
682
file.write(refresh_token)
683
with self._open("datadome", "w") as file:
684
file.write(datadome)
685
except EnvironmentError as err:
686
log.error("error saving credentials! - %s", err)
687
688
def set(self, section: str, option: str, value: str) -> bool:
689
"""
690
Sets an option in config.ini if provided.
691
"""
692
if self.file is not None:
693
try:
694
config = configparser.ConfigParser()
695
config.optionxform = str # type: ignore
696
config.read(self.file, encoding="utf-8")
697
if section not in config.sections():
698
config.add_section(section)
699
config.set(section, option, str(value))
700
with open(self.file, "w", encoding="utf-8") as configfile:
701
config.write(configfile)
702
return True
703
except EnvironmentError as err:
704
log.error("error writing config.ini! - %s", err)
705
return False
706
707