maxibot 0.98.1__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.
- maxibot/__init__.py +644 -0
- maxibot/apihelper.py +241 -0
- maxibot/core/attachments/photo.py +45 -0
- maxibot/core/network/client.py +118 -0
- maxibot/core/network/polling.py +75 -0
- maxibot/types.py +753 -0
- maxibot/util.py +98 -0
- maxibot-0.98.1.dist-info/METADATA +56 -0
- maxibot-0.98.1.dist-info/RECORD +11 -0
- maxibot-0.98.1.dist-info/WHEEL +5 -0
- maxibot-0.98.1.dist-info/top_level.txt +1 -0
maxibot/apihelper.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from typing import Dict, Any, List, Optional
|
|
3
|
+
|
|
4
|
+
from maxibot.core.network.client import Client
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
proxy = None
|
|
8
|
+
ignore_warnings = True
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Api:
|
|
12
|
+
"""
|
|
13
|
+
Клиент для рабты с api MAX
|
|
14
|
+
"""
|
|
15
|
+
def __init__(self, token: str):
|
|
16
|
+
"""
|
|
17
|
+
Docstring for __init__
|
|
18
|
+
|
|
19
|
+
:param token: Токен бота
|
|
20
|
+
:type token: str
|
|
21
|
+
"""
|
|
22
|
+
if ignore_warnings:
|
|
23
|
+
requests.packages.urllib3.disable_warnings()
|
|
24
|
+
self.client = Client(token=token, proxy=proxy)
|
|
25
|
+
|
|
26
|
+
def get_my_info(self) -> Dict[str, Any]:
|
|
27
|
+
"""
|
|
28
|
+
Получает информацию о текущем боте
|
|
29
|
+
|
|
30
|
+
:return: Информация о боте
|
|
31
|
+
:rtype: Dict[str, Any]
|
|
32
|
+
"""
|
|
33
|
+
return self.client.request("GET", "/me")
|
|
34
|
+
|
|
35
|
+
def get_updates(self, allowed_updates: List[str], extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
36
|
+
"""
|
|
37
|
+
Получает новые обновления от API через лонгполлинг
|
|
38
|
+
|
|
39
|
+
:param allowed_updates: Список типов обновлений, которые нужно получать
|
|
40
|
+
:param extra: Дополнительные параметры запроса
|
|
41
|
+
|
|
42
|
+
:return: Список обновлений
|
|
43
|
+
:rtype: Dict[str, Any]
|
|
44
|
+
"""
|
|
45
|
+
params = extra or {}
|
|
46
|
+
|
|
47
|
+
if allowed_updates:
|
|
48
|
+
params["types"] = ",".join(allowed_updates)
|
|
49
|
+
|
|
50
|
+
return self.client.request("GET", "/updates", params=params)
|
|
51
|
+
|
|
52
|
+
def get_message(self, msg_id: str):
|
|
53
|
+
"""
|
|
54
|
+
Получает сообщение по `msg_id`
|
|
55
|
+
"""
|
|
56
|
+
return self.client.request("GET", f"/messages/{msg_id}")
|
|
57
|
+
|
|
58
|
+
def send_message(
|
|
59
|
+
self,
|
|
60
|
+
chat_id: str = None,
|
|
61
|
+
msg_id: str = None,
|
|
62
|
+
text: str = None,
|
|
63
|
+
method: str = "POST",
|
|
64
|
+
attachments: Optional[List[Dict[str, Any]]] = None,
|
|
65
|
+
parse_mode: str = "markdown",
|
|
66
|
+
notify: bool = True
|
|
67
|
+
) -> Dict[str, Any]:
|
|
68
|
+
"""
|
|
69
|
+
Отправляет/удаляет/обновляет сообщение в чате
|
|
70
|
+
|
|
71
|
+
:param chat_id: Идентификатор чата
|
|
72
|
+
:type chat_id: str
|
|
73
|
+
|
|
74
|
+
:param text: Текст сообщения
|
|
75
|
+
:type text: str
|
|
76
|
+
|
|
77
|
+
:param attachments: Вложения сообщения
|
|
78
|
+
:type text: Optional[List[Dict[str, Any]]]
|
|
79
|
+
|
|
80
|
+
:return: Информация об отправленном сообщении
|
|
81
|
+
:rtype: Dict[str, Any]
|
|
82
|
+
"""
|
|
83
|
+
# query параметры запроса
|
|
84
|
+
if chat_id:
|
|
85
|
+
params = {"chat_id": chat_id}
|
|
86
|
+
elif msg_id and method in ("DELETE", "PUT"):
|
|
87
|
+
params = {"message_id": msg_id}
|
|
88
|
+
|
|
89
|
+
data = {}
|
|
90
|
+
if text:
|
|
91
|
+
data = {"text": text}
|
|
92
|
+
|
|
93
|
+
if attachments:
|
|
94
|
+
data["attachments"] = attachments
|
|
95
|
+
|
|
96
|
+
if parse_mode:
|
|
97
|
+
data["format"] = parse_mode
|
|
98
|
+
|
|
99
|
+
if notify:
|
|
100
|
+
data["notify"] = notify
|
|
101
|
+
|
|
102
|
+
return self.client.request(method, "/messages", params=params, data=data)
|
|
103
|
+
|
|
104
|
+
def get_upload_file_url(self, type_attach: str):
|
|
105
|
+
"""
|
|
106
|
+
Апи метод для получения url загрузки файла.
|
|
107
|
+
|
|
108
|
+
:param type_attach: Тип файла, который требуется загрузить
|
|
109
|
+
:type type_attach: str
|
|
110
|
+
|
|
111
|
+
:return: Json с url для загрузки файла
|
|
112
|
+
:rtype: Dict[str: Any]
|
|
113
|
+
"""
|
|
114
|
+
return self.client.request("POST", f"/uploads?type={type_attach}")
|
|
115
|
+
|
|
116
|
+
def load_file(self, url: str, files: Dict, content_types: str = None):
|
|
117
|
+
"""
|
|
118
|
+
Апи метод для получения url загрузки файла.
|
|
119
|
+
|
|
120
|
+
:param type_attach: Тип файла, который требуется загрузить
|
|
121
|
+
:type type_attach: str
|
|
122
|
+
|
|
123
|
+
:return: Json с url для загрузки файла
|
|
124
|
+
:rtype: Dict[str: Any]
|
|
125
|
+
"""
|
|
126
|
+
return self.client.request(method="POST", url=url, files=files, content_types=content_types)
|
|
127
|
+
|
|
128
|
+
def answer_callback(
|
|
129
|
+
self,
|
|
130
|
+
callback_id: str,
|
|
131
|
+
text: Optional[str] = None,
|
|
132
|
+
notification: Optional[str] = None,
|
|
133
|
+
attachments: Optional[List[Dict[str, Any]]] = None,
|
|
134
|
+
link: Optional[Dict[str, Any]] = None,
|
|
135
|
+
notify: bool = True,
|
|
136
|
+
format: Optional[str] = None,
|
|
137
|
+
) -> Dict[str, Any]:
|
|
138
|
+
"""
|
|
139
|
+
Метод позволяет отправить уведомление пользователю и/или обновить
|
|
140
|
+
исходное сообщение после нажатия на inline-кнопку.
|
|
141
|
+
|
|
142
|
+
:param callback_id: Уникальный идентификатор callback-запроса.
|
|
143
|
+
Получается из поля `callback.callback_id` в обновлении
|
|
144
|
+
:type callback_id: str
|
|
145
|
+
|
|
146
|
+
:param text: Новый текст сообщения. Если указан, сообщение будет обновлено
|
|
147
|
+
:type text: Optional[str]
|
|
148
|
+
|
|
149
|
+
:param notification: Текст всплывающего уведомления для пользователя.
|
|
150
|
+
Пользователь увидит это уведомление как всплывающее сообщение
|
|
151
|
+
Пока не очень работает, в тесте :)
|
|
152
|
+
:type notification: Optional[str]
|
|
153
|
+
|
|
154
|
+
:param attachments: Новые вложения сообщения. Если указаны, сообщение будет обновлено.
|
|
155
|
+
Для полной замены вложений передайте новый список.
|
|
156
|
+
Чтобы удалить все вложения, передайте пустой список.
|
|
157
|
+
:type attachments: Optional[List[Dict[str, Any]]]
|
|
158
|
+
|
|
159
|
+
:param link: Ссылка на сообщение для reply/forward формата.
|
|
160
|
+
Должен содержать поля `type` ("reply" или "forward") и `mid`
|
|
161
|
+
:type link: Optional[Dict[str, Any]]
|
|
162
|
+
|
|
163
|
+
:param notify: Отправлять ли системное уведомление в чат об изменении сообщения.
|
|
164
|
+
По умолчанию True - участники увидят "Сообщение было изменено"
|
|
165
|
+
:type notify: bool
|
|
166
|
+
|
|
167
|
+
:param format: Формат текста сообщения. Доступные значения: "markdown", "html"
|
|
168
|
+
:type format: Optional[str]
|
|
169
|
+
|
|
170
|
+
:return: Ответ от MAX API
|
|
171
|
+
:rtype: Dict[str, Any]
|
|
172
|
+
|
|
173
|
+
:raises HTTPError: При ошибке HTTP запроса
|
|
174
|
+
|
|
175
|
+
Примеры использования:
|
|
176
|
+
|
|
177
|
+
1. Только уведомление:
|
|
178
|
+
api.answer_callback(
|
|
179
|
+
callback_id="callback123",
|
|
180
|
+
notification="Действие выполнено!"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
2. Обновление сообщения с уведомлением:
|
|
184
|
+
api.answer_callback(
|
|
185
|
+
callback_id="callback123",
|
|
186
|
+
text="**Сообщение обновлено!**",
|
|
187
|
+
notification="Обновление выполнено",
|
|
188
|
+
format="markdown",
|
|
189
|
+
notify=False # Не показывать "Сообщение было изменено" в чате
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
3, 4. Пока в тесте
|
|
193
|
+
3. Обновление с новыми вложениями:
|
|
194
|
+
api.answer_callback(
|
|
195
|
+
callback_id="callback123",
|
|
196
|
+
text="Вот новые вложения:",
|
|
197
|
+
attachments=[
|
|
198
|
+
{
|
|
199
|
+
"type": "photo",
|
|
200
|
+
"payload": {"url": "https://example.com/photo.jpg"}
|
|
201
|
+
}
|
|
202
|
+
],
|
|
203
|
+
notification="Фотография добавлена"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
4. Удаление всех вложений (оставить только текст):
|
|
207
|
+
api.answer_callback(
|
|
208
|
+
callback_id="callback123",
|
|
209
|
+
text="Вложения удалены",
|
|
210
|
+
attachments=[], # Пустой список удалит все вложения
|
|
211
|
+
notification="Вложения удалены"
|
|
212
|
+
)
|
|
213
|
+
"""
|
|
214
|
+
params = {"callback_id": callback_id}
|
|
215
|
+
data: Dict[str, Any] = {}
|
|
216
|
+
|
|
217
|
+
# Если нужно изменить сообщение (text, attachments, link, format)
|
|
218
|
+
if text is not None or attachments is not None or link is not None or format is not None:
|
|
219
|
+
msg: Dict[str, Any] = {"notify": notify}
|
|
220
|
+
if text is not None:
|
|
221
|
+
msg["text"] = text
|
|
222
|
+
if attachments is not None:
|
|
223
|
+
msg["attachments"] = attachments
|
|
224
|
+
if link is not None:
|
|
225
|
+
msg["link"] = link
|
|
226
|
+
if format is not None:
|
|
227
|
+
msg["format"] = format
|
|
228
|
+
data["message"] = msg
|
|
229
|
+
|
|
230
|
+
# Если нужно отправить уведомление
|
|
231
|
+
if notification is not None:
|
|
232
|
+
data["notification"] = notification
|
|
233
|
+
|
|
234
|
+
print(f"Answer params: {params}")
|
|
235
|
+
print(f"Answer data: {data}")
|
|
236
|
+
|
|
237
|
+
# Если data пустой, отправляем пустой объект
|
|
238
|
+
if not data:
|
|
239
|
+
data = {}
|
|
240
|
+
|
|
241
|
+
return self.client.request("POST", "/answers", params=params, data=data)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from typing import Any, Union
|
|
2
|
+
|
|
3
|
+
from ...apihelper import Api
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Photo:
|
|
7
|
+
"""
|
|
8
|
+
Класс формирования объекта attachments для метода MaxiBot.send_photo
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, photo: Union[Any, str], api: Api):
|
|
12
|
+
try:
|
|
13
|
+
self.api = api
|
|
14
|
+
self.photo = photo
|
|
15
|
+
upload_url = self._get_upload_url().get("url")
|
|
16
|
+
if not upload_url:
|
|
17
|
+
return []
|
|
18
|
+
load_file_result = self._load_file_to_max(url=upload_url)
|
|
19
|
+
self.token_dict = list(list(load_file_result.values())[0].values())[0]
|
|
20
|
+
except Exception:
|
|
21
|
+
return []
|
|
22
|
+
|
|
23
|
+
def _get_upload_url(self, type_attach: str = "image"):
|
|
24
|
+
"""
|
|
25
|
+
Шаг 1.
|
|
26
|
+
Метод получения url для загрузки фото
|
|
27
|
+
"""
|
|
28
|
+
return self.api.get_upload_file_url(type_attach=type_attach)
|
|
29
|
+
|
|
30
|
+
def _load_file_to_max(self, url: str):
|
|
31
|
+
"""
|
|
32
|
+
Шаг 2.
|
|
33
|
+
Метод загрузки файла по url для загрузки фото
|
|
34
|
+
"""
|
|
35
|
+
files = {"data": self.photo}
|
|
36
|
+
return self.api.load_file(url=url, files=files)
|
|
37
|
+
|
|
38
|
+
def to_dict(self):
|
|
39
|
+
"""
|
|
40
|
+
Метод формирования необходимого для API MAX форматат attachments для image
|
|
41
|
+
"""
|
|
42
|
+
return {
|
|
43
|
+
"type": "image",
|
|
44
|
+
"payload": self.token_dict
|
|
45
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from typing import Dict, Any, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Client:
|
|
8
|
+
"""
|
|
9
|
+
Класс низкоуровневых запросов к API MAX
|
|
10
|
+
"""
|
|
11
|
+
# BASE_URL = "https://botapi.max.ru"
|
|
12
|
+
BASE_URL = "https://platform-api.max.ru"
|
|
13
|
+
|
|
14
|
+
def __init__(self, token: str, proxy: Optional[dict] = {"https": "", "http": ""}):
|
|
15
|
+
"""
|
|
16
|
+
Инициализация клиента
|
|
17
|
+
|
|
18
|
+
:param token: Description
|
|
19
|
+
:type token: str
|
|
20
|
+
"""
|
|
21
|
+
self.token = token
|
|
22
|
+
self.proxy = proxy
|
|
23
|
+
self.session = requests.Session()
|
|
24
|
+
|
|
25
|
+
def _make_url(self, path: str) -> str:
|
|
26
|
+
"""
|
|
27
|
+
Метод формирует полную ссылку к API запросу
|
|
28
|
+
|
|
29
|
+
:param path: API метод
|
|
30
|
+
:type path: str
|
|
31
|
+
:return: Полный URL запроса к API
|
|
32
|
+
:rtype: str
|
|
33
|
+
"""
|
|
34
|
+
url = f"{self.BASE_URL}{path}"
|
|
35
|
+
# if "?" in url:
|
|
36
|
+
# url += f"&access_token={self.token}"
|
|
37
|
+
# else:
|
|
38
|
+
# url += f"?access_token={self.token}"
|
|
39
|
+
return url
|
|
40
|
+
|
|
41
|
+
def request(
|
|
42
|
+
self,
|
|
43
|
+
method: str,
|
|
44
|
+
path: str = None,
|
|
45
|
+
url: str = None,
|
|
46
|
+
params: Optional[Dict[str, Any]] = None,
|
|
47
|
+
data: Optional[Dict[str, Any]] = None,
|
|
48
|
+
files: Optional[Dict[str, Any]] = None,
|
|
49
|
+
content_types: Optional[str] = None
|
|
50
|
+
) -> Dict[str, Any]:
|
|
51
|
+
"""
|
|
52
|
+
Главные метод по отправке запроса к API MAX
|
|
53
|
+
|
|
54
|
+
:param method: HTTP-метод (GET, POST, PUT, DELETE)
|
|
55
|
+
:type method: str
|
|
56
|
+
|
|
57
|
+
:param path: Путь к методу API
|
|
58
|
+
:type path: str
|
|
59
|
+
|
|
60
|
+
:param params: Параметры запроса
|
|
61
|
+
:type params: Optional[Dict[str, Any]]
|
|
62
|
+
|
|
63
|
+
:param data: Данные для отправки в теле запроса
|
|
64
|
+
:type data: Optional[Dict[str, Any]]
|
|
65
|
+
|
|
66
|
+
:param files: Файлы для отправкиescription
|
|
67
|
+
:type files: Optional[Dict[str, Any]]
|
|
68
|
+
|
|
69
|
+
:return: Ответ API MAX на заданный метод
|
|
70
|
+
:rtype: Dict[str, Any]
|
|
71
|
+
"""
|
|
72
|
+
url = self._make_url(path) if not url else url
|
|
73
|
+
header = {
|
|
74
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 "
|
|
75
|
+
"(KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36",
|
|
76
|
+
"Authorization": self.token
|
|
77
|
+
}
|
|
78
|
+
if content_types:
|
|
79
|
+
header["Content-Type"] = content_types
|
|
80
|
+
if data and not files:
|
|
81
|
+
header["Content-Type"] = "application/json"
|
|
82
|
+
data = json.dumps(data)
|
|
83
|
+
# print(f"Request: {method} {url}")
|
|
84
|
+
# if params:
|
|
85
|
+
# print(f"Params: {params}")
|
|
86
|
+
# if data:
|
|
87
|
+
# print(f"Data: {data}")
|
|
88
|
+
|
|
89
|
+
response = self.session.request(
|
|
90
|
+
method=method,
|
|
91
|
+
url=url,
|
|
92
|
+
params=params,
|
|
93
|
+
data=data,
|
|
94
|
+
files=files,
|
|
95
|
+
headers=header,
|
|
96
|
+
verify=False,
|
|
97
|
+
proxies=self.proxy,
|
|
98
|
+
timeout=60
|
|
99
|
+
)
|
|
100
|
+
try:
|
|
101
|
+
response.raise_for_status()
|
|
102
|
+
result = response.json()
|
|
103
|
+
# print(f"Response: {result}")
|
|
104
|
+
return result
|
|
105
|
+
except requests.exceptions.HTTPError as e:
|
|
106
|
+
error_text = f"HTTP error: {e}"
|
|
107
|
+
try:
|
|
108
|
+
error_json = response.json()
|
|
109
|
+
error_text = f"{error_text}, API response: {error_json}"
|
|
110
|
+
except Exception:
|
|
111
|
+
error_text = f"{error_text}, Response text: {response.text}"
|
|
112
|
+
|
|
113
|
+
print(error_text)
|
|
114
|
+
# raise Exception(error_text)
|
|
115
|
+
return error_text
|
|
116
|
+
except Exception as e:
|
|
117
|
+
print(f"Request error: {e}")
|
|
118
|
+
raise
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import traceback
|
|
3
|
+
|
|
4
|
+
from typing import Callable, List, Optional, Dict, Any
|
|
5
|
+
|
|
6
|
+
# from api import Api
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Polling:
|
|
10
|
+
"""
|
|
11
|
+
Класс получения обновлений из API MAX через поллинг
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, api, allowed_updates: Optional[List[str]] = None):
|
|
15
|
+
"""
|
|
16
|
+
Инициализация класса
|
|
17
|
+
|
|
18
|
+
:param api: Клиент АПИ
|
|
19
|
+
:type api: Api
|
|
20
|
+
|
|
21
|
+
:param allowed_updates: Клиент АПИ
|
|
22
|
+
:type allowed_updates: Optional[List[str]]
|
|
23
|
+
"""
|
|
24
|
+
self.api = api
|
|
25
|
+
self.allowed_updates = allowed_updates
|
|
26
|
+
self.is_running = False
|
|
27
|
+
self.marker = None
|
|
28
|
+
|
|
29
|
+
def stop(self):
|
|
30
|
+
"""
|
|
31
|
+
Метод остановки поллинга
|
|
32
|
+
"""
|
|
33
|
+
self.is_running = False
|
|
34
|
+
|
|
35
|
+
async def loop(self, handler: Callable[[Dict[str, Any]], None]):
|
|
36
|
+
"""
|
|
37
|
+
Главный цикл поллинга
|
|
38
|
+
|
|
39
|
+
:param handler: Description
|
|
40
|
+
:type handler: Callable[[Dict[str, Any]], None]
|
|
41
|
+
"""
|
|
42
|
+
self.is_running = True
|
|
43
|
+
print("Starting polling loop")
|
|
44
|
+
|
|
45
|
+
while self.is_running:
|
|
46
|
+
try:
|
|
47
|
+
updates_data = await self._get_updates()
|
|
48
|
+
if "marker" in updates_data.keys():
|
|
49
|
+
self.marker = updates_data["marker"]
|
|
50
|
+
updates = updates_data.get("updates", [])
|
|
51
|
+
for update in updates:
|
|
52
|
+
try:
|
|
53
|
+
handler(update)
|
|
54
|
+
except Exception:
|
|
55
|
+
print(f"Error handling update {traceback.format_exc()}")
|
|
56
|
+
|
|
57
|
+
except Exception:
|
|
58
|
+
print(f"Some error in get updates {traceback.format_exc()}")
|
|
59
|
+
|
|
60
|
+
async def _get_updates(self) -> Dict[str, Any]:
|
|
61
|
+
"""
|
|
62
|
+
Метод получения обновлений по боту из API MAX
|
|
63
|
+
|
|
64
|
+
:return: Description
|
|
65
|
+
:rtype: Dict[str, Any]
|
|
66
|
+
"""
|
|
67
|
+
params = {}
|
|
68
|
+
if self.marker is not None:
|
|
69
|
+
params["marker"] = self.marker
|
|
70
|
+
updates_data = await asyncio.to_thread(
|
|
71
|
+
self.api.get_updates,
|
|
72
|
+
self.allowed_updates or [],
|
|
73
|
+
params
|
|
74
|
+
)
|
|
75
|
+
return updates_data
|