zeed 1.2.2__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.
- zeed/__init__.py +74 -0
- zeed/api.py +144 -0
- zeed/bot.py +545 -0
- zeed/conversation.py +233 -0
- zeed/exceptions.py +23 -0
- zeed/filters.py +213 -0
- zeed/fsm/__init__.py +8 -0
- zeed/fsm/context.py +22 -0
- zeed/fsm/state.py +17 -0
- zeed/fsm/storage.py +159 -0
- zeed/keyboards.py +157 -0
- zeed/logger.py +46 -0
- zeed/middleware.py +81 -0
- zeed/security.py +136 -0
- zeed/types.py +294 -0
- zeed/utils.py +81 -0
- zeed/webhook.py +128 -0
- zeed-1.2.2.dist-info/METADATA +220 -0
- zeed-1.2.2.dist-info/RECORD +22 -0
- zeed-1.2.2.dist-info/WHEEL +5 -0
- zeed-1.2.2.dist-info/licenses/LICENSE +21 -0
- zeed-1.2.2.dist-info/top_level.txt +1 -0
zeed/__init__.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from .api import TelegramAPI
|
|
2
|
+
from .bot import Bot
|
|
3
|
+
from .exceptions import (
|
|
4
|
+
TelegramError, GrambotError, SecurityError,
|
|
5
|
+
ValidationError, WebhookError,
|
|
6
|
+
)
|
|
7
|
+
from .logger import get_logger, setup_logging, quiet
|
|
8
|
+
|
|
9
|
+
from .types import (
|
|
10
|
+
CallbackQuery, Chat, InlineQuery, Message, Update, User,
|
|
11
|
+
InlineQueryResultArticle, InlineQueryResultPhoto,
|
|
12
|
+
InlineQueryResultGif,
|
|
13
|
+
)
|
|
14
|
+
from .keyboards import (
|
|
15
|
+
ForceReply, InlineKeyboardBuilder, InlineKeyboardButton,
|
|
16
|
+
InlineKeyboardMarkup, KeyboardButton, RemoveKeyboard,
|
|
17
|
+
ReplyKeyboardMarkup,
|
|
18
|
+
)
|
|
19
|
+
from .filters import (
|
|
20
|
+
BaseFilter, CallbackData, CallbackDataPrefix, ChatId, ChatType,
|
|
21
|
+
Command, ContentTypes, Func, InlineQueryText, Regexp, Text, UserId,
|
|
22
|
+
)
|
|
23
|
+
from .fsm import (
|
|
24
|
+
BaseStorage, FileStorage, FSMContext, MemoryStorage,
|
|
25
|
+
SQLiteStorage, State, StatesGroup,
|
|
26
|
+
)
|
|
27
|
+
from .middleware import (
|
|
28
|
+
BaseMiddleware, LoggingMiddleware, ThrottlingMiddleware,
|
|
29
|
+
TimingMiddleware,
|
|
30
|
+
)
|
|
31
|
+
from .conversation import ConversationContext, ConversationCancelled
|
|
32
|
+
from .webhook import WebhookServer
|
|
33
|
+
from .utils import (
|
|
34
|
+
chunked, escape_html, escape_markdown, safe_text,
|
|
35
|
+
split_message, truncate, user_mention,
|
|
36
|
+
)
|
|
37
|
+
from .security import (
|
|
38
|
+
constant_time_eq, mask_token, redact, safe_filename,
|
|
39
|
+
safe_path, validate_callback_data, validate_text,
|
|
40
|
+
compile_safe_regex, check_size,
|
|
41
|
+
MAX_MESSAGE_LEN, MAX_CAPTION_LEN, MAX_CALLBACK_DATA,
|
|
42
|
+
MAX_UPLOAD_BYTES, MAX_WEBHOOK_BYTES, MAX_REGEX_LEN,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
__version__ = "1.2.2"
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"Bot", "TelegramAPI",
|
|
49
|
+
"GrambotError", "TelegramError", "SecurityError",
|
|
50
|
+
"ValidationError", "WebhookError",
|
|
51
|
+
"Update", "Message", "Chat", "User", "CallbackQuery",
|
|
52
|
+
"InlineQuery", "InlineQueryResultArticle",
|
|
53
|
+
"InlineQueryResultPhoto", "InlineQueryResultGif",
|
|
54
|
+
"InlineKeyboardMarkup", "InlineKeyboardButton",
|
|
55
|
+
"ReplyKeyboardMarkup", "KeyboardButton",
|
|
56
|
+
"ForceReply", "RemoveKeyboard", "InlineKeyboardBuilder",
|
|
57
|
+
"BaseFilter", "Func", "Command", "Text", "ContentTypes",
|
|
58
|
+
"ChatType", "ChatId", "UserId", "Regexp", "CallbackData",
|
|
59
|
+
"CallbackDataPrefix", "InlineQueryText",
|
|
60
|
+
"FSMContext", "State", "StatesGroup",
|
|
61
|
+
"BaseStorage", "MemoryStorage", "FileStorage", "SQLiteStorage",
|
|
62
|
+
"BaseMiddleware", "LoggingMiddleware", "ThrottlingMiddleware",
|
|
63
|
+
"TimingMiddleware",
|
|
64
|
+
"WebhookServer",
|
|
65
|
+
"ConversationContext", "ConversationCancelled",
|
|
66
|
+
"escape_html", "escape_markdown", "safe_text",
|
|
67
|
+
"split_message", "chunked", "user_mention", "truncate",
|
|
68
|
+
"redact", "mask_token", "safe_path", "safe_filename",
|
|
69
|
+
"validate_callback_data", "validate_text",
|
|
70
|
+
"compile_safe_regex", "check_size", "constant_time_eq",
|
|
71
|
+
"MAX_MESSAGE_LEN", "MAX_CAPTION_LEN", "MAX_CALLBACK_DATA",
|
|
72
|
+
"MAX_UPLOAD_BYTES", "MAX_WEBHOOK_BYTES", "MAX_REGEX_LEN",
|
|
73
|
+
"get_logger", "setup_logging", "quiet",
|
|
74
|
+
]
|
zeed/api.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import socket
|
|
3
|
+
import urllib3
|
|
4
|
+
|
|
5
|
+
from .exceptions import TelegramError, SecurityError
|
|
6
|
+
from .security import MAX_UPLOAD_BYTES, safe_filename
|
|
7
|
+
from .logger import get_logger
|
|
8
|
+
|
|
9
|
+
log = get_logger("zeed.api")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _camel(name):
|
|
13
|
+
head, *tail = name.split("_")
|
|
14
|
+
return head + "".join(p.capitalize() for p in tail)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_SOCKET_OPTIONS = [
|
|
18
|
+
(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1),
|
|
19
|
+
(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TelegramAPI:
|
|
24
|
+
def __init__(self, token, pool_size=16, timeout=30.0,
|
|
25
|
+
connect_timeout=5.0, retries=0):
|
|
26
|
+
if not token or not isinstance(token, str):
|
|
27
|
+
raise SecurityError("Token must be a non-empty string")
|
|
28
|
+
if ":" not in token:
|
|
29
|
+
raise SecurityError("Token has invalid format")
|
|
30
|
+
|
|
31
|
+
self.token = token
|
|
32
|
+
self.base_url = f"https://api.telegram.org/bot{token}"
|
|
33
|
+
self.file_url = f"https://api.telegram.org/file/bot{token}"
|
|
34
|
+
|
|
35
|
+
if retries > 0:
|
|
36
|
+
retry_policy = urllib3.Retry(
|
|
37
|
+
retries,
|
|
38
|
+
backoff_factor=0.1,
|
|
39
|
+
status_forcelist=None,
|
|
40
|
+
connect=retries,
|
|
41
|
+
read=0,
|
|
42
|
+
redirect=0,
|
|
43
|
+
raise_on_status=False,
|
|
44
|
+
)
|
|
45
|
+
else:
|
|
46
|
+
retry_policy = False
|
|
47
|
+
|
|
48
|
+
self._http = urllib3.PoolManager(
|
|
49
|
+
num_pools=pool_size,
|
|
50
|
+
maxsize=pool_size,
|
|
51
|
+
block=True,
|
|
52
|
+
timeout=urllib3.Timeout(
|
|
53
|
+
connect=connect_timeout,
|
|
54
|
+
read=timeout,
|
|
55
|
+
),
|
|
56
|
+
retries=retry_policy,
|
|
57
|
+
socket_options=_SOCKET_OPTIONS,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def call(self, method, files=None, **params):
|
|
61
|
+
url = f"{self.base_url}/{_camel(method)}"
|
|
62
|
+
try:
|
|
63
|
+
if files:
|
|
64
|
+
fields = self._build_multipart(files, params)
|
|
65
|
+
resp = self._http.request("POST", url, fields=fields)
|
|
66
|
+
else:
|
|
67
|
+
body = json.dumps(
|
|
68
|
+
params, ensure_ascii=False, separators=(",", ":")
|
|
69
|
+
).encode("utf-8")
|
|
70
|
+
resp = self._http.request(
|
|
71
|
+
"POST", url, body=body,
|
|
72
|
+
headers={"Content-Type": "application/json"},
|
|
73
|
+
)
|
|
74
|
+
except urllib3.exceptions.HTTPError as e:
|
|
75
|
+
log.error("HTTP error calling %s: %s", method, e)
|
|
76
|
+
raise TelegramError(f"HTTP error: {e}") from e
|
|
77
|
+
|
|
78
|
+
return self._parse(resp.data)
|
|
79
|
+
|
|
80
|
+
def stream_file(self, file_path, chunk_size=64 * 1024):
|
|
81
|
+
url = f"{self.file_url}/{file_path}"
|
|
82
|
+
resp = self._http.request("GET", url, preload_content=False)
|
|
83
|
+
try:
|
|
84
|
+
while True:
|
|
85
|
+
chunk = resp.read(chunk_size)
|
|
86
|
+
if not chunk:
|
|
87
|
+
break
|
|
88
|
+
yield chunk
|
|
89
|
+
finally:
|
|
90
|
+
resp.release_conn()
|
|
91
|
+
|
|
92
|
+
def close(self):
|
|
93
|
+
self._http.clear()
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def _parse(raw):
|
|
97
|
+
try:
|
|
98
|
+
payload = json.loads(raw)
|
|
99
|
+
except json.JSONDecodeError:
|
|
100
|
+
raise TelegramError(f"Invalid JSON from Telegram: {raw[:200]!r}")
|
|
101
|
+
if not payload.get("ok"):
|
|
102
|
+
raise TelegramError(
|
|
103
|
+
payload.get("description", "Unknown error"),
|
|
104
|
+
payload.get("error_code"),
|
|
105
|
+
payload,
|
|
106
|
+
)
|
|
107
|
+
return payload["result"]
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def _build_multipart(files, params):
|
|
111
|
+
fields = {}
|
|
112
|
+
for k, v in params.items():
|
|
113
|
+
if v is None:
|
|
114
|
+
continue
|
|
115
|
+
if isinstance(v, (dict, list)):
|
|
116
|
+
fields[k] = json.dumps(v, ensure_ascii=False)
|
|
117
|
+
elif isinstance(v, bool):
|
|
118
|
+
fields[k] = "true" if v else "false"
|
|
119
|
+
else:
|
|
120
|
+
fields[k] = str(v)
|
|
121
|
+
|
|
122
|
+
for k, v in files.items():
|
|
123
|
+
if hasattr(v, "read"):
|
|
124
|
+
data = v.read()
|
|
125
|
+
if isinstance(data, str):
|
|
126
|
+
data = data.encode()
|
|
127
|
+
if len(data) > MAX_UPLOAD_BYTES:
|
|
128
|
+
raise SecurityError(f"File too large: {len(data)} bytes")
|
|
129
|
+
name = safe_filename(getattr(v, "name", k))
|
|
130
|
+
fields[k] = (name, data, "application/octet-stream")
|
|
131
|
+
elif isinstance(v, (bytes, bytearray)):
|
|
132
|
+
if len(v) > MAX_UPLOAD_BYTES:
|
|
133
|
+
raise SecurityError(f"File too large: {len(v)} bytes")
|
|
134
|
+
fields[k] = (f"{safe_filename(k)}.bin", bytes(v),
|
|
135
|
+
"application/octet-stream")
|
|
136
|
+
elif isinstance(v, str) and v.startswith("https://"):
|
|
137
|
+
fields[k] = v
|
|
138
|
+
elif isinstance(v, str) and v.startswith("http://"):
|
|
139
|
+
raise SecurityError("Only HTTPS URLs allowed")
|
|
140
|
+
else:
|
|
141
|
+
raise SecurityError(
|
|
142
|
+
f"Unsupported file for {k!r}: {type(v).__name__}"
|
|
143
|
+
)
|
|
144
|
+
return fields
|