kernelbot 0.0.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.
- kernelbot/__init__.py +13 -0
- kernelbot/apihelper.py +131 -0
- kernelbot/bot.py +237 -0
- kernelbot/types.py +85 -0
- kernelbot/util.py +27 -0
- kernelbot/version.py +1 -0
- kernelbot-0.0.2.dist-info/METADATA +293 -0
- kernelbot-0.0.2.dist-info/RECORD +11 -0
- kernelbot-0.0.2.dist-info/WHEEL +5 -0
- kernelbot-0.0.2.dist-info/licenses/LICENSE +21 -0
- kernelbot-0.0.2.dist-info/top_level.txt +1 -0
kernelbot/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""kernelbot — KernelGram Bot API library, telebot-compatible."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from .version import __version__
|
|
6
|
+
from .bot import TeleBot
|
|
7
|
+
from . import apihelper
|
|
8
|
+
from . import types
|
|
9
|
+
from . import util
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("kernelbot")
|
|
12
|
+
|
|
13
|
+
__all__ = ["TeleBot", "apihelper", "types", "util", "__version__"]
|
kernelbot/apihelper.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Low-level HTTP layer for the KernelGram Bot API.
|
|
2
|
+
|
|
3
|
+
Mirrors the public surface of `telebot.apihelper`.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import socket
|
|
8
|
+
import requests
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from .version import __version__
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("kernelbot")
|
|
14
|
+
|
|
15
|
+
# --- KernelGram endpoints (hardcoded) ---
|
|
16
|
+
API_HOST = "https://api.kernelgram.club"
|
|
17
|
+
API_URL = API_HOST + "/bot{0}/{1}"
|
|
18
|
+
FILE_URL = API_HOST + "/file/bot{0}/{1}"
|
|
19
|
+
|
|
20
|
+
# --- Behaviour flags ---
|
|
21
|
+
CONNECT_TIMEOUT = 3.5
|
|
22
|
+
READ_TIMEOUT = 999
|
|
23
|
+
SESSION_TIME_TO_LIVE = None
|
|
24
|
+
RETRY_ON_ERROR = False
|
|
25
|
+
MAX_RETRIES = 3
|
|
26
|
+
|
|
27
|
+
FORCE_IPV4 = True
|
|
28
|
+
CUSTOM_REQUEST_SENDER = None
|
|
29
|
+
proxy = None
|
|
30
|
+
|
|
31
|
+
# Поля, которые при multipart-запросе KernelGram ожидает как JSON-строку.
|
|
32
|
+
_JSON_FIELDS = ("reply_markup",)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _build_session():
|
|
36
|
+
session = requests.Session()
|
|
37
|
+
if FORCE_IPV4:
|
|
38
|
+
from requests.adapters import HTTPAdapter
|
|
39
|
+
from urllib3.util import connection
|
|
40
|
+
|
|
41
|
+
def _ipv4_only(address, *args, **kwargs):
|
|
42
|
+
host, port = address
|
|
43
|
+
infos = socket.getaddrinfo(host, port, socket.AF_INET, socket.SOCK_STREAM)
|
|
44
|
+
family, socktype, proto, _canon, sockaddr = infos[0]
|
|
45
|
+
sock = socket.socket(family, socktype, proto)
|
|
46
|
+
try:
|
|
47
|
+
sock.connect(sockaddr)
|
|
48
|
+
except Exception:
|
|
49
|
+
sock.close()
|
|
50
|
+
raise
|
|
51
|
+
return sock
|
|
52
|
+
|
|
53
|
+
connection.create_connection = _ipv4_only
|
|
54
|
+
|
|
55
|
+
adapter = HTTPAdapter(max_retries=MAX_RETRIES)
|
|
56
|
+
session.mount("https://", adapter)
|
|
57
|
+
session.mount("http://", adapter)
|
|
58
|
+
return session
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
_session = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _get_session():
|
|
65
|
+
global _session
|
|
66
|
+
if _session is None:
|
|
67
|
+
_session = _build_session()
|
|
68
|
+
return _session
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _serialize_for_multipart(params):
|
|
72
|
+
"""Привести dict-параметры к JSON-строкам для multipart-запроса."""
|
|
73
|
+
out = dict(params)
|
|
74
|
+
for key in _JSON_FIELDS:
|
|
75
|
+
if key in out and not isinstance(out[key], str):
|
|
76
|
+
out[key] = json.dumps(out[key], ensure_ascii=False)
|
|
77
|
+
return out
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _make_request(token, method_name, method="get", params=None, files=None, **kwargs):
|
|
81
|
+
url = API_URL.format(token, method_name)
|
|
82
|
+
params = params or {}
|
|
83
|
+
timeout = kwargs.pop("timeout", READ_TIMEOUT + CONNECT_TIMEOUT)
|
|
84
|
+
|
|
85
|
+
if CUSTOM_REQUEST_SENDER is not None:
|
|
86
|
+
return CUSTOM_REQUEST_SENDER(method=method, url=url, params=params, files=files, timeout=timeout)
|
|
87
|
+
|
|
88
|
+
session = _get_session()
|
|
89
|
+
proxies = proxy
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
if method == "get":
|
|
93
|
+
resp = session.get(url, params=params, timeout=timeout, proxies=proxies, **kwargs)
|
|
94
|
+
elif method == "post":
|
|
95
|
+
if files:
|
|
96
|
+
data = _serialize_for_multipart(params)
|
|
97
|
+
resp = session.post(url, data=data, files=files, timeout=timeout, proxies=proxies, **kwargs)
|
|
98
|
+
else:
|
|
99
|
+
resp = session.post(url, json=params, timeout=timeout, proxies=proxies, **kwargs)
|
|
100
|
+
else:
|
|
101
|
+
raise ValueError("Unsupported HTTP method: %r" % method)
|
|
102
|
+
except requests.exceptions.RequestException as e:
|
|
103
|
+
logger.error("Request to %s failed: %s", url, e)
|
|
104
|
+
raise
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
result = resp.json()
|
|
108
|
+
except ValueError:
|
|
109
|
+
resp.raise_for_status()
|
|
110
|
+
raise
|
|
111
|
+
|
|
112
|
+
if not result.get("ok"):
|
|
113
|
+
description = result.get("description", "Unknown error")
|
|
114
|
+
logger.error("KernelGram API error in %s: %s", method_name, description)
|
|
115
|
+
raise ApiTelegramException(method_name, result, result)
|
|
116
|
+
|
|
117
|
+
return result
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ApiTelegramException(Exception):
|
|
121
|
+
"""Raised when the KernelGram Bot API returns ok=false."""
|
|
122
|
+
|
|
123
|
+
def __init__(self, function_name, result, payload):
|
|
124
|
+
super().__init__(
|
|
125
|
+
"KernelGram API error in %s: %s" % (function_name, result.get("description"))
|
|
126
|
+
)
|
|
127
|
+
self.function_name = function_name
|
|
128
|
+
self.result = result
|
|
129
|
+
self.error_code = result.get("error_code")
|
|
130
|
+
self.description = result.get("description")
|
|
131
|
+
self.payload = payload
|
kernelbot/bot.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""TeleBot class for KernelGram. Mirrors telebot.TeleBot."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import time
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from . import apihelper
|
|
8
|
+
from . import types
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("kernelbot")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TeleBot:
|
|
14
|
+
"""Synchronous KernelGram bot, API-compatible with pyTelegramBotAPI."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, token, parse_mode=None, threaded=True, num_threads=2):
|
|
17
|
+
if not token or not isinstance(token, str):
|
|
18
|
+
raise ValueError("Token must be a non-empty string")
|
|
19
|
+
self.token = token
|
|
20
|
+
self.parse_mode = parse_mode
|
|
21
|
+
self.threaded = threaded
|
|
22
|
+
self.num_threads = num_threads
|
|
23
|
+
self._message_handlers = []
|
|
24
|
+
self._callback_handlers = []
|
|
25
|
+
self._update_listener = None
|
|
26
|
+
|
|
27
|
+
# ---------------- API methods ----------------
|
|
28
|
+
|
|
29
|
+
def get_me(self):
|
|
30
|
+
return types.User(**apihelper._make_request(self.token, "getMe"))
|
|
31
|
+
|
|
32
|
+
def get_updates(self, offset=None, limit=None, timeout=20, allowed_updates=None):
|
|
33
|
+
params = {"timeout": timeout}
|
|
34
|
+
if offset is not None:
|
|
35
|
+
params["offset"] = offset
|
|
36
|
+
if limit is not None:
|
|
37
|
+
params["limit"] = limit
|
|
38
|
+
if allowed_updates is not None:
|
|
39
|
+
params["allowed_updates"] = allowed_updates
|
|
40
|
+
return apihelper._make_request(self.token, "getUpdates", method="get", params=params)
|
|
41
|
+
|
|
42
|
+
def send_message(self, chat_id, text, parse_mode=None, reply_markup=None,
|
|
43
|
+
disable_web_page_preview=None, **kwargs):
|
|
44
|
+
params = {"chat_id": chat_id, "text": text}
|
|
45
|
+
pm = parse_mode if parse_mode is not None else self.parse_mode
|
|
46
|
+
if pm:
|
|
47
|
+
params["parse_mode"] = pm
|
|
48
|
+
if reply_markup is not None:
|
|
49
|
+
params["reply_markup"] = reply_markup
|
|
50
|
+
if disable_web_page_preview is not None:
|
|
51
|
+
params["disable_web_page_preview"] = disable_web_page_preview
|
|
52
|
+
params.update(kwargs)
|
|
53
|
+
result = apihelper._make_request(self.token, "sendMessage", method="post", params=params)
|
|
54
|
+
return types.Message(**result)
|
|
55
|
+
|
|
56
|
+
def send_photo(self, chat_id, photo, caption=None, parse_mode=None,
|
|
57
|
+
reply_markup=None, **kwargs):
|
|
58
|
+
params = {"chat_id": chat_id}
|
|
59
|
+
pm = parse_mode if parse_mode is not None else self.parse_mode
|
|
60
|
+
if caption is not None:
|
|
61
|
+
params["caption"] = caption
|
|
62
|
+
if pm:
|
|
63
|
+
params["parse_mode"] = pm
|
|
64
|
+
if reply_markup is not None:
|
|
65
|
+
params["reply_markup"] = reply_markup
|
|
66
|
+
params.update(kwargs)
|
|
67
|
+
|
|
68
|
+
files = None
|
|
69
|
+
if hasattr(photo, "read"):
|
|
70
|
+
files = {"photo": photo}
|
|
71
|
+
result = apihelper._make_request(self.token, "sendPhoto", method="post",
|
|
72
|
+
params=params, files=files)
|
|
73
|
+
else:
|
|
74
|
+
params["photo"] = photo
|
|
75
|
+
result = apihelper._make_request(self.token, "sendPhoto", method="post",
|
|
76
|
+
params=params)
|
|
77
|
+
return types.Message(**result)
|
|
78
|
+
|
|
79
|
+
def delete_message(self, chat_id, message_id):
|
|
80
|
+
params = {"chat_id": chat_id, "message_id": message_id}
|
|
81
|
+
return apihelper._make_request(self.token, "deleteMessage", method="post", params=params)
|
|
82
|
+
|
|
83
|
+
def edit_message_text(self, text, chat_id=None, message_id=None, inline_message_id=None,
|
|
84
|
+
parse_mode=None, reply_markup=None, **kwargs):
|
|
85
|
+
params = {"text": text}
|
|
86
|
+
if chat_id is not None:
|
|
87
|
+
params["chat_id"] = chat_id
|
|
88
|
+
if message_id is not None:
|
|
89
|
+
params["message_id"] = message_id
|
|
90
|
+
if inline_message_id is not None:
|
|
91
|
+
params["inline_message_id"] = inline_message_id
|
|
92
|
+
pm = parse_mode if parse_mode is not None else self.parse_mode
|
|
93
|
+
if pm:
|
|
94
|
+
params["parse_mode"] = pm
|
|
95
|
+
if reply_markup is not None:
|
|
96
|
+
params["reply_markup"] = reply_markup
|
|
97
|
+
params.update(kwargs)
|
|
98
|
+
result = apihelper._make_request(self.token, "editMessageText", method="post", params=params)
|
|
99
|
+
if isinstance(result, dict) and "message_id" in result:
|
|
100
|
+
return types.Message(**result)
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
def edit_message_caption(self, caption, chat_id=None, message_id=None,
|
|
104
|
+
parse_mode=None, reply_markup=None, **kwargs):
|
|
105
|
+
params = {"caption": caption}
|
|
106
|
+
if chat_id is not None:
|
|
107
|
+
params["chat_id"] = chat_id
|
|
108
|
+
if message_id is not None:
|
|
109
|
+
params["message_id"] = message_id
|
|
110
|
+
pm = parse_mode if parse_mode is not None else self.parse_mode
|
|
111
|
+
if pm:
|
|
112
|
+
params["parse_mode"] = pm
|
|
113
|
+
if reply_markup is not None:
|
|
114
|
+
params["reply_markup"] = reply_markup
|
|
115
|
+
params.update(kwargs)
|
|
116
|
+
result = apihelper._make_request(self.token, "editMessageCaption", method="post", params=params)
|
|
117
|
+
if isinstance(result, dict) and "message_id" in result:
|
|
118
|
+
return types.Message(**result)
|
|
119
|
+
return result
|
|
120
|
+
|
|
121
|
+
def answer_callback_query(self, callback_query_id, text=None, show_alert=False,
|
|
122
|
+
url=None, cache_time=None, **kwargs):
|
|
123
|
+
params = {"callback_query_id": callback_query_id}
|
|
124
|
+
if text is not None:
|
|
125
|
+
params["text"] = text
|
|
126
|
+
if show_alert:
|
|
127
|
+
params["show_alert"] = True
|
|
128
|
+
if url is not None:
|
|
129
|
+
params["url"] = url
|
|
130
|
+
if cache_time is not None:
|
|
131
|
+
params["cache_time"] = cache_time
|
|
132
|
+
params.update(kwargs)
|
|
133
|
+
return apihelper._make_request(self.token, "answerCallbackQuery", method="post", params=params)
|
|
134
|
+
|
|
135
|
+
def reply_to(self, message, text, **kwargs):
|
|
136
|
+
return self.send_message(message.chat.id, text, **kwargs)
|
|
137
|
+
|
|
138
|
+
def get_file(self, file_id):
|
|
139
|
+
result = apihelper._make_request(self.token, "getFile", method="get", params={"file_id": file_id})
|
|
140
|
+
return types.File(**result)
|
|
141
|
+
|
|
142
|
+
# ---------------- Handlers ----------------
|
|
143
|
+
|
|
144
|
+
def message_handler(self, commands=None, regexp=None, func=None,
|
|
145
|
+
content_types=None, chat_types=None):
|
|
146
|
+
def decorator(handler):
|
|
147
|
+
self._message_handlers.append({
|
|
148
|
+
"function": handler,
|
|
149
|
+
"commands": commands,
|
|
150
|
+
"regexp": re.compile(regexp) if regexp else None,
|
|
151
|
+
"func": func,
|
|
152
|
+
"content_types": content_types or ["text"],
|
|
153
|
+
"chat_types": chat_types,
|
|
154
|
+
})
|
|
155
|
+
return handler
|
|
156
|
+
return decorator
|
|
157
|
+
|
|
158
|
+
def callback_query_handler(self, func=None):
|
|
159
|
+
def decorator(handler):
|
|
160
|
+
self._callback_handlers.append({"function": handler, "func": func})
|
|
161
|
+
return handler
|
|
162
|
+
return decorator
|
|
163
|
+
|
|
164
|
+
def set_update_listener(self, listener):
|
|
165
|
+
self._update_listener = listener
|
|
166
|
+
|
|
167
|
+
def _test_message_handler(self, handler, message):
|
|
168
|
+
if handler["content_types"] and message.content_type not in handler["content_types"]:
|
|
169
|
+
return False
|
|
170
|
+
if handler["chat_types"] and getattr(message.chat, "type", None) not in handler["chat_types"]:
|
|
171
|
+
return False
|
|
172
|
+
if handler["commands"]:
|
|
173
|
+
text = getattr(message, "text", "") or ""
|
|
174
|
+
if not any(text.startswith("/" + c) for c in handler["commands"]):
|
|
175
|
+
return False
|
|
176
|
+
if handler["regexp"] and not handler["regexp"].search(getattr(message, "text", "") or ""):
|
|
177
|
+
return False
|
|
178
|
+
if handler["func"] and not handler["func"](message):
|
|
179
|
+
return False
|
|
180
|
+
return True
|
|
181
|
+
|
|
182
|
+
def process_new_updates(self, updates):
|
|
183
|
+
for update in updates:
|
|
184
|
+
if self._update_listener:
|
|
185
|
+
self._update_listener([update])
|
|
186
|
+
|
|
187
|
+
msg = update.get("message")
|
|
188
|
+
if msg:
|
|
189
|
+
message = types.Message(**msg)
|
|
190
|
+
for handler in self._message_handlers:
|
|
191
|
+
if self._test_message_handler(handler, message):
|
|
192
|
+
handler["function"](message)
|
|
193
|
+
break
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
cb = update.get("callback_query")
|
|
197
|
+
if cb:
|
|
198
|
+
callback = types.CallbackQuery(**cb)
|
|
199
|
+
for handler in self._callback_handlers:
|
|
200
|
+
if handler["func"] is None or handler["func"](callback):
|
|
201
|
+
handler["function"](callback)
|
|
202
|
+
break
|
|
203
|
+
|
|
204
|
+
# ---------------- Polling ----------------
|
|
205
|
+
|
|
206
|
+
def polling(self, interval=0, timeout=20, allowed_updates=None, offset=None):
|
|
207
|
+
offset = offset or self._last_update_id()
|
|
208
|
+
logger.info("kernelbot: polling started")
|
|
209
|
+
while True:
|
|
210
|
+
try:
|
|
211
|
+
result = self.get_updates(offset=offset, timeout=timeout,
|
|
212
|
+
allowed_updates=allowed_updates)
|
|
213
|
+
updates = result.get("result", [])
|
|
214
|
+
if updates:
|
|
215
|
+
offset = updates[-1]["update_id"] + 1
|
|
216
|
+
self.process_new_updates(updates)
|
|
217
|
+
if interval:
|
|
218
|
+
time.sleep(interval)
|
|
219
|
+
except KeyboardInterrupt:
|
|
220
|
+
logger.info("kernelbot: polling stopped")
|
|
221
|
+
return
|
|
222
|
+
except Exception as e:
|
|
223
|
+
logger.error("kernelbot: polling error: %s", e)
|
|
224
|
+
time.sleep(3)
|
|
225
|
+
|
|
226
|
+
def infinity_polling(self, timeout=20, **kwargs):
|
|
227
|
+
self.polling(timeout=timeout, **kwargs)
|
|
228
|
+
|
|
229
|
+
def _last_update_id(self):
|
|
230
|
+
try:
|
|
231
|
+
result = self.get_updates(offset=-1, timeout=0)
|
|
232
|
+
updates = result.get("result", [])
|
|
233
|
+
if updates:
|
|
234
|
+
return updates[-1]["update_id"] + 1
|
|
235
|
+
except Exception:
|
|
236
|
+
pass
|
|
237
|
+
return None
|
kernelbot/types.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""KernelGram Bot API types, mirroring telebot.types."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TeleBotBase:
|
|
5
|
+
"""Base class: turns dicts into attribute-accessible objects."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, **kwargs):
|
|
8
|
+
for key, value in kwargs.items():
|
|
9
|
+
if isinstance(value, dict):
|
|
10
|
+
value = self._wrap(value)
|
|
11
|
+
elif isinstance(value, list):
|
|
12
|
+
value = [self._wrap(v) for v in value]
|
|
13
|
+
setattr(self, key, value)
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def _wrap(value):
|
|
17
|
+
if isinstance(value, dict):
|
|
18
|
+
return TeleBotBase(**value)
|
|
19
|
+
return value
|
|
20
|
+
|
|
21
|
+
def __repr__(self):
|
|
22
|
+
return "<%s %s>" % (self.__class__.__name__, self.__dict__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class User(TeleBotBase):
|
|
26
|
+
@property
|
|
27
|
+
def full_name(self):
|
|
28
|
+
name = getattr(self, "first_name", "") or ""
|
|
29
|
+
last = getattr(self, "last_name", "") or ""
|
|
30
|
+
return (name + " " + last).strip()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Chat(TeleBotBase):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Message(TeleBotBase):
|
|
38
|
+
def __init__(self, **kwargs):
|
|
39
|
+
if "from" in kwargs:
|
|
40
|
+
kwargs["from_user"] = kwargs.pop("from")
|
|
41
|
+
super().__init__(**kwargs)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def content_type(self):
|
|
45
|
+
for key in (
|
|
46
|
+
"text", "audio", "document", "animation", "photo", "sticker",
|
|
47
|
+
"video", "video_note", "voice", "location", "contact", "venue",
|
|
48
|
+
"dice", "new_chat_members", "left_chat_member", "new_chat_title",
|
|
49
|
+
"new_chat_photo", "delete_chat_photo", "group_chat_created",
|
|
50
|
+
"supergroup_chat_created", "channel_chat_created",
|
|
51
|
+
"migrate_to_chat_id", "migrate_from_chat_id", "pinned_message",
|
|
52
|
+
"invoice", "successful_payment", "connected_website", "poll",
|
|
53
|
+
"passport_data", "proximity_alert_triggered", "video_chat_scheduled",
|
|
54
|
+
"video_chat_started", "video_chat_ended",
|
|
55
|
+
"video_chat_participants_invited", "web_app_data",
|
|
56
|
+
"message_auto_delete_timer_changed", "forum_topic_created",
|
|
57
|
+
"forum_topic_closed", "forum_topic_reopened", "forum_topic_edited",
|
|
58
|
+
"general_forum_topic_hidden", "general_forum_topic_unhidden",
|
|
59
|
+
"write_access_allowed", "user_shared", "chat_shared", "story",
|
|
60
|
+
):
|
|
61
|
+
if hasattr(self, key):
|
|
62
|
+
return key
|
|
63
|
+
return "text"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Update(TeleBotBase):
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class CallbackQuery(TeleBotBase):
|
|
71
|
+
def __init__(self, **kwargs):
|
|
72
|
+
if "from" in kwargs:
|
|
73
|
+
kwargs["from_user"] = kwargs.pop("from")
|
|
74
|
+
super().__init__(**kwargs)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class InlineQuery(TeleBotBase):
|
|
78
|
+
def __init__(self, **kwargs):
|
|
79
|
+
if "from" in kwargs:
|
|
80
|
+
kwargs["from_user"] = kwargs.pop("from")
|
|
81
|
+
super().__init__(**kwargs)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class File(TeleBotBase):
|
|
85
|
+
pass
|
kernelbot/util.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Utility helpers for kernelbot."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def split_string(text, chars_per_string):
|
|
5
|
+
"""Split text into chunks of at most `chars_per_string` characters."""
|
|
6
|
+
return [text[i:i + chars_per_string] for i in range(0, len(text), chars_per_string)]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def smart_split(text, chars_per_string=3000):
|
|
10
|
+
"""Split text preferring '\\n', '. ' and ' ' boundaries."""
|
|
11
|
+
if len(text) <= chars_per_string:
|
|
12
|
+
return [text]
|
|
13
|
+
|
|
14
|
+
parts = []
|
|
15
|
+
remaining = text
|
|
16
|
+
while len(remaining) > chars_per_string:
|
|
17
|
+
chunk = remaining[:chars_per_string]
|
|
18
|
+
for sep in ("\n", ". ", " "):
|
|
19
|
+
idx = chunk.rfind(sep)
|
|
20
|
+
if idx != -1:
|
|
21
|
+
chunk = remaining[:idx + len(sep)]
|
|
22
|
+
break
|
|
23
|
+
parts.append(chunk)
|
|
24
|
+
remaining = remaining[len(chunk):]
|
|
25
|
+
if remaining:
|
|
26
|
+
parts.append(remaining)
|
|
27
|
+
return parts
|
kernelbot/version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.2"
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kernelbot
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: A simple, but extensible Python implementation for the KernelGram Bot API. Telebot-compatible.
|
|
5
|
+
Author-email: ReaalRick <rickReaal@proton.me>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ReaalRick/kernelbot
|
|
8
|
+
Project-URL: Repository, https://github.com/ReaalRick/kernelbot
|
|
9
|
+
Project-URL: Issues, https://github.com/ReaalRick/kernelbot/issues
|
|
10
|
+
Keywords: kernelgram,bot,api,telebot
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: requests>=2.28.0
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# kernelbot
|
|
27
|
+
|
|
28
|
+
Простая, но расширяемая реализация **KernelGram Bot API** на Python.
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/kernelbot/)
|
|
31
|
+
[](https://www.python.org/)
|
|
32
|
+
[](LICENSE)
|
|
33
|
+
[](https://kernelgram.club)
|
|
34
|
+
|
|
35
|
+
`kernelbot` — это замена [`pyTelegramBotAPI`](https://github.com/eternnoir/pyTelegramBotAPI) (telebot), адаптированная для [KernelGram](https://kernelgram.club) — мессенджера по типу телеграма но со своим сервером. Если у вас есть код под `telebot`, достаточно поменять импорт — и он заработает.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Содержание
|
|
40
|
+
|
|
41
|
+
- [Официальная документация](#официальная-документация)
|
|
42
|
+
- [Установка](#установка)
|
|
43
|
+
- [Первый бот](#первый-бот)
|
|
44
|
+
- [Что нужно](#что-нужно)
|
|
45
|
+
- [Простой эхо-бот](#простой-эхо-бот)
|
|
46
|
+
- [Общая документация API](#общая-документация-api)
|
|
47
|
+
- [Типы](#типы)
|
|
48
|
+
- [Методы](#методы)
|
|
49
|
+
- [Обработчики сообщений](#обработчики-сообщений)
|
|
50
|
+
- [Обработчики callback-запросов](#обработчики-callback-запросов)
|
|
51
|
+
- [Отправка фото](#отправка-фото)
|
|
52
|
+
- [Отправка больших сообщений](#отправка-больших-сообщений)
|
|
53
|
+
- [Продвинутое использование](#продвинутое-использование)
|
|
54
|
+
- [Только IPv4](#только-ipv4)
|
|
55
|
+
- [Скачивание файлов](#скачивание-файлов)
|
|
56
|
+
- [Прокси](#прокси)
|
|
57
|
+
- [Ограничения KernelGram](#ограничения-kernelgram)
|
|
58
|
+
- [Переход с telebot](#переход-с-telebot)
|
|
59
|
+
- [Лицензия](#лицензия)
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Официальная документация
|
|
64
|
+
|
|
65
|
+
- FAQ по ботам KernelGram: https://kernelgram.club/bots
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Установка
|
|
70
|
+
|
|
71
|
+
Библиотека протестирована на Python 3.8+.
|
|
72
|
+
|
|
73
|
+
Установка через pip:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
pip install kernelbot
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Или из исходников:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
pip install git+https://github.com/ReaalRick/kernelbot.git
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Первый бот
|
|
92
|
+
|
|
93
|
+
### Что нужно
|
|
94
|
+
|
|
95
|
+
Токен, полученный у `@BotFather` внутри приложения KernelGram. Назовём его `TOKEN`.
|
|
96
|
+
|
|
97
|
+
### Простой эхо-бот
|
|
98
|
+
|
|
99
|
+
Класс `TeleBot` инкапсулирует все вызовы API в одном классе. Он предоставляет функции вида `send_xyz` (`send_message`, `send_photo` и другие) и несколько способов слушать входящие сообщения.
|
|
100
|
+
|
|
101
|
+
Создайте файл `echo_bot.py`:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
import kernelbot
|
|
105
|
+
|
|
106
|
+
bot = kernelbot.TeleBot("TOKEN", parse_mode=None)
|
|
107
|
+
|
|
108
|
+
@bot.message_handler(commands=['start', 'help'])
|
|
109
|
+
def send_welcome(message):
|
|
110
|
+
bot.reply_to(message, "Howdy, how are you doing?")
|
|
111
|
+
|
|
112
|
+
@bot.message_handler(func=lambda m: True)
|
|
113
|
+
def echo_all(message):
|
|
114
|
+
bot.reply_to(message, message.text)
|
|
115
|
+
|
|
116
|
+
bot.infinity_polling()
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Запуск:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
python echo_bot.py
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Проверьте: отправьте /start и любое текстовое сообщение.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### Общая документация API
|
|
130
|
+
|
|
131
|
+
Все типы описаны в kernelbot.types. Они соответствуют определениям KernelGram Bot API
|
|
132
|
+
|
|
133
|
+
У объекта Message есть атрибут content_type, одно из значений:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
text, audio, document, animation, photo, sticker, video,
|
|
137
|
+
video_note, voice, location, contact, venue, dice,
|
|
138
|
+
new_chat_members, left_chat_member, new_chat_title, new_chat_photo,
|
|
139
|
+
delete_chat_photo, group_chat_created, supergroup_chat_created,
|
|
140
|
+
channel_chat_created, migrate_to_chat_id, migrate_from_chat_id,
|
|
141
|
+
pinned_message, invoice, successful_payment, connected_website,
|
|
142
|
+
poll, passport_data, proximity_alert_triggered, video_chat_scheduled,
|
|
143
|
+
video_chat_started, video_chat_ended, video_chat_participants_invited,
|
|
144
|
+
web_app_data, message_auto_delete_timer_changed, forum_topic_created,
|
|
145
|
+
forum_topic_closed, forum_topic_reopened, forum_topic_edited,
|
|
146
|
+
general_forum_topic_hidden, general_forum_topic_unhidden,
|
|
147
|
+
write_access_allowed, user_shared, chat_shared, story.
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
У типа Message также есть from_user — KernelGram Bot API возвращает отправителя в поле from, но from — зарезервированное слово в Python, поэтому оно переименовано.
|
|
151
|
+
|
|
152
|
+
Методы
|
|
153
|
+
|
|
154
|
+
Все методы API находятся в классе TeleBot. Имена следуют обычным соглашениям Python:
|
|
155
|
+
|
|
156
|
+
KernelGram API метод kernelbot
|
|
157
|
+
getMe get_me()
|
|
158
|
+
sendMessage send_message()
|
|
159
|
+
sendPhoto send_photo()
|
|
160
|
+
editMessageText edit_message_text()
|
|
161
|
+
editMessageCaption edit_message_caption()
|
|
162
|
+
deleteMessage delete_message()
|
|
163
|
+
answerCallbackQuery answer_callback_query()
|
|
164
|
+
getUpdates get_updates()
|
|
165
|
+
getFile get_file()
|
|
166
|
+
|
|
167
|
+
Обработчики сообщений
|
|
168
|
+
|
|
169
|
+
Обработчик сообщений — это функция, декорированная message_handler экземпляра TeleBot:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
@bot.message_handler(commands=['start'])
|
|
173
|
+
def handle_start(message):
|
|
174
|
+
pass
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Поддерживаемые фильтры:
|
|
178
|
+
|
|
179
|
+
Имя Аргументы Условие
|
|
180
|
+
content_types список строк (по умолчанию ['text']) True, если message.content_type в списке
|
|
181
|
+
regexp строка регулярного выражения True, если re.search совпадает с message.text
|
|
182
|
+
commands список строк True, если message.text начинается с одной из команд
|
|
183
|
+
chat_types список типов чатов True, если message.chat.type в списке
|
|
184
|
+
func lambda или ссылка на функцию True, если вызываемый объект возвращает True
|
|
185
|
+
|
|
186
|
+
Все обработчики проверяются в порядке объявления.
|
|
187
|
+
|
|
188
|
+
Обработчики callback-запросов
|
|
189
|
+
|
|
190
|
+
Инлайн-кнопки передаются через reply_markup с полем inline_keyboard:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
@bot.message_handler(commands=["start"])
|
|
194
|
+
def start(message):
|
|
195
|
+
markup = {
|
|
196
|
+
"inline_keyboard": [
|
|
197
|
+
[{"text": "Say hi", "callback_data": "hi"}],
|
|
198
|
+
],
|
|
199
|
+
}
|
|
200
|
+
bot.send_message(message.chat.id, "Choose:", reply_markup=markup)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@bot.callback_query_handler(func=lambda call: call.data == "hi")
|
|
204
|
+
def on_hi(call):
|
|
205
|
+
bot.answer_callback_query(call.id, text="Hello!")
|
|
206
|
+
bot.send_message(call.message.chat.id, "Hi there!")
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Отправка фото
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
with open("photo.jpg", "rb") as f:
|
|
213
|
+
bot.send_photo(chat_id, f, caption="<b>Look</b>", reply_markup=markup)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Отправка больших сообщений
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from kernelbot import util
|
|
220
|
+
|
|
221
|
+
large_text = open("large_text.txt", "rb").read()
|
|
222
|
+
for text in util.smart_split(large_text, chars_per_string=3000):
|
|
223
|
+
bot.send_message(chat_id, text)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
Продвинутое использование
|
|
229
|
+
|
|
230
|
+
Только IPv4
|
|
231
|
+
|
|
232
|
+
API KernelGram обслуживается через Cloudflare, который отдаёт и IPv4-, и IPv6-адреса. В некоторых мобильных сетях IPv6 до Cloudflare не доходит, что вызывает ClientConnectorDNSError или No route to host.
|
|
233
|
+
|
|
234
|
+
kernelbot по умолчанию форсит IPv4. Чтобы отключить:
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
from kernelbot import apihelper
|
|
238
|
+
apihelper.FORCE_IPV4 = False
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Скачивание файлов
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
import requests
|
|
245
|
+
from kernelbot import apihelper
|
|
246
|
+
|
|
247
|
+
file_info = bot.get_file(file_id)
|
|
248
|
+
url = apihelper.FILE_URL.format(bot.token, file_info.file_path)
|
|
249
|
+
content = requests.get(url).content
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Прокси
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
from kernelbot import apihelper
|
|
256
|
+
apihelper.proxy = {"https": "socks5://user:pass@host:port"}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Требует pip install requests[socks] для SOCKS-прокси.
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
Ограничения KernelGram
|
|
264
|
+
|
|
265
|
+
Некоторые методы и возможности Telegram Bot API в KernelGram не реализованы или работают иначе:
|
|
266
|
+
|
|
267
|
+
· setMyProfilePhoto — отсутствует (METHOD_NOT_FOUND). Аватар бота устанавливается через @BotFather.
|
|
268
|
+
· Reply-клавиатуры (reply_markup.keyboard) принимаются API, но не отображаются в клиенте. Используйте инлайн-клавиатуры (reply_markup.inline_keyboard).
|
|
269
|
+
· Все методы set* для профиля бота (имя, описание, команды) доступны только через @BotFather.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
Переход с telebot
|
|
274
|
+
|
|
275
|
+
kernelbot повторяет публичный API pyTelegramBotAPI. Для перехода достаточно двух строк:
|
|
276
|
+
|
|
277
|
+
```python
|
|
278
|
+
# Было
|
|
279
|
+
import telebot
|
|
280
|
+
bot = telebot.TeleBot(TOKEN)
|
|
281
|
+
|
|
282
|
+
# Стало
|
|
283
|
+
import kernelbot
|
|
284
|
+
bot = kernelbot.TeleBot(TOKEN)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Всё остальное — обработчики, фильтры, reply_to, send_message, infinity_polling — работает так же.
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
Лицензия
|
|
292
|
+
|
|
293
|
+
MIT. См. LICENSE.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
kernelbot/__init__.py,sha256=14_q0BvQUHiZBTzeDmzJRZgWudtJCmp2o4ZCO-8yPKw,315
|
|
2
|
+
kernelbot/apihelper.py,sha256=4ycGi6NR5sfWWzGgyVX7C3o6GKVvr1JtD_O7wRbtlJg,4007
|
|
3
|
+
kernelbot/bot.py,sha256=4z3mQrHgv4jLX5sOjU6lSbEmoOx_7reolCoWL6KFKnA,9622
|
|
4
|
+
kernelbot/types.py,sha256=wBHCWWcL-tcOj6SkkL0RdMITdVYVzsztFDZsMVm8ITg,2722
|
|
5
|
+
kernelbot/util.py,sha256=XDEvIVTgm3yRgYN7wXdiMoOSLxeQ2rq7N2OlANaU3Ws,854
|
|
6
|
+
kernelbot/version.py,sha256=QvlVh4JTl3JL7jQAja76yKtT-IvF4631ASjWY1wS6AQ,22
|
|
7
|
+
kernelbot-0.0.2.dist-info/licenses/LICENSE,sha256=xGZxQ1A4VzAX7SoOPIteaWD3qhQPSexXlCIEpL1VtE4,1066
|
|
8
|
+
kernelbot-0.0.2.dist-info/METADATA,sha256=BACWDPn7CF-4YCglm8GTzLl9nuqncZL4wEElnSQ_YH8,10622
|
|
9
|
+
kernelbot-0.0.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
10
|
+
kernelbot-0.0.2.dist-info/top_level.txt,sha256=9xoFJVdmfKyFREyqgNFOBERj4BgFFfP16riRn_2265E,10
|
|
11
|
+
kernelbot-0.0.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ReaalRick
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
kernelbot
|