styledbots 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.
- styledbots/__init__.py +38 -0
- styledbots/core/__init__.py +4 -0
- styledbots/core/api.py +184 -0
- styledbots/core/bot.py +270 -0
- styledbots/core/exceptions.py +26 -0
- styledbots/core/types.py +107 -0
- styledbots/filters/__init__.py +1 -0
- styledbots/filters/filters.py +88 -0
- styledbots/handlers/__init__.py +2 -0
- styledbots/keyboards/__init__.py +3 -0
- styledbots/keyboards/builder.py +28 -0
- styledbots/keyboards/inline.py +98 -0
- styledbots/keyboards/reply.py +75 -0
- styledbots/storage/__init__.py +3 -0
- styledbots/storage/base.py +23 -0
- styledbots/storage/file.py +46 -0
- styledbots/storage/memory.py +27 -0
- styledbots/styles/__init__.py +3 -0
- styledbots/styles/formatter.py +40 -0
- styledbots/styles/style.py +57 -0
- styledbots/styles/templates.py +36 -0
- styledbots/utils/__init__.py +1 -0
- styledbots/utils/helpers.py +26 -0
- styledbots/version.py +3 -0
- styledbots-1.0.0.dist-info/METADATA +144 -0
- styledbots-1.0.0.dist-info/RECORD +29 -0
- styledbots-1.0.0.dist-info/WHEEL +5 -0
- styledbots-1.0.0.dist-info/licenses/LICENSE +21 -0
- styledbots-1.0.0.dist-info/top_level.txt +1 -0
styledbots/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
StyledBots - Modern, clean & powerful Telegram Bot library
|
|
3
|
+
Better than / equal to Telebot • Perfect for Pydroid 3
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .core.bot import Bot
|
|
7
|
+
from .core.exceptions import (
|
|
8
|
+
StyledBotsError,
|
|
9
|
+
TelegramAPIError,
|
|
10
|
+
NetworkError,
|
|
11
|
+
InvalidTokenError,
|
|
12
|
+
)
|
|
13
|
+
from .core.types import Message, User, Chat, CallbackQuery, Update
|
|
14
|
+
from .styles.style import Style
|
|
15
|
+
from .keyboards.inline import InlineKeyboard
|
|
16
|
+
from .keyboards.reply import ReplyKeyboard
|
|
17
|
+
from .keyboards.builder import KeyboardBuilder
|
|
18
|
+
from .filters.filters import filters
|
|
19
|
+
from .version import __version__
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"Bot",
|
|
23
|
+
"Style",
|
|
24
|
+
"InlineKeyboard",
|
|
25
|
+
"ReplyKeyboard",
|
|
26
|
+
"KeyboardBuilder",
|
|
27
|
+
"filters",
|
|
28
|
+
"Message",
|
|
29
|
+
"User",
|
|
30
|
+
"Chat",
|
|
31
|
+
"CallbackQuery",
|
|
32
|
+
"Update",
|
|
33
|
+
"StyledBotsError",
|
|
34
|
+
"TelegramAPIError",
|
|
35
|
+
"NetworkError",
|
|
36
|
+
"InvalidTokenError",
|
|
37
|
+
"__version__",
|
|
38
|
+
]
|
styledbots/core/api.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from typing import Optional, Dict, Any, List, Union
|
|
3
|
+
from .exceptions import TelegramAPIError, NetworkError, InvalidTokenError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TelegramAPI:
|
|
7
|
+
"""Low-level Telegram Bot API client"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, token: str, timeout: int = 30):
|
|
10
|
+
self.token = token
|
|
11
|
+
self.base_url = f"https://api.telegram.org/bot{token}/"
|
|
12
|
+
self.timeout = timeout
|
|
13
|
+
self.session = requests.Session()
|
|
14
|
+
|
|
15
|
+
def _request(self, method: str, params: dict = None, files: dict = None) -> dict:
|
|
16
|
+
url = self.base_url + method
|
|
17
|
+
try:
|
|
18
|
+
if files:
|
|
19
|
+
response = self.session.post(url, data=params, files=files, timeout=self.timeout)
|
|
20
|
+
else:
|
|
21
|
+
response = self.session.post(url, json=params or {}, timeout=self.timeout)
|
|
22
|
+
|
|
23
|
+
data = response.json()
|
|
24
|
+
|
|
25
|
+
if not data.get("ok"):
|
|
26
|
+
description = data.get("description", "Unknown error")
|
|
27
|
+
error_code = data.get("error_code")
|
|
28
|
+
if error_code == 401:
|
|
29
|
+
raise InvalidTokenError(description)
|
|
30
|
+
raise TelegramAPIError(description, error_code)
|
|
31
|
+
|
|
32
|
+
return data.get("result")
|
|
33
|
+
|
|
34
|
+
except requests.exceptions.RequestException as e:
|
|
35
|
+
raise NetworkError(f"Network error: {e}")
|
|
36
|
+
|
|
37
|
+
# ===== Core methods =====
|
|
38
|
+
|
|
39
|
+
def get_me(self) -> dict:
|
|
40
|
+
return self._request("getMe")
|
|
41
|
+
|
|
42
|
+
def get_updates(self, offset: int = None, timeout: int = 25, limit_types: list = None) -> list:
|
|
43
|
+
params = {"timeout": timeout}
|
|
44
|
+
if offset is not None:
|
|
45
|
+
params["offset"] = offset
|
|
46
|
+
if allowed_updates:
|
|
47
|
+
params["allowed_updates"] = allowed_updates
|
|
48
|
+
return self._request("getUpdates", params) or []
|
|
49
|
+
|
|
50
|
+
def send_message(
|
|
51
|
+
self,
|
|
52
|
+
chat_id: Union[int, str],
|
|
53
|
+
text: str,
|
|
54
|
+
parse_mode: str = "HTML",
|
|
55
|
+
reply_markup: dict = None,
|
|
56
|
+
reply_to_message_id: int = None,
|
|
57
|
+
disable_web_page_preview: bool = False,
|
|
58
|
+
disable_notification: bool = False,
|
|
59
|
+
**kwargs
|
|
60
|
+
) -> dict:
|
|
61
|
+
params = {
|
|
62
|
+
"chat_id": chat_id,
|
|
63
|
+
"text": text,
|
|
64
|
+
"parse_mode": parse_mode,
|
|
65
|
+
"disable_web_page_preview": disable_web_page_preview,
|
|
66
|
+
"disable_notification": disable_notification,
|
|
67
|
+
**kwargs
|
|
68
|
+
}
|
|
69
|
+
if reply_markup:
|
|
70
|
+
params["reply_markup"] = reply_markup
|
|
71
|
+
if reply_to_message_id:
|
|
72
|
+
params["reply_to_message_id"] = reply_to_message_id
|
|
73
|
+
return self._request("sendMessage", params)
|
|
74
|
+
|
|
75
|
+
def edit_message_text(
|
|
76
|
+
self,
|
|
77
|
+
text: str,
|
|
78
|
+
chat_id: Union[int, str] = None,
|
|
79
|
+
message_id: int = None,
|
|
80
|
+
inline_message_id: str = None,
|
|
81
|
+
parse_mode: str = "HTML",
|
|
82
|
+
reply_markup: dict = None,
|
|
83
|
+
**kwargs
|
|
84
|
+
) -> dict:
|
|
85
|
+
params = {
|
|
86
|
+
"text": text,
|
|
87
|
+
"parse_mode": parse_mode,
|
|
88
|
+
**kwargs
|
|
89
|
+
}
|
|
90
|
+
if chat_id:
|
|
91
|
+
params["chat_id"] = chat_id
|
|
92
|
+
if message_id:
|
|
93
|
+
params["message_id"] = message_id
|
|
94
|
+
if inline_message_id:
|
|
95
|
+
params["inline_message_id"] = inline_message_id
|
|
96
|
+
if reply_markup:
|
|
97
|
+
params["reply_markup"] = reply_markup
|
|
98
|
+
return self._request("editMessageText", params)
|
|
99
|
+
|
|
100
|
+
def delete_message(self, chat_id: Union[int, str], message_id: int) -> bool:
|
|
101
|
+
return self._request("deleteMessage", {
|
|
102
|
+
"chat_id": chat_id,
|
|
103
|
+
"message_id": message_id
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
def answer_callback_query(
|
|
107
|
+
self,
|
|
108
|
+
callback_query_id: str,
|
|
109
|
+
text: str = None,
|
|
110
|
+
show_alert: bool = False,
|
|
111
|
+
url: str = None,
|
|
112
|
+
cache_time: int = 0
|
|
113
|
+
) -> bool:
|
|
114
|
+
params = {
|
|
115
|
+
"callback_query_id": callback_query_id,
|
|
116
|
+
"show_alert": show_alert,
|
|
117
|
+
"cache_time": cache_time
|
|
118
|
+
}
|
|
119
|
+
if text:
|
|
120
|
+
params["text"] = text
|
|
121
|
+
if url:
|
|
122
|
+
params["url"] = url
|
|
123
|
+
return self._request("answerCallbackQuery", params)
|
|
124
|
+
|
|
125
|
+
def send_photo(
|
|
126
|
+
self,
|
|
127
|
+
chat_id: Union[int, str],
|
|
128
|
+
photo: Union[str, bytes],
|
|
129
|
+
caption: str = None,
|
|
130
|
+
parse_mode: str = "HTML",
|
|
131
|
+
reply_markup: dict = None,
|
|
132
|
+
**kwargs
|
|
133
|
+
) -> dict:
|
|
134
|
+
params = {
|
|
135
|
+
"chat_id": chat_id,
|
|
136
|
+
"parse_mode": parse_mode,
|
|
137
|
+
**kwargs
|
|
138
|
+
}
|
|
139
|
+
if caption:
|
|
140
|
+
params["caption"] = caption
|
|
141
|
+
if reply_markup:
|
|
142
|
+
params["reply_markup"] = reply_markup
|
|
143
|
+
|
|
144
|
+
if isinstance(photo, str) and (photo.startswith("http") or photo.startswith("AgAC")):
|
|
145
|
+
params["photo"] = photo
|
|
146
|
+
return self._request("sendPhoto", params)
|
|
147
|
+
else:
|
|
148
|
+
files = {"photo": photo}
|
|
149
|
+
return self._request("sendPhoto", params, files=files)
|
|
150
|
+
|
|
151
|
+
def send_document(
|
|
152
|
+
self,
|
|
153
|
+
chat_id: Union[int, str],
|
|
154
|
+
document: Union[str, bytes],
|
|
155
|
+
caption: str = None,
|
|
156
|
+
parse_mode: str = "HTML",
|
|
157
|
+
reply_markup: dict = None,
|
|
158
|
+
**kwargs
|
|
159
|
+
) -> dict:
|
|
160
|
+
params = {
|
|
161
|
+
"chat_id": chat_id,
|
|
162
|
+
"parse_mode": parse_mode,
|
|
163
|
+
**kwargs
|
|
164
|
+
}
|
|
165
|
+
if caption:
|
|
166
|
+
params["caption"] = caption
|
|
167
|
+
if reply_markup:
|
|
168
|
+
params["reply_markup"] = reply_markup
|
|
169
|
+
|
|
170
|
+
if isinstance(document, str) and not document.startswith("/"):
|
|
171
|
+
params["document"] = document
|
|
172
|
+
return self._request("sendDocument", params)
|
|
173
|
+
else:
|
|
174
|
+
files = {"document": document}
|
|
175
|
+
return self._request("sendDocument", params, files=files)
|
|
176
|
+
|
|
177
|
+
def get_chat(self, chat_id: Union[int, str]) -> dict:
|
|
178
|
+
return self._request("getChat", {"chat_id": chat_id})
|
|
179
|
+
|
|
180
|
+
def get_chat_member(self, chat_id: Union[int, str], user_id: int) -> dict:
|
|
181
|
+
return self._request("getChatMember", {
|
|
182
|
+
"chat_id": chat_id,
|
|
183
|
+
"user_id": user_id
|
|
184
|
+
})
|
styledbots/core/bot.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import threading
|
|
3
|
+
from typing import Callable, Optional, List, Dict, Any, Union
|
|
4
|
+
from .api import TelegramAPI
|
|
5
|
+
from .types import Update, Message, CallbackQuery
|
|
6
|
+
from .exceptions import StyledBotsError, HandlerError
|
|
7
|
+
from ..filters.filters import filters as Filters
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Bot:
|
|
11
|
+
"""
|
|
12
|
+
Main Bot class - Clean, powerful, Telebot-compatible API
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, token: str, parse_mode: str = "HTML", threaded: bool = True):
|
|
16
|
+
if not token or ":" not in token:
|
|
17
|
+
raise ValueError("Invalid bot token")
|
|
18
|
+
|
|
19
|
+
self.token = token
|
|
20
|
+
self.parse_mode = parse_mode
|
|
21
|
+
self.api = TelegramAPI(token)
|
|
22
|
+
self.threaded = threaded
|
|
23
|
+
|
|
24
|
+
# Handlers storage
|
|
25
|
+
self.message_handlers: List[dict] = []
|
|
26
|
+
self.callback_handlers: List[dict] = []
|
|
27
|
+
self.command_handlers: Dict[str, Callable] = {}
|
|
28
|
+
self.middlewares: List[Callable] = []
|
|
29
|
+
|
|
30
|
+
# State
|
|
31
|
+
self._offset = 0
|
|
32
|
+
self._running = False
|
|
33
|
+
self._stop_event = threading.Event()
|
|
34
|
+
|
|
35
|
+
# Bot info
|
|
36
|
+
self.me = None
|
|
37
|
+
try:
|
|
38
|
+
self.me = self.api.get_me()
|
|
39
|
+
print(f"✅ StyledBots started as @{self.me.get('username')}")
|
|
40
|
+
except Exception as e:
|
|
41
|
+
print(f"⚠️ Warning: Could not get bot info: {e}")
|
|
42
|
+
|
|
43
|
+
# ==================== Decorators ====================
|
|
44
|
+
|
|
45
|
+
def message_handler(self, commands: List[str] = None, regexp: str = None,
|
|
46
|
+
func: Callable = None, content_types: List[str] = None,
|
|
47
|
+
chat_types: List[str] = None, **kwargs):
|
|
48
|
+
"""
|
|
49
|
+
Decorator for message handlers (Telebot compatible + better)
|
|
50
|
+
"""
|
|
51
|
+
def decorator(handler: Callable):
|
|
52
|
+
filters_list = []
|
|
53
|
+
|
|
54
|
+
if commands:
|
|
55
|
+
filters_list.append(Filters.command(commands))
|
|
56
|
+
if regexp:
|
|
57
|
+
filters_list.append(Filters.regexp(regexp))
|
|
58
|
+
if content_types:
|
|
59
|
+
filters_list.append(Filters.content_type(content_types))
|
|
60
|
+
if chat_types:
|
|
61
|
+
filters_list.append(Filters.chat_type(chat_types))
|
|
62
|
+
if func:
|
|
63
|
+
filters_list.append(Filters.func(func))
|
|
64
|
+
|
|
65
|
+
self.message_handlers.append({
|
|
66
|
+
"handler": handler,
|
|
67
|
+
"filters": filters_list,
|
|
68
|
+
"kwargs": kwargs
|
|
69
|
+
})
|
|
70
|
+
return handler
|
|
71
|
+
return decorator
|
|
72
|
+
|
|
73
|
+
def callback_query_handler(self, func: Callable = None, **kwargs):
|
|
74
|
+
"""Decorator for callback query handlers"""
|
|
75
|
+
def decorator(handler: Callable):
|
|
76
|
+
filters_list = []
|
|
77
|
+
if func:
|
|
78
|
+
filters_list.append(Filters.func(func))
|
|
79
|
+
|
|
80
|
+
self.callback_handlers.append({
|
|
81
|
+
"handler": handler,
|
|
82
|
+
"filters": filters_list,
|
|
83
|
+
"kwargs": kwargs
|
|
84
|
+
})
|
|
85
|
+
return handler
|
|
86
|
+
return decorator
|
|
87
|
+
|
|
88
|
+
def command(self, *commands: str):
|
|
89
|
+
"""Simple command decorator: @bot.command("start", "help")"""
|
|
90
|
+
def decorator(handler: Callable):
|
|
91
|
+
for cmd in commands:
|
|
92
|
+
self.command_handlers[cmd.lower()] = handler
|
|
93
|
+
return handler
|
|
94
|
+
return decorator
|
|
95
|
+
|
|
96
|
+
def middleware_handler(self, update_types: List[str] = None):
|
|
97
|
+
"""Register middleware"""
|
|
98
|
+
def decorator(handler: Callable):
|
|
99
|
+
self.middlewares.append(handler)
|
|
100
|
+
return handler
|
|
101
|
+
return decorator
|
|
102
|
+
|
|
103
|
+
# ==================== Sending methods ====================
|
|
104
|
+
|
|
105
|
+
def send_message(self, chat_id: Union[int, str], text: str,
|
|
106
|
+
parse_mode: str = None, reply_markup=None,
|
|
107
|
+
reply_to_message_id: int = None, **kwargs):
|
|
108
|
+
if hasattr(reply_markup, "to_dict"):
|
|
109
|
+
reply_markup = reply_markup.to_dict()
|
|
110
|
+
return self.api.send_message(
|
|
111
|
+
chat_id=chat_id,
|
|
112
|
+
text=text,
|
|
113
|
+
parse_mode=parse_mode or self.parse_mode,
|
|
114
|
+
reply_markup=reply_markup,
|
|
115
|
+
reply_to_message_id=reply_to_message_id,
|
|
116
|
+
**kwargs
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def reply_to(self, message: Message, text: str, **kwargs):
|
|
120
|
+
return self.send_message(
|
|
121
|
+
chat_id=message.chat.id,
|
|
122
|
+
text=text,
|
|
123
|
+
reply_to_message_id=message.message_id,
|
|
124
|
+
**kwargs
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def edit_message_text(self, text: str, chat_id=None, message_id=None,
|
|
128
|
+
reply_markup=None, **kwargs):
|
|
129
|
+
if hasattr(reply_markup, "to_dict"):
|
|
130
|
+
reply_markup = reply_markup.to_dict()
|
|
131
|
+
return self.api.edit_message_text(
|
|
132
|
+
text=text,
|
|
133
|
+
chat_id=chat_id,
|
|
134
|
+
message_id=message_id,
|
|
135
|
+
reply_markup=reply_markup,
|
|
136
|
+
parse_mode=kwargs.pop("parse_mode", self.parse_mode),
|
|
137
|
+
**kwargs
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def delete_message(self, chat_id, message_id):
|
|
141
|
+
return self.api.delete_message(chat_id, message_id)
|
|
142
|
+
|
|
143
|
+
def answer_callback_query(self, callback_query_id: str, text: str = None,
|
|
144
|
+
show_alert: bool = False, **kwargs):
|
|
145
|
+
return self.api.answer_callback_query(
|
|
146
|
+
callback_query_id, text=text, show_alert=show_alert, **kwargs
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def send_photo(self, chat_id, photo, caption=None, reply_markup=None, **kwargs):
|
|
150
|
+
if hasattr(reply_markup, "to_dict"):
|
|
151
|
+
reply_markup = reply_markup.to_dict()
|
|
152
|
+
return self.api.send_photo(
|
|
153
|
+
chat_id, photo, caption=caption,
|
|
154
|
+
reply_markup=reply_markup,
|
|
155
|
+
parse_mode=kwargs.pop("parse_mode", self.parse_mode),
|
|
156
|
+
**kwargs
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def send_document(self, chat_id, document, caption=None, reply_markup=None, **kwargs):
|
|
160
|
+
if hasattr(reply_markup, "to_dict"):
|
|
161
|
+
reply_markup = reply_markup.to_dict()
|
|
162
|
+
return self.api.send_document(
|
|
163
|
+
chat_id, document, caption=caption,
|
|
164
|
+
reply_markup=reply_markup,
|
|
165
|
+
parse_mode=kwargs.pop("parse_mode", self.parse_mode),
|
|
166
|
+
**kwargs
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# ==================== Processing ====================
|
|
170
|
+
|
|
171
|
+
def _check_filters(self, filters_list: list, obj) -> bool:
|
|
172
|
+
if not filters_list:
|
|
173
|
+
return True
|
|
174
|
+
return all(f(obj) for f in filters_list)
|
|
175
|
+
|
|
176
|
+
def process_new_updates(self, updates: list):
|
|
177
|
+
for update_data in updates:
|
|
178
|
+
try:
|
|
179
|
+
update = Update(update_data, bot=self)
|
|
180
|
+
|
|
181
|
+
# Middlewares
|
|
182
|
+
for mw in self.middlewares:
|
|
183
|
+
try:
|
|
184
|
+
mw(update)
|
|
185
|
+
except Exception as e:
|
|
186
|
+
print(f"Middleware error: {e}")
|
|
187
|
+
|
|
188
|
+
# Commands (fast path)
|
|
189
|
+
if update.message and update.message.text and update.message.text.startswith("/"):
|
|
190
|
+
cmd = update.message.text.split()[0][1:].split("@")[0].lower()
|
|
191
|
+
if cmd in self.command_handlers:
|
|
192
|
+
try:
|
|
193
|
+
self.command_handlers[cmd](update.message)
|
|
194
|
+
except Exception as e:
|
|
195
|
+
print(f"Command handler error: {e}")
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
# Message handlers
|
|
199
|
+
if update.message:
|
|
200
|
+
for h in self.message_handlers:
|
|
201
|
+
if self._check_filters(h["filters"], update.message):
|
|
202
|
+
try:
|
|
203
|
+
h["handler"](update.message)
|
|
204
|
+
except Exception as e:
|
|
205
|
+
print(f"Message handler error: {e}")
|
|
206
|
+
break
|
|
207
|
+
|
|
208
|
+
# Callback handlers
|
|
209
|
+
if update.callback_query:
|
|
210
|
+
for h in self.callback_handlers:
|
|
211
|
+
if self._check_filters(h["filters"], update.callback_query):
|
|
212
|
+
try:
|
|
213
|
+
h["handler"](update.callback_query)
|
|
214
|
+
except Exception as e:
|
|
215
|
+
print(f"Callback handler error: {e}")
|
|
216
|
+
break
|
|
217
|
+
|
|
218
|
+
except Exception as e:
|
|
219
|
+
print(f"Update processing error: {e}")
|
|
220
|
+
|
|
221
|
+
# ==================== Polling ====================
|
|
222
|
+
|
|
223
|
+
def infinity_polling(self, timeout: int = 25, skip_pending: bool = True,
|
|
224
|
+
allowed_updates: list = None, **kwargs):
|
|
225
|
+
"""Start infinite polling (Telebot style)"""
|
|
226
|
+
print("🚀 StyledBots is polling...")
|
|
227
|
+
self._running = True
|
|
228
|
+
self._stop_event.clear()
|
|
229
|
+
|
|
230
|
+
if skip_pending:
|
|
231
|
+
# Skip old updates
|
|
232
|
+
try:
|
|
233
|
+
updates = self.api.get_updates(timeout=1)
|
|
234
|
+
if updates:
|
|
235
|
+
self._offset = updates[-1]["update_id"] + 1
|
|
236
|
+
except Exception:
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
while self._running and not self._stop_event.is_set():
|
|
240
|
+
try:
|
|
241
|
+
updates = self.api.get_updates(
|
|
242
|
+
offset=self._offset,
|
|
243
|
+
timeout=timeout,
|
|
244
|
+
allowed_updates=allowed_updates
|
|
245
|
+
)
|
|
246
|
+
if updates:
|
|
247
|
+
self._offset = updates[-1]["update_id"] + 1
|
|
248
|
+
self.process_new_updates(updates)
|
|
249
|
+
except Exception as e:
|
|
250
|
+
print(f"Polling error: {e}")
|
|
251
|
+
time.sleep(3)
|
|
252
|
+
|
|
253
|
+
print("🛑 Polling stopped")
|
|
254
|
+
|
|
255
|
+
def stop_polling(self):
|
|
256
|
+
self._running = False
|
|
257
|
+
self._stop_event.set()
|
|
258
|
+
|
|
259
|
+
def polling(self, non_stop: bool = True, **kwargs):
|
|
260
|
+
"""Alias for infinity_polling"""
|
|
261
|
+
self.infinity_polling(**kwargs)
|
|
262
|
+
|
|
263
|
+
# ==================== Helpers ====================
|
|
264
|
+
|
|
265
|
+
def get_me(self):
|
|
266
|
+
return self.api.get_me()
|
|
267
|
+
|
|
268
|
+
def __repr__(self):
|
|
269
|
+
username = self.me.get("username") if self.me else "Unknown"
|
|
270
|
+
return f"<StyledBots Bot @{username}>"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class StyledBotsError(Exception):
|
|
2
|
+
"""Base exception for StyledBots"""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TelegramAPIError(StyledBotsError):
|
|
7
|
+
"""Raised when Telegram API returns an error"""
|
|
8
|
+
def __init__(self, description: str, error_code: int = None):
|
|
9
|
+
self.description = description
|
|
10
|
+
self.error_code = error_code
|
|
11
|
+
super().__init__(f"Telegram API Error [{error_code}]: {description}")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NetworkError(StyledBotsError):
|
|
15
|
+
"""Raised on network / connection problems"""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class InvalidTokenError(StyledBotsError):
|
|
20
|
+
"""Raised when bot token is invalid"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class HandlerError(StyledBotsError):
|
|
25
|
+
"""Raised when a handler fails"""
|
|
26
|
+
pass
|
styledbots/core/types.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from typing import Optional, Dict, Any, List
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class User:
|
|
5
|
+
def __init__(self, data: dict):
|
|
6
|
+
self.id: int = data.get("id")
|
|
7
|
+
self.is_bot: bool = data.get("is_bot", False)
|
|
8
|
+
self.first_name: str = data.get("first_name", "")
|
|
9
|
+
self.last_name: Optional[str] = data.get("last_name")
|
|
10
|
+
self.username: Optional[str] = data.get("username")
|
|
11
|
+
self.language_code: Optional[str] = data.get("language_code")
|
|
12
|
+
self.is_premium: bool = data.get("is_premium", False)
|
|
13
|
+
self._raw = data
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def full_name(self) -> str:
|
|
17
|
+
if self.last_name:
|
|
18
|
+
return f"{self.first_name} {self.last_name}"
|
|
19
|
+
return self.first_name
|
|
20
|
+
|
|
21
|
+
def __repr__(self):
|
|
22
|
+
return f"<User {self.id} @{self.username or self.first_name}>"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Chat:
|
|
26
|
+
def __init__(self, data: dict):
|
|
27
|
+
self.id: int = data.get("id")
|
|
28
|
+
self.type: str = data.get("type", "private")
|
|
29
|
+
self.title: Optional[str] = data.get("title")
|
|
30
|
+
self.username: Optional[str] = data.get("username")
|
|
31
|
+
self.first_name: Optional[str] = data.get("first_name")
|
|
32
|
+
self.last_name: Optional[str] = data.get("last_name")
|
|
33
|
+
self._raw = data
|
|
34
|
+
|
|
35
|
+
def __repr__(self):
|
|
36
|
+
return f"<Chat {self.id} ({self.type})>"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Message:
|
|
40
|
+
def __init__(self, data: dict, bot=None):
|
|
41
|
+
self.message_id: int = data.get("message_id")
|
|
42
|
+
self.date: int = data.get("date")
|
|
43
|
+
self.chat = Chat(data.get("chat", {}))
|
|
44
|
+
self.from_user = User(data["from"]) if data.get("from") else None
|
|
45
|
+
self.text: Optional[str] = data.get("text")
|
|
46
|
+
self.caption: Optional[str] = data.get("caption")
|
|
47
|
+
self.content_type: str = self._detect_content_type(data)
|
|
48
|
+
self.reply_to_message = Message(data["reply_to_message"], bot) if data.get("reply_to_message") else None
|
|
49
|
+
self._raw = data
|
|
50
|
+
self._bot = bot
|
|
51
|
+
|
|
52
|
+
def _detect_content_type(self, data: dict) -> str:
|
|
53
|
+
for key in ("text", "photo", "video", "document", "audio", "voice",
|
|
54
|
+
"sticker", "animation", "video_note", "contact", "location"):
|
|
55
|
+
if key in data:
|
|
56
|
+
return key
|
|
57
|
+
return "unknown"
|
|
58
|
+
|
|
59
|
+
def reply(self, text: str, **kwargs):
|
|
60
|
+
"""Shortcut: reply to this message"""
|
|
61
|
+
if self._bot:
|
|
62
|
+
return self._bot.send_message(
|
|
63
|
+
self.chat.id,
|
|
64
|
+
text,
|
|
65
|
+
reply_to_message_id=self.message_id,
|
|
66
|
+
**kwargs
|
|
67
|
+
)
|
|
68
|
+
raise RuntimeError("Bot instance not attached")
|
|
69
|
+
|
|
70
|
+
def __repr__(self):
|
|
71
|
+
return f"<Message {self.message_id} from {self.from_user}>"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class CallbackQuery:
|
|
75
|
+
def __init__(self, data: dict, bot=None):
|
|
76
|
+
self.id: str = data.get("id")
|
|
77
|
+
self.from_user = User(data.get("from", {}))
|
|
78
|
+
self.data: Optional[str] = data.get("data")
|
|
79
|
+
self.message = Message(data["message"], bot) if data.get("message") else None
|
|
80
|
+
self.chat_instance: Optional[str] = data.get("chat_instance")
|
|
81
|
+
self._raw = data
|
|
82
|
+
self._bot = bot
|
|
83
|
+
|
|
84
|
+
def answer(self, text: str = None, show_alert: bool = False, **kwargs):
|
|
85
|
+
if self._bot:
|
|
86
|
+
return self._bot.answer_callback_query(
|
|
87
|
+
self.id,
|
|
88
|
+
text=text,
|
|
89
|
+
show_alert=show_alert,
|
|
90
|
+
**kwargs
|
|
91
|
+
)
|
|
92
|
+
raise RuntimeError("Bot instance not attached")
|
|
93
|
+
|
|
94
|
+
def __repr__(self):
|
|
95
|
+
return f"<CallbackQuery {self.id} data={self.data}>"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class Update:
|
|
99
|
+
def __init__(self, data: dict, bot=None):
|
|
100
|
+
self.update_id: int = data.get("update_id")
|
|
101
|
+
self.message = Message(data["message"], bot) if data.get("message") else None
|
|
102
|
+
self.edited_message = Message(data["edited_message"], bot) if data.get("edited_message") else None
|
|
103
|
+
self.callback_query = CallbackQuery(data["callback_query"], bot) if data.get("callback_query") else None
|
|
104
|
+
self._raw = data
|
|
105
|
+
|
|
106
|
+
def __repr__(self):
|
|
107
|
+
return f"<Update {self.update_id}>"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .filters import filters
|