TgrEzLi 0.1.2__py3-none-any.whl → 1.0.0__py3-none-any.whl
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.
Potentially problematic release.
This version of TgrEzLi might be problematic. Click here for more details.
- TgrEzLi/__init__.py +26 -10
- TgrEzLi/api_server.py +120 -0
- TgrEzLi/config.py +5 -0
- TgrEzLi/core.py +482 -373
- TgrEzLi/crypto.py +39 -0
- TgrEzLi/defaults.py +16 -0
- TgrEzLi/handlers.py +70 -0
- TgrEzLi/logger.py +65 -0
- TgrEzLi/models.py +82 -0
- TgrEzLi/requests.py +49 -0
- TgrEzLi/types.py +119 -0
- tgrezli-1.0.0.dist-info/METADATA +274 -0
- tgrezli-1.0.0.dist-info/RECORD +15 -0
- {tgrezli-0.1.2.dist-info → tgrezli-1.0.0.dist-info}/WHEEL +1 -2
- tgrezli-1.0.0.dist-info/entry_points.txt +3 -0
- tgrezli-0.1.2.dist-info/METADATA +0 -238
- tgrezli-0.1.2.dist-info/RECORD +0 -6
- tgrezli-0.1.2.dist-info/top_level.txt +0 -1
TgrEzLi/crypto.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Encrypted credential storage (PyCypherLib / PyCypher)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from PyCypher import Cy
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CredentialManager:
|
|
10
|
+
"""Store and load Telegram token + chat dict encrypted with a password (PyCypherLib)."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, filename: str = "tgrdata.cy") -> None:
|
|
13
|
+
self.filename = filename
|
|
14
|
+
|
|
15
|
+
def signup(self, token: str, chat_dict: dict[str, str], password: str) -> None:
|
|
16
|
+
"""Create encrypted file with token and chat IDs. Fails if file already exists."""
|
|
17
|
+
|
|
18
|
+
if os.path.exists(self.filename):
|
|
19
|
+
raise FileExistsError("Encrypted file already exists.")
|
|
20
|
+
lines = [token] + [f"{name}:{cid}" for name, cid in chat_dict.items()]
|
|
21
|
+
Cy().encLines(self.filename).Lines(lines).P(password)
|
|
22
|
+
|
|
23
|
+
def login(self, password: str) -> tuple[str, dict[str, str]]:
|
|
24
|
+
"""Read encrypted file and return (token, chat_dict)."""
|
|
25
|
+
if not os.path.exists(self.filename):
|
|
26
|
+
raise FileNotFoundError("Encrypted file not found.")
|
|
27
|
+
try:
|
|
28
|
+
lines = Cy().decLines(self.filename).P(password)
|
|
29
|
+
except Exception as e:
|
|
30
|
+
raise ValueError("Invalid password or corrupted encrypted file.") from e
|
|
31
|
+
token = lines[0]
|
|
32
|
+
chat_dict: dict[str, str] = {}
|
|
33
|
+
for element in lines[1:]:
|
|
34
|
+
name, cid = element.split(":", 1)
|
|
35
|
+
chat_dict[name] = cid
|
|
36
|
+
return token, chat_dict
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
__all__ = ["CredentialManager"]
|
TgrEzLi/defaults.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Default constants for TgrEzLi."""
|
|
2
|
+
|
|
3
|
+
# API server
|
|
4
|
+
DEFAULT_API_HOST = "localhost"
|
|
5
|
+
DEFAULT_API_PORT = 9999
|
|
6
|
+
MAX_REQUEST_BODY_BYTES = 1 * 1024 * 1024 # 1 MiB
|
|
7
|
+
|
|
8
|
+
# Logging
|
|
9
|
+
DEFAULT_LOG_FILE = "TgrEzLi.log"
|
|
10
|
+
DEFAULT_LOGGER_NAME = "TgrEzLi"
|
|
11
|
+
|
|
12
|
+
# Credentials (crypto)
|
|
13
|
+
DEFAULT_CREDENTIAL_FILE = "tgrdata.cy"
|
|
14
|
+
|
|
15
|
+
# Banner
|
|
16
|
+
DEFAULT_BANNER_FILLER = "█"
|
TgrEzLi/handlers.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Handler registry and user-callback invocation (threaded)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import threading
|
|
5
|
+
import traceback
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
|
|
8
|
+
from TgrEzLi.models import clear_context, set_context
|
|
9
|
+
from TgrEzLi.types import TgArgsData, TgCbData, TgCmdData, TgMsgData
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def call_user_function(
|
|
13
|
+
logger: Any,
|
|
14
|
+
func: Callable[[], None],
|
|
15
|
+
*,
|
|
16
|
+
TgMsg: TgMsgData | None = None,
|
|
17
|
+
TgCmd: TgCmdData | None = None,
|
|
18
|
+
TgCb: TgCbData | None = None,
|
|
19
|
+
TgArgs: TgArgsData | None = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Run the user handler in a background thread with thread-local context set."""
|
|
22
|
+
def worker() -> None:
|
|
23
|
+
try:
|
|
24
|
+
set_context(TgMsg=TgMsg, TgCmd=TgCmd, TgCb=TgCb, TgArgs=TgArgs)
|
|
25
|
+
func()
|
|
26
|
+
except Exception as e: # noqa: BLE001
|
|
27
|
+
logger.error("Error in user handler: %s", e)
|
|
28
|
+
logger.debug(traceback.format_exc())
|
|
29
|
+
finally:
|
|
30
|
+
clear_context()
|
|
31
|
+
|
|
32
|
+
thread = threading.Thread(target=worker, daemon=True)
|
|
33
|
+
thread.start()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class HandlerRegistry:
|
|
37
|
+
"""Registry for message, command, callback, and API route handlers."""
|
|
38
|
+
|
|
39
|
+
__slots__ = ("message_handlers", "command_handlers", "callback_handlers", "api_routes")
|
|
40
|
+
|
|
41
|
+
def __init__(self) -> None:
|
|
42
|
+
self.message_handlers: list[tuple[set[str], Callable[[], None]]] = []
|
|
43
|
+
self.command_handlers: dict[str, list[tuple[set[str], Callable[[], None]]]] = {}
|
|
44
|
+
self.callback_handlers: list[tuple[set[str], Callable[[], None]]] = []
|
|
45
|
+
self.api_routes: dict[str, dict[str, Any]] = {} # path -> {"args": [...], "func": ...}
|
|
46
|
+
|
|
47
|
+
def register_message_handler(self, func: Callable[[], None], chats: set[str]) -> None:
|
|
48
|
+
name = getattr(func, "__name__", None)
|
|
49
|
+
if name is not None:
|
|
50
|
+
self.message_handlers[:] = [(c, f) for c, f in self.message_handlers if getattr(f, "__name__", None) != name]
|
|
51
|
+
self.message_handlers.append((chats, func))
|
|
52
|
+
|
|
53
|
+
def register_command_handler(self, command: str, func: Callable[[], None], chats: set[str]) -> None:
|
|
54
|
+
name = getattr(func, "__name__", None)
|
|
55
|
+
lst = self.command_handlers.setdefault(command, [])
|
|
56
|
+
if name is not None:
|
|
57
|
+
lst[:] = [(c, f) for c, f in lst if getattr(f, "__name__", None) != name]
|
|
58
|
+
lst.append((chats, func))
|
|
59
|
+
|
|
60
|
+
def register_callback_handler(self, func: Callable[[], None], chats: set[str]) -> None:
|
|
61
|
+
name = getattr(func, "__name__", None)
|
|
62
|
+
if name is not None:
|
|
63
|
+
self.callback_handlers[:] = [(c, f) for c, f in self.callback_handlers if getattr(f, "__name__", None) != name]
|
|
64
|
+
self.callback_handlers.append((chats, func))
|
|
65
|
+
|
|
66
|
+
def register_api_route(self, endpoint: str, args_list: list[str], func: Callable[[], None]) -> None:
|
|
67
|
+
self.api_routes[endpoint] = {"args": args_list, "func": func}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
__all__ = ["call_user_function", "HandlerRegistry"]
|
TgrEzLi/logger.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Logger for TgrEzLi: ezlog for console, optional file append when save_log is True."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
from ezlog import EzLog
|
|
7
|
+
|
|
8
|
+
from TgrEzLi.defaults import DEFAULT_LOG_FILE
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _format_msg(msg: str, *args: object) -> str:
|
|
12
|
+
return msg % args if args else msg
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TgrezliLogger:
|
|
16
|
+
"""Adapter: EzLog for console output and optional file append (save_log)."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
save_log: bool = True,
|
|
21
|
+
log_file: str | None = DEFAULT_LOG_FILE,
|
|
22
|
+
) -> None:
|
|
23
|
+
self._ez = EzLog()
|
|
24
|
+
self._save_log = save_log
|
|
25
|
+
self._log_file = log_file or None
|
|
26
|
+
|
|
27
|
+
def set_save_log(self, flag: bool) -> None:
|
|
28
|
+
"""Enable or disable writing log lines to the file."""
|
|
29
|
+
self._save_log = flag
|
|
30
|
+
|
|
31
|
+
def _write_file(self, level: str, msg: str) -> None:
|
|
32
|
+
if not self._save_log or not self._log_file:
|
|
33
|
+
return
|
|
34
|
+
try:
|
|
35
|
+
line = f"[{datetime.now():%Y-%m-%d %H:%M:%S}] [{level}] TgrEzLi - {msg}\n"
|
|
36
|
+
with open(self._log_file, "a", encoding="utf-8") as f:
|
|
37
|
+
f.write(line)
|
|
38
|
+
except OSError:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
def info(self, msg: str, *args: object) -> None:
|
|
42
|
+
formatted = _format_msg(msg, *args)
|
|
43
|
+
self._ez.info(formatted)
|
|
44
|
+
self._write_file("INFO", formatted)
|
|
45
|
+
|
|
46
|
+
def debug(self, msg: str, *args: object) -> None:
|
|
47
|
+
formatted = _format_msg(msg, *args)
|
|
48
|
+
self._ez.d(formatted)
|
|
49
|
+
self._write_file("DEBUG", formatted)
|
|
50
|
+
|
|
51
|
+
def error(self, msg: str, *args: object) -> None:
|
|
52
|
+
formatted = _format_msg(msg, *args)
|
|
53
|
+
self._ez.error(formatted)
|
|
54
|
+
self._write_file("ERROR", formatted)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_default_logger(
|
|
58
|
+
save_log: bool = True,
|
|
59
|
+
log_file: str | None = DEFAULT_LOG_FILE,
|
|
60
|
+
) -> TgrezliLogger:
|
|
61
|
+
"""Return a logger that uses ezlog for console and optionally appends to a file."""
|
|
62
|
+
return TgrezliLogger(save_log=save_log, log_file=log_file)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
__all__ = ["TgrezliLogger", "get_default_logger"]
|
TgrEzLi/models.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Data payloads and thread-local proxies for handler context (TgMsg, TgCmd, TgCb, TgArgs)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import threading
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from TgrEzLi.types import TgArgsData, TgCbData, TgCmdData, TgMsgData
|
|
8
|
+
|
|
9
|
+
# Thread-local storage for current handler payload
|
|
10
|
+
_local: threading.local = threading.local()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_proxy(attr: str, label: str) -> Any:
|
|
14
|
+
obj = getattr(_local, attr, None)
|
|
15
|
+
if obj is None:
|
|
16
|
+
raise AttributeError(f"{label} is not available in this context (use inside a handler).")
|
|
17
|
+
return obj
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TgMsgProxy:
|
|
21
|
+
"""Proxy to current message payload; use only inside onMessage handlers."""
|
|
22
|
+
|
|
23
|
+
def __getattr__(self, name: str) -> Any:
|
|
24
|
+
return getattr(_get_proxy("TgMsg", "TgMsg"), name)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TgCmdProxy:
|
|
28
|
+
"""Proxy to current command payload; use only inside onCommand handlers."""
|
|
29
|
+
|
|
30
|
+
def __getattr__(self, name: str) -> Any:
|
|
31
|
+
return getattr(_get_proxy("TgCmd", "TgCmd"), name)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TgCbProxy:
|
|
35
|
+
"""Proxy to current callback payload; use only inside onCallback handlers."""
|
|
36
|
+
|
|
37
|
+
def __getattr__(self, name: str) -> Any:
|
|
38
|
+
return getattr(_get_proxy("TgCb", "TgCb"), name)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TgArgsProxy:
|
|
42
|
+
"""Proxy to current API request body; use only inside onApiReq handlers."""
|
|
43
|
+
|
|
44
|
+
def __getattr__(self, name: str) -> Any:
|
|
45
|
+
return getattr(_get_proxy("TgArgs", "TgArgs"), name)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def set_context(TgMsg: TgMsgData | None = None, TgCmd: TgCmdData | None = None,
|
|
49
|
+
TgCb: TgCbData | None = None, TgArgs: TgArgsData | None = None) -> None:
|
|
50
|
+
"""Set thread-local context (used by handlers module)."""
|
|
51
|
+
_local.TgMsg = TgMsg
|
|
52
|
+
_local.TgCmd = TgCmd
|
|
53
|
+
_local.TgCb = TgCb
|
|
54
|
+
_local.TgArgs = TgArgs
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def clear_context() -> None:
|
|
58
|
+
"""Clear thread-local context."""
|
|
59
|
+
_local.TgMsg = None
|
|
60
|
+
_local.TgCmd = None
|
|
61
|
+
_local.TgCb = None
|
|
62
|
+
_local.TgArgs = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# Public proxies (singletons)
|
|
66
|
+
TgMsg = TgMsgProxy()
|
|
67
|
+
TgCmd = TgCmdProxy()
|
|
68
|
+
TgCb = TgCbProxy()
|
|
69
|
+
TgArgs = TgArgsProxy()
|
|
70
|
+
|
|
71
|
+
__all__ = [
|
|
72
|
+
"TgMsgData",
|
|
73
|
+
"TgCmdData",
|
|
74
|
+
"TgCbData",
|
|
75
|
+
"TgArgsData",
|
|
76
|
+
"TgMsg",
|
|
77
|
+
"TgCmd",
|
|
78
|
+
"TgCb",
|
|
79
|
+
"TgArgs",
|
|
80
|
+
"set_context",
|
|
81
|
+
"clear_context",
|
|
82
|
+
]
|
TgrEzLi/requests.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""HTTP client for sending requests to the embedded API server (TReq)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TgrezliRequestError(Exception):
|
|
8
|
+
"""Raised when a TReq request fails (network or HTTP error)."""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TReq:
|
|
12
|
+
"""Fluent client for POST requests to the TgrEzLi API server."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, endpoint: str, host: str = "localhost", port: int = 9999, timeout_sec: float = 30.0) -> None:
|
|
15
|
+
self.endpoint = endpoint if endpoint.startswith("/") else f"/{endpoint}"
|
|
16
|
+
self._host = host
|
|
17
|
+
self._port = port
|
|
18
|
+
self._timeout = timeout_sec
|
|
19
|
+
self._body: dict = {}
|
|
20
|
+
|
|
21
|
+
def host(self, host: str) -> TReq:
|
|
22
|
+
self._host = host
|
|
23
|
+
return self
|
|
24
|
+
|
|
25
|
+
def port(self, port: int) -> TReq:
|
|
26
|
+
self._port = port
|
|
27
|
+
return self
|
|
28
|
+
|
|
29
|
+
def timeout(self, seconds: float) -> TReq:
|
|
30
|
+
self._timeout = seconds
|
|
31
|
+
return self
|
|
32
|
+
|
|
33
|
+
def arg(self, name: str, value: object) -> TReq:
|
|
34
|
+
self._body[name] = value
|
|
35
|
+
return self
|
|
36
|
+
|
|
37
|
+
def body(self, body_dict: dict) -> TReq:
|
|
38
|
+
self._body = dict(body_dict)
|
|
39
|
+
return self
|
|
40
|
+
|
|
41
|
+
def send(self) -> requests.Response:
|
|
42
|
+
url = f"http://{self._host}:{self._port}{self.endpoint}"
|
|
43
|
+
try:
|
|
44
|
+
return requests.post(url, json=self._body, timeout=self._timeout)
|
|
45
|
+
except requests.RequestException as e:
|
|
46
|
+
raise TgrezliRequestError(f"Request to {url} failed: {e}") from e
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
__all__ = ["TReq", "TgrezliRequestError"]
|
TgrEzLi/types.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Type definitions and data structures for TgrEzLi."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
# --- Config ---
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=False)
|
|
11
|
+
class TgrezliConfig:
|
|
12
|
+
"""Runtime configuration for TEL (log, API server)."""
|
|
13
|
+
|
|
14
|
+
save_log: bool = True
|
|
15
|
+
log_file: str = "TgrEzLi.log"
|
|
16
|
+
api_host: str = "localhost"
|
|
17
|
+
api_port: int = 9999
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# --- Handler payloads (internal; passed from core to user via thread-local) ---
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class TgMsgData:
|
|
25
|
+
"""Payload for onMessage handlers."""
|
|
26
|
+
|
|
27
|
+
text: str
|
|
28
|
+
msg_id: int | None
|
|
29
|
+
chat_id: str
|
|
30
|
+
user_id: int | None
|
|
31
|
+
user_name: str | None
|
|
32
|
+
timestamp: Any
|
|
33
|
+
raw_update: Any
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def msgId(self) -> int | None:
|
|
37
|
+
return self.msg_id
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def chatId(self) -> str:
|
|
41
|
+
return self.chat_id
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def userId(self) -> int | None:
|
|
45
|
+
return self.user_id
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def userName(self) -> str | None:
|
|
49
|
+
return self.user_name
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class TgCmdData:
|
|
54
|
+
"""Payload for onCommand handlers."""
|
|
55
|
+
|
|
56
|
+
command: str
|
|
57
|
+
args: str
|
|
58
|
+
msg_id: int | None
|
|
59
|
+
chat_id: str
|
|
60
|
+
user_id: int | None
|
|
61
|
+
user_name: str | None
|
|
62
|
+
timestamp: Any
|
|
63
|
+
raw_update: Any
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def msgId(self) -> int | None:
|
|
67
|
+
return self.msg_id
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def chatId(self) -> str:
|
|
71
|
+
return self.chat_id
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def userId(self) -> int | None:
|
|
75
|
+
return self.user_id
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def userName(self) -> str | None:
|
|
79
|
+
return self.user_name
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class TgCbData:
|
|
84
|
+
"""Payload for onCallback handlers."""
|
|
85
|
+
|
|
86
|
+
text: str | None
|
|
87
|
+
value: str
|
|
88
|
+
msg_id: int | None
|
|
89
|
+
chat_id: str
|
|
90
|
+
user_id: int | None
|
|
91
|
+
user_name: str | None
|
|
92
|
+
timestamp: Any
|
|
93
|
+
raw_update: Any
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def msgId(self) -> int | None:
|
|
97
|
+
return self.msg_id
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def chatId(self) -> str:
|
|
101
|
+
return self.chat_id
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def userId(self) -> int | None:
|
|
105
|
+
return self.user_id
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def userName(self) -> str | None:
|
|
109
|
+
return self.user_name
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass
|
|
113
|
+
class TgArgsData:
|
|
114
|
+
"""Payload for onApiReq handlers (POST body)."""
|
|
115
|
+
|
|
116
|
+
_data: dict[str, Any]
|
|
117
|
+
|
|
118
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
119
|
+
return self._data.get(key, default)
|