bafser-tgapi 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.
- bafser_tgapi/__init__.py +7 -0
- bafser_tgapi/bot.py +312 -0
- bafser_tgapi/bot_with_db.py +27 -0
- bafser_tgapi/db/msg.py +51 -0
- bafser_tgapi/db/user.py +76 -0
- bafser_tgapi/methods.py +187 -0
- bafser_tgapi/types.py +316 -0
- bafser_tgapi/utils.py +185 -0
- bafser_tgapi-1.0.0.dist-info/METADATA +115 -0
- bafser_tgapi-1.0.0.dist-info/RECORD +12 -0
- bafser_tgapi-1.0.0.dist-info/WHEEL +4 -0
- bafser_tgapi-1.0.0.dist-info/licenses/LICENSE +21 -0
bafser_tgapi/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from .bot import Bot, BotCmdArgs
|
|
2
|
+
from .bot_with_db import BotWithDB
|
|
3
|
+
from .db.msg import MsgBase
|
|
4
|
+
from .db.user import TgUserBase
|
|
5
|
+
from .methods import *
|
|
6
|
+
from .types import *
|
|
7
|
+
from .utils import check_webhook_token, configure_webhook, get_bot_name, get_url, process_update, run_long_polling, set_webhook, setup
|
bafser_tgapi/bot.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Callable, Iterable, Self, Type, TypeVar, cast
|
|
3
|
+
|
|
4
|
+
from bafser import ParametrizedLogger, add_file_logger
|
|
5
|
+
from typing_extensions import Protocol
|
|
6
|
+
|
|
7
|
+
from .methods import *
|
|
8
|
+
from .types import *
|
|
9
|
+
from .utils import get_bot_name
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T", bound="Bot")
|
|
12
|
+
type tcmd_dsc_text = str
|
|
13
|
+
type tcmd_dsc_usage = str
|
|
14
|
+
re_param = re.compile("<[a-z]+>", re.IGNORECASE)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Bot:
|
|
18
|
+
class tcmd_fn[T: "Bot"](Protocol):
|
|
19
|
+
def __call__(self, bot: T, args: "BotCmdArgs", **kwargs: str) -> str | None:
|
|
20
|
+
...
|
|
21
|
+
type tcmd_dsc = tcmd_dsc_text | tuple[tcmd_dsc_text, tcmd_dsc_usage | list[tcmd_dsc_usage]]
|
|
22
|
+
type tcallback[T: "Bot"] = Callable[[T], None]
|
|
23
|
+
_tcommand = tuple[tcmd_fn[Self], tuple[tcmd_dsc | None, tcmd_dsc | None]]
|
|
24
|
+
|
|
25
|
+
update: Update
|
|
26
|
+
message: Message | None = None
|
|
27
|
+
callback_query: CallbackQuery | None = None
|
|
28
|
+
inline_query: InlineQuery | None = None
|
|
29
|
+
chosen_inline_result: ChosenInlineResult | None = None
|
|
30
|
+
|
|
31
|
+
_commands: dict[str, _tcommand] = {}
|
|
32
|
+
_callback: dict[Callable[..., Any], tcallback[Self]] = {}
|
|
33
|
+
_sender: User | None = None
|
|
34
|
+
chat: Chat | None = None
|
|
35
|
+
TextWrongCommand = "Wrong command"
|
|
36
|
+
TextCmdForAdmin = "Эта команда только для админов"
|
|
37
|
+
logger: "BotLogger"
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def sender(self):
|
|
41
|
+
return self._sender
|
|
42
|
+
|
|
43
|
+
@sender.setter
|
|
44
|
+
def sender(self, value: User | None):
|
|
45
|
+
self._sender = value
|
|
46
|
+
self.logger.user = value
|
|
47
|
+
|
|
48
|
+
def init(self):
|
|
49
|
+
fmt = "%(asctime)s;%(levelname)s;%(module)s;%(uid)-10s;%(uname)-15s;%(cmd)-15s;%(message)s"
|
|
50
|
+
self.logger = BotLogger(add_file_logger("logs/bot.csv", "bot", fmt, ["uid", "uname", "cmd"]))
|
|
51
|
+
|
|
52
|
+
def get_desc(v: Bot.tcmd_dsc):
|
|
53
|
+
return v if isinstance(v, str) else v[0]
|
|
54
|
+
|
|
55
|
+
for_all: list[BotCommand] = []
|
|
56
|
+
for_adm: list[BotCommand] = []
|
|
57
|
+
for cmd in self._commands.keys():
|
|
58
|
+
pub = self._commands[cmd][1][0]
|
|
59
|
+
adm = self._commands[cmd][1][1]
|
|
60
|
+
if not adm:
|
|
61
|
+
adm = pub
|
|
62
|
+
cmd = re_param.sub("", cmd)
|
|
63
|
+
if pub:
|
|
64
|
+
for_all.append(BotCommand(command=cmd, description=get_desc(pub)))
|
|
65
|
+
if adm:
|
|
66
|
+
for_adm.append(BotCommand(command=cmd, description=get_desc(adm)))
|
|
67
|
+
|
|
68
|
+
setMyCommands(for_all)
|
|
69
|
+
setMyCommands(for_adm, BotCommandScope.all_chat_administrators())
|
|
70
|
+
|
|
71
|
+
def get_my_commands(self, for_admin: bool = False):
|
|
72
|
+
i = 1 if for_admin else 0
|
|
73
|
+
r: list[tuple[str, Bot.tcmd_dsc]] = []
|
|
74
|
+
for key in self._commands.keys():
|
|
75
|
+
v = self._commands[key][1][i]
|
|
76
|
+
if v:
|
|
77
|
+
r.append((key, v))
|
|
78
|
+
return r
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def add_command(cls: Type[T], command: str, *, desc: tcmd_dsc | None = None, desc_adm: tcmd_dsc | None = None):
|
|
82
|
+
def wrapper(fn: Bot.tcmd_fn[T]):
|
|
83
|
+
if "<" in command:
|
|
84
|
+
parts: list[tuple[str, bool]] = []
|
|
85
|
+
param = None
|
|
86
|
+
for ch in command:
|
|
87
|
+
if ch == "<":
|
|
88
|
+
param = ""
|
|
89
|
+
elif ch == ">":
|
|
90
|
+
if param:
|
|
91
|
+
parts.append((param, True))
|
|
92
|
+
param = None
|
|
93
|
+
elif param is not None:
|
|
94
|
+
param += ch
|
|
95
|
+
else:
|
|
96
|
+
if not parts or parts[-1][1]:
|
|
97
|
+
parts.append(("", False))
|
|
98
|
+
parts[-1] = (parts[-1][0] + ch, False)
|
|
99
|
+
res = ""
|
|
100
|
+
varnames: list[str] = []
|
|
101
|
+
for part, isvar in parts:
|
|
102
|
+
if isvar:
|
|
103
|
+
res += "(.*)"
|
|
104
|
+
varnames.append(part)
|
|
105
|
+
else:
|
|
106
|
+
res += part
|
|
107
|
+
reg = re.compile(res, re.IGNORECASE)
|
|
108
|
+
fn.regex = reg # type: ignore
|
|
109
|
+
|
|
110
|
+
def comparer(cmd: str):
|
|
111
|
+
regex = cast(re.Pattern[str], fn.regex) # type: ignore
|
|
112
|
+
m = regex.match(cmd)
|
|
113
|
+
if not m or len(m.groups()) != len(varnames):
|
|
114
|
+
return None
|
|
115
|
+
return dict(zip(varnames, m.groups()))
|
|
116
|
+
fn.comparer = comparer # type: ignore
|
|
117
|
+
|
|
118
|
+
cls._commands[command] = (fn, (desc, desc_adm))
|
|
119
|
+
return fn
|
|
120
|
+
return wrapper
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def cmd_for_admin(cls: Type[T], fn: tcmd_fn[T]):
|
|
124
|
+
def wrapped(bot: T, args: BotCmdArgs, **kwargs: str):
|
|
125
|
+
if bot.chat is None or bot.sender is None:
|
|
126
|
+
return "403(500!)"
|
|
127
|
+
ok, r = getChatMember(bot.chat.id, bot.sender.id)
|
|
128
|
+
if not ok:
|
|
129
|
+
return "403(500)"
|
|
130
|
+
if r.status != "creator" and r.status != "administrator":
|
|
131
|
+
return cls.TextCmdForAdmin
|
|
132
|
+
return fn(bot, args, **kwargs)
|
|
133
|
+
return wrapped
|
|
134
|
+
|
|
135
|
+
def _process_update(self, update: Update):
|
|
136
|
+
self.update = update
|
|
137
|
+
self.message = Undefined.default(update.message)
|
|
138
|
+
self.callback_query = Undefined.default(update.callback_query)
|
|
139
|
+
self.inline_query = Undefined.default(update.inline_query)
|
|
140
|
+
self.chosen_inline_result = Undefined.default(update.chosen_inline_result)
|
|
141
|
+
self.sender = None
|
|
142
|
+
self.chat = None
|
|
143
|
+
self.logger._reset()
|
|
144
|
+
if self.message and self.message.text != "":
|
|
145
|
+
self.sender = Undefined.default(self.message.sender)
|
|
146
|
+
self.chat = self.message.chat
|
|
147
|
+
self._on_message()
|
|
148
|
+
if self.callback_query:
|
|
149
|
+
self.sender = self.callback_query.sender
|
|
150
|
+
self.chat = self.callback_query.message.chat if Undefined.defined(self.callback_query.message) else None
|
|
151
|
+
self._on_callback_query()
|
|
152
|
+
if self.inline_query:
|
|
153
|
+
self.sender = self.inline_query.sender
|
|
154
|
+
self._call_callback(self.on_inline_query)
|
|
155
|
+
if self.chosen_inline_result:
|
|
156
|
+
self.sender = self.chosen_inline_result.sender
|
|
157
|
+
self._call_callback(self.on_chosen_inline_result)
|
|
158
|
+
|
|
159
|
+
def _call_callback(self, key: Callable[..., Any]):
|
|
160
|
+
fn = self._callback.get(key)
|
|
161
|
+
if fn:
|
|
162
|
+
fn(self)
|
|
163
|
+
|
|
164
|
+
@classmethod
|
|
165
|
+
def on_message(cls: Type[T], fn: tcallback[T]):
|
|
166
|
+
cls._callback[cls.on_message] = fn
|
|
167
|
+
return fn
|
|
168
|
+
|
|
169
|
+
@classmethod
|
|
170
|
+
def on_inline_query(cls: Type[T], fn: tcallback[T]):
|
|
171
|
+
cls._callback[cls.on_inline_query] = fn
|
|
172
|
+
return fn
|
|
173
|
+
|
|
174
|
+
@classmethod
|
|
175
|
+
def on_chosen_inline_result(cls: Type[T], fn: tcallback[T]):
|
|
176
|
+
cls._callback[cls.on_chosen_inline_result] = fn
|
|
177
|
+
return fn
|
|
178
|
+
|
|
179
|
+
def _on_message(self):
|
|
180
|
+
assert self.message
|
|
181
|
+
if self.message.text.startswith("/"):
|
|
182
|
+
r = self._on_command(self.message.text[1:])
|
|
183
|
+
if r:
|
|
184
|
+
if isinstance(r, str):
|
|
185
|
+
self.sendMessage(r)
|
|
186
|
+
elif r is False and self.message.chat.type == "private":
|
|
187
|
+
self.sendMessage(self.TextWrongCommand)
|
|
188
|
+
else:
|
|
189
|
+
self._call_callback(self.on_message)
|
|
190
|
+
|
|
191
|
+
def _on_command(self, input: str):
|
|
192
|
+
args = BotCmdArgs(input)
|
|
193
|
+
if args.command == "":
|
|
194
|
+
return False
|
|
195
|
+
cmd, kwargs = self._find_command(args.command)
|
|
196
|
+
if not cmd:
|
|
197
|
+
return False
|
|
198
|
+
fn, _ = cmd
|
|
199
|
+
self.logger.cmd = args.command
|
|
200
|
+
r = fn(self, args, **kwargs)
|
|
201
|
+
if r:
|
|
202
|
+
return r
|
|
203
|
+
return True
|
|
204
|
+
|
|
205
|
+
def _find_command(self, cmd: str) -> tuple[_tcommand | None, dict[str, str]]:
|
|
206
|
+
c = self._commands.get(cmd, None)
|
|
207
|
+
if c:
|
|
208
|
+
return c, {}
|
|
209
|
+
for c in self._commands.values():
|
|
210
|
+
if hasattr(c[0], "comparer"):
|
|
211
|
+
kwargs = cast(dict[str, str] | None, c[0].comparer(cmd)) # type: ignore
|
|
212
|
+
if kwargs:
|
|
213
|
+
return c, kwargs
|
|
214
|
+
return None, {}
|
|
215
|
+
|
|
216
|
+
def _on_callback_query(self):
|
|
217
|
+
assert self.callback_query
|
|
218
|
+
r = self._on_command(Undefined.default(self.callback_query.data, ""))
|
|
219
|
+
if r:
|
|
220
|
+
self.answerCallbackQuery(r if isinstance(r, str) else None)
|
|
221
|
+
else:
|
|
222
|
+
self.answerCallbackQuery(self.TextWrongCommand)
|
|
223
|
+
|
|
224
|
+
def sendMessage(self, text: str, message_thread_id: int | None = None, use_markdown: bool = False,
|
|
225
|
+
reply_markup: InlineKeyboardMarkup | None = None, reply_parameters: ReplyParameters | None = None,
|
|
226
|
+
entities: List[MessageEntity] | None = None, chat_id: str | int | None = None):
|
|
227
|
+
if chat_id is None:
|
|
228
|
+
if self.message:
|
|
229
|
+
chat_id = self.message.chat.id
|
|
230
|
+
if message_thread_id is None and self.message.is_topic_message and Undefined.defined(self.message.message_thread_id):
|
|
231
|
+
message_thread_id = self.message.message_thread_id
|
|
232
|
+
elif self.callback_query and Undefined.defined(self.callback_query.message):
|
|
233
|
+
chat_id = self.callback_query.message.chat.id
|
|
234
|
+
if message_thread_id is None and Undefined.defined(self.callback_query.message.message_thread_id):
|
|
235
|
+
message_thread_id = self.callback_query.message.message_thread_id
|
|
236
|
+
else:
|
|
237
|
+
raise Exception("tgapi: cant send message without chat id")
|
|
238
|
+
return sendMessage(chat_id, text, message_thread_id, use_markdown, reply_markup, reply_parameters, entities)
|
|
239
|
+
|
|
240
|
+
def answerCallbackQuery(self, text: str | None = None, show_alert: bool = False, url: str | None = None, cache_time: int = 0):
|
|
241
|
+
if self.callback_query is None:
|
|
242
|
+
raise Exception("tgapi: Bot.answerCallbackQuery is avaible only inside on_callback_query")
|
|
243
|
+
return answerCallbackQuery(self.callback_query.id, text, show_alert, url, cache_time)
|
|
244
|
+
|
|
245
|
+
def answerInlineQuery(self, results: list[InlineQueryResult], cache_time: int = 300, is_personal: bool = False, next_offset: str | None = None):
|
|
246
|
+
if self.inline_query is None:
|
|
247
|
+
raise Exception("tgapi: Bot.answerInlineQuery is avaible only inside on_inline_query")
|
|
248
|
+
return answerInlineQuery(self.inline_query.id, results, cache_time, is_personal, next_offset)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@Bot.on_inline_query
|
|
252
|
+
def _(bot: Bot):
|
|
253
|
+
bot.answerInlineQuery([])
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class BotLogger(ParametrizedLogger):
|
|
257
|
+
user: User | None = None
|
|
258
|
+
cmd = ""
|
|
259
|
+
|
|
260
|
+
def _reset(self):
|
|
261
|
+
self.user = None
|
|
262
|
+
self.cmd = ""
|
|
263
|
+
|
|
264
|
+
def _get_args(self) -> dict[str, str | int]:
|
|
265
|
+
return {
|
|
266
|
+
"uid": self.user.id if self.user else -1,
|
|
267
|
+
"uname": self.user.username if self.user else "",
|
|
268
|
+
"cmd": self.cmd
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class BotCmdArgs(Iterable[str]):
|
|
273
|
+
input: str
|
|
274
|
+
args: list[str]
|
|
275
|
+
raw_args = ""
|
|
276
|
+
raw_argsI = -1
|
|
277
|
+
command = ""
|
|
278
|
+
|
|
279
|
+
def __init__(self, input: str):
|
|
280
|
+
self.input = input
|
|
281
|
+
self.args = [str.strip(v) for v in input.split()]
|
|
282
|
+
|
|
283
|
+
if len(self.args) == 0:
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
command = self.args[0]
|
|
287
|
+
mention = command.find("@")
|
|
288
|
+
if mention > 0:
|
|
289
|
+
bot_name = command[mention:]
|
|
290
|
+
if bot_name != get_bot_name():
|
|
291
|
+
return
|
|
292
|
+
command = command[:mention]
|
|
293
|
+
self.command = command
|
|
294
|
+
|
|
295
|
+
self.args = self.args[1:]
|
|
296
|
+
|
|
297
|
+
i = input.find(" ")
|
|
298
|
+
if i > 0:
|
|
299
|
+
while i < len(input) and input[i] == " ":
|
|
300
|
+
i += 1
|
|
301
|
+
self.raw_argsI = MessageEntity.len(input[:i])
|
|
302
|
+
self.raw_args = input[i:]
|
|
303
|
+
|
|
304
|
+
def __getitem__(self, i: int):
|
|
305
|
+
return self.args[i]
|
|
306
|
+
|
|
307
|
+
def __len__(self):
|
|
308
|
+
return len(self.args)
|
|
309
|
+
|
|
310
|
+
def __iter__(self):
|
|
311
|
+
for arg in self.args:
|
|
312
|
+
yield arg
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Any, Type, TypeVar
|
|
2
|
+
|
|
3
|
+
from bafser import db_session
|
|
4
|
+
from sqlalchemy.orm import Session
|
|
5
|
+
|
|
6
|
+
from .bot import Bot, BotCmdArgs
|
|
7
|
+
from .types import User
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T", bound="BotWithDB[Any]", covariant=True)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BotWithDB[TUser](Bot):
|
|
13
|
+
db_sess: Session | None = None
|
|
14
|
+
user: TUser | None = None
|
|
15
|
+
|
|
16
|
+
def get_user(self, db_sess: Session, sender: User) -> TUser:
|
|
17
|
+
raise Exception("tgapi: Method BotWithDB.get_user must be implemented in subclass")
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def cmd_connect_db(cls: Type[T], fn: Bot.tcmd_fn[T]):
|
|
21
|
+
def wrapped(bot: T, args: BotCmdArgs, **kwargs: str):
|
|
22
|
+
assert bot.sender
|
|
23
|
+
with db_session.create_session() as db_sess:
|
|
24
|
+
bot.db_sess = db_sess
|
|
25
|
+
bot.user = bot.get_user(db_sess, bot.sender)
|
|
26
|
+
return fn(bot, args, **kwargs)
|
|
27
|
+
return wrapped
|
bafser_tgapi/db/msg.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from bafser import IdMixin, Log, SqlAlchemyBase, Undefined, UserBase
|
|
6
|
+
from sqlalchemy import BigInteger, String
|
|
7
|
+
from sqlalchemy.orm import Mapped, Session, mapped_column
|
|
8
|
+
|
|
9
|
+
import bafser_tgapi
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MsgBase(SqlAlchemyBase, IdMixin):
|
|
13
|
+
__abstract__ = True
|
|
14
|
+
|
|
15
|
+
message_id: Mapped[int] = mapped_column()
|
|
16
|
+
message_thread_id: Mapped[Optional[int]] = mapped_column()
|
|
17
|
+
chat_id: Mapped[int] = mapped_column(BigInteger)
|
|
18
|
+
text: Mapped[Optional[str]] = mapped_column(String(512))
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def new(cls, creator: UserBase, message_id: int, chat_id: int, text: str | None = None, message_thread_id: int | None = None,
|
|
22
|
+
*_: Any, **kwargs: Any):
|
|
23
|
+
db_sess = Session.object_session(creator)
|
|
24
|
+
assert db_sess
|
|
25
|
+
msg, add_changes = cls._new(db_sess, message_id, chat_id, text, message_thread_id, **kwargs)
|
|
26
|
+
|
|
27
|
+
db_sess.add(msg)
|
|
28
|
+
Log.added(msg, creator, add_changes)
|
|
29
|
+
|
|
30
|
+
return msg
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def _new(cls, db_sess: Session, message_id: int, chat_id: int, text: str | None, message_thread_id: int | None, **kwargs: Any):
|
|
34
|
+
user = cls(message_id=message_id, chat_id=chat_id, text=text, message_thread_id=message_thread_id)
|
|
35
|
+
changes = [
|
|
36
|
+
("message_id", user.message_id),
|
|
37
|
+
("chat_id", user.chat_id),
|
|
38
|
+
("text", user.text),
|
|
39
|
+
("message_thread_id", user.message_thread_id),
|
|
40
|
+
]
|
|
41
|
+
return user, changes
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def new_from_data(cls, creator: UserBase, data: bafser_tgapi.Message):
|
|
45
|
+
return cls.new(creator, data.message_id, data.chat.id, data.text, Undefined.default(data.message_thread_id))
|
|
46
|
+
|
|
47
|
+
def delete(self, actor: UserBase, commit=True):
|
|
48
|
+
db_sess = Session.object_session(self)
|
|
49
|
+
assert db_sess
|
|
50
|
+
db_sess.delete(self)
|
|
51
|
+
Log.deleted(self, actor, commit=commit)
|
bafser_tgapi/db/user.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from typing import Any, override
|
|
2
|
+
|
|
3
|
+
from bafser import UserBase, UserKwargs, randstr
|
|
4
|
+
from sqlalchemy import BigInteger, String, func
|
|
5
|
+
from sqlalchemy.orm import Mapped, Session, mapped_column
|
|
6
|
+
|
|
7
|
+
from ..types import User
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TgUserBase(UserBase):
|
|
11
|
+
__abstract__ = True
|
|
12
|
+
_default_role = -1
|
|
13
|
+
id_tg: Mapped[int] = mapped_column(BigInteger, index=True, unique=True)
|
|
14
|
+
is_bot: Mapped[bool] = mapped_column()
|
|
15
|
+
first_name: Mapped[str] = mapped_column(String(128))
|
|
16
|
+
last_name: Mapped[str] = mapped_column(String(128))
|
|
17
|
+
username: Mapped[str] = mapped_column(String(128))
|
|
18
|
+
language_code: Mapped[str] = mapped_column(String(16))
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def new(cls, db_sess: Session, id_tg: int, is_bot: bool, first_name: str, last_name: str, username: str, language_code: str,
|
|
22
|
+
*_: Any, **__: Any):
|
|
23
|
+
fake_creator = UserBase.get_fake_system()
|
|
24
|
+
return super().new(fake_creator, str(id_tg), randstr(8), username, [cls._default_role], db_sess=db_sess,
|
|
25
|
+
id_tg=id_tg, is_bot=is_bot, first_name=first_name, last_name=last_name, username=username, language_code=language_code)
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
@override
|
|
29
|
+
def _new(cls, db_sess: Session, user_kwargs: UserKwargs, *,
|
|
30
|
+
id_tg: int, is_bot: bool, first_name: str, last_name: str, username: str, language_code: str, **kwargs: Any):
|
|
31
|
+
user = cls(**user_kwargs,
|
|
32
|
+
id_tg=id_tg, is_bot=is_bot, first_name=first_name, last_name=last_name, username=username, language_code=language_code)
|
|
33
|
+
changes = [
|
|
34
|
+
("id_tg", user.id_tg),
|
|
35
|
+
("is_bot", user.is_bot),
|
|
36
|
+
("first_name", user.first_name),
|
|
37
|
+
("last_name", user.last_name),
|
|
38
|
+
("username", user.username),
|
|
39
|
+
("language_code", user.language_code),
|
|
40
|
+
]
|
|
41
|
+
return user, changes
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
@override
|
|
45
|
+
def create_admin(cls, db_sess: Session):
|
|
46
|
+
return cls.new(db_sess, 0, False, "Админ", "", "admin", "en")
|
|
47
|
+
|
|
48
|
+
def __repr__(self):
|
|
49
|
+
return f"<User> [{self.id} {self.id_tg}] {self.username}"
|
|
50
|
+
|
|
51
|
+
def get_name(self):
|
|
52
|
+
return f"{self.first_name} {self.last_name}".strip()
|
|
53
|
+
|
|
54
|
+
def get_username(self):
|
|
55
|
+
if self.username != "":
|
|
56
|
+
return self.username
|
|
57
|
+
return self.get_name()
|
|
58
|
+
|
|
59
|
+
def get_tagname(self):
|
|
60
|
+
if self.username != "":
|
|
61
|
+
return f"@{self.username}"
|
|
62
|
+
return f"🥷 {self.get_name()}"
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def new_from_data(cls, db_sess: Session, data: "User"):
|
|
66
|
+
return cls.new(db_sess, data.id, data.is_bot, data.first_name, data.last_name, data.username, data.language_code)
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def get_by_id_tg(cls, db_sess: Session, id_tg: int):
|
|
70
|
+
return cls.query(db_sess).filter(cls.id_tg == id_tg).first()
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def get_by_username(cls, db_sess: Session, username: str):
|
|
74
|
+
if username.startswith("@"):
|
|
75
|
+
username = username[1:]
|
|
76
|
+
return cls.query(db_sess).filter(func.lower(cls.username) == username.lower()).first()
|
bafser_tgapi/methods.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
from typing import List, Union
|
|
2
|
+
|
|
3
|
+
from .types import *
|
|
4
|
+
from .utils import call
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# https://core.telegram.org/bots/api#getupdates
|
|
8
|
+
def getUpdates(offset: int = 0, timeout: int = 0):
|
|
9
|
+
ok, r = call("getUpdates", {"offset": offset, "timeout": timeout}, timeout=timeout + 5)
|
|
10
|
+
if not ok:
|
|
11
|
+
return False, r
|
|
12
|
+
return True, list(map(lambda x: Update.new(x).valid(), r["result"]))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# https://core.telegram.org/bots/api#getwebhookinfo
|
|
16
|
+
def getWebhookInfo():
|
|
17
|
+
ok, r = call("getWebhookInfo")
|
|
18
|
+
if not ok:
|
|
19
|
+
return False, r
|
|
20
|
+
return True, WebhookInfo.new(r["result"]).valid()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# https://core.telegram.org/bots/api#setwebhook
|
|
24
|
+
def setWebhook(url: str, secret_token: str | None = None, allowed_updates: list[str] | None = None):
|
|
25
|
+
return call("setWebhook", {"url": url, "secret_token": secret_token, "allowed_updates": allowed_updates})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# https://core.telegram.org/bots/api#deletewebhook
|
|
29
|
+
def deleteWebhook(drop_pending_updates: bool | None = None):
|
|
30
|
+
return call("deleteWebhook", {"drop_pending_updates": drop_pending_updates})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# https://core.telegram.org/bots/api#sendmessage
|
|
34
|
+
def sendMessage(chat_id: str | int, text: str, message_thread_id: int | None = None, use_markdown: bool = False,
|
|
35
|
+
reply_markup: InlineKeyboardMarkup | None = None, reply_parameters: ReplyParameters | None = None,
|
|
36
|
+
entities: List[MessageEntity] | None = None):
|
|
37
|
+
ok, r = call("sendMessage", {
|
|
38
|
+
"chat_id": chat_id,
|
|
39
|
+
"message_thread_id": message_thread_id,
|
|
40
|
+
"text": text,
|
|
41
|
+
"parse_mode": "MarkdownV2" if use_markdown else None,
|
|
42
|
+
"reply_markup": reply_markup,
|
|
43
|
+
"reply_parameters": reply_parameters,
|
|
44
|
+
"entities": entities,
|
|
45
|
+
})
|
|
46
|
+
if not ok:
|
|
47
|
+
return False, r
|
|
48
|
+
return True, Message.new(r["result"]).valid()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# https://core.telegram.org/bots/api#editmessagetext
|
|
52
|
+
def editMessageText(chat_id: Union[int, str], message_id: int, text: str, use_markdown: bool = False,
|
|
53
|
+
reply_markup: InlineKeyboardMarkup | None = None, entities: List[MessageEntity] | None = None):
|
|
54
|
+
ok, r = call("editMessageText", {
|
|
55
|
+
"chat_id": chat_id,
|
|
56
|
+
"message_id": message_id,
|
|
57
|
+
"text": text,
|
|
58
|
+
"parse_mode": "MarkdownV2" if use_markdown else None,
|
|
59
|
+
"reply_markup": reply_markup,
|
|
60
|
+
"entities": entities,
|
|
61
|
+
})
|
|
62
|
+
if not ok:
|
|
63
|
+
return False, r
|
|
64
|
+
return True, Message.new(r["result"]).valid()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# https://core.telegram.org/bots/api#editmessagetext
|
|
68
|
+
def editMessageText_inline(inline_message_id: str, text: str, use_markdown: bool = False, reply_markup: InlineKeyboardMarkup | None = None):
|
|
69
|
+
ok, r = call("editMessageText", {
|
|
70
|
+
"inline_message_id": inline_message_id,
|
|
71
|
+
"text": text,
|
|
72
|
+
"parse_mode": "MarkdownV2" if use_markdown else None,
|
|
73
|
+
"reply_markup": reply_markup,
|
|
74
|
+
})
|
|
75
|
+
if not ok:
|
|
76
|
+
return False, r
|
|
77
|
+
return True, Message.new(r["result"]).valid()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# https://core.telegram.org/bots/api#editmessagereplymarkup
|
|
81
|
+
def editMessageReplyMarkup(chat_id: Union[int, str], message_id: int, reply_markup: InlineKeyboardMarkup):
|
|
82
|
+
ok, r = call("editMessageReplyMarkup", {
|
|
83
|
+
"chat_id": chat_id,
|
|
84
|
+
"message_id": message_id,
|
|
85
|
+
"reply_markup": reply_markup,
|
|
86
|
+
})
|
|
87
|
+
if not ok:
|
|
88
|
+
return False, r
|
|
89
|
+
return True, Message.new(r["result"]).valid()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# https://core.telegram.org/bots/api#editmessagereplymarkup
|
|
93
|
+
def editMessageReplyMarkup_inline(inline_message_id: str, reply_markup: InlineKeyboardMarkup):
|
|
94
|
+
ok, r = call("editMessageReplyMarkup", {
|
|
95
|
+
"inline_message_id": inline_message_id,
|
|
96
|
+
"reply_markup": reply_markup,
|
|
97
|
+
})
|
|
98
|
+
if not ok:
|
|
99
|
+
return False, r
|
|
100
|
+
return True, Message.new(r["result"]).valid()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# https://core.telegram.org/bots/api#deletemessage
|
|
104
|
+
def deleteMessage(chat_id: Union[int, str], message_id: int):
|
|
105
|
+
ok, r = call("deleteMessage", {
|
|
106
|
+
"chat_id": chat_id,
|
|
107
|
+
"message_id": message_id,
|
|
108
|
+
})
|
|
109
|
+
if not ok:
|
|
110
|
+
return False, r
|
|
111
|
+
return True, r["result"]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# https://core.telegram.org/bots/api#answerinlinequery
|
|
115
|
+
def answerInlineQuery(
|
|
116
|
+
inline_query_id: str,
|
|
117
|
+
results: list[InlineQueryResult],
|
|
118
|
+
cache_time: int = 300,
|
|
119
|
+
is_personal: bool = False,
|
|
120
|
+
next_offset: str | None = None,
|
|
121
|
+
# button: InlineQueryResultsButton = None,
|
|
122
|
+
):
|
|
123
|
+
ok, r = call("answerInlineQuery", {
|
|
124
|
+
"inline_query_id": inline_query_id,
|
|
125
|
+
"results": results,
|
|
126
|
+
"cache_time": cache_time,
|
|
127
|
+
"is_personal": is_personal,
|
|
128
|
+
"next_offset": next_offset,
|
|
129
|
+
})
|
|
130
|
+
if not ok:
|
|
131
|
+
return False, r
|
|
132
|
+
return True, r["result"]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# https://core.telegram.org/bots/api#answercallbackquery
|
|
136
|
+
def answerCallbackQuery(
|
|
137
|
+
callback_query_id: str,
|
|
138
|
+
text: str | None = None,
|
|
139
|
+
show_alert: bool = False,
|
|
140
|
+
url: str | None = None,
|
|
141
|
+
cache_time: int = 0,
|
|
142
|
+
):
|
|
143
|
+
ok, r = call("answerCallbackQuery", {
|
|
144
|
+
"callback_query_id": callback_query_id,
|
|
145
|
+
"text": text,
|
|
146
|
+
"show_alert": show_alert,
|
|
147
|
+
"url": url,
|
|
148
|
+
"cache_time": cache_time,
|
|
149
|
+
})
|
|
150
|
+
if not ok:
|
|
151
|
+
return False, r
|
|
152
|
+
return True, r["result"]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# https://core.telegram.org/bots/api#setmycommands
|
|
156
|
+
def setMyCommands(commands: list[BotCommand], scope: BotCommandScope | None = None, language_code: str | None = None):
|
|
157
|
+
ok, r = call("setMyCommands", {
|
|
158
|
+
"commands": commands,
|
|
159
|
+
"scope": scope,
|
|
160
|
+
"language_code": language_code,
|
|
161
|
+
})
|
|
162
|
+
if not ok:
|
|
163
|
+
return False, r
|
|
164
|
+
return True, r["result"]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# https://core.telegram.org/bots/api#getchatmember
|
|
168
|
+
def getChatMember(chat_id: Union[str, int], user_id: int):
|
|
169
|
+
ok, r = call("getChatMember", {
|
|
170
|
+
"chat_id": chat_id,
|
|
171
|
+
"user_id": user_id,
|
|
172
|
+
})
|
|
173
|
+
if not ok:
|
|
174
|
+
return False, r
|
|
175
|
+
return True, ChatMember.new(r["result"]).valid()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# https://core.telegram.org/bots/api#pinchatmessage
|
|
179
|
+
def pinChatMessage(chat_id: Union[str, int], message_id: int, disable_notification: bool = True):
|
|
180
|
+
ok, r = call("pinChatMessage", {
|
|
181
|
+
"chat_id": chat_id,
|
|
182
|
+
"message_id": message_id,
|
|
183
|
+
"disable_notification": disable_notification,
|
|
184
|
+
})
|
|
185
|
+
if not ok:
|
|
186
|
+
return False, r
|
|
187
|
+
return True, r["result"]
|
bafser_tgapi/types.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Any, Literal, Union, override
|
|
3
|
+
|
|
4
|
+
from bafser import JsonObj, JsonOpt, Undefined
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class WebhookInfo(JsonObj):
|
|
8
|
+
# https://core.telegram.org/bots/api#webhookinfo
|
|
9
|
+
url: str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class User(JsonObj):
|
|
13
|
+
# https://core.telegram.org/bots/api#user
|
|
14
|
+
id: int
|
|
15
|
+
is_bot: bool
|
|
16
|
+
first_name: str
|
|
17
|
+
last_name: str = ""
|
|
18
|
+
username: str = ""
|
|
19
|
+
language_code: str = ""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Chat(JsonObj):
|
|
23
|
+
# https://core.telegram.org/bots/api#chat
|
|
24
|
+
id: int
|
|
25
|
+
type: Literal["private", "group", "supergroup", "channel"]
|
|
26
|
+
title: str = ""
|
|
27
|
+
is_forum: bool = False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class MessageEntity(JsonObj):
|
|
31
|
+
class _User(JsonObj):
|
|
32
|
+
id: int
|
|
33
|
+
|
|
34
|
+
Type = Literal["mention", "hashtag", "cashtag", "bot_command", "url", "email", "phone_number", "bold", "italic", "underline",
|
|
35
|
+
"strikethrough", "spoiler", "blockquote", "expandable_blockquote", "code", "pre", "text_link", "text_mention", "custom_emoji"]
|
|
36
|
+
# https://core.telegram.org/bots/api#messageentity
|
|
37
|
+
type: Type
|
|
38
|
+
offset: int
|
|
39
|
+
length: int
|
|
40
|
+
url: JsonOpt[str] = Undefined
|
|
41
|
+
user: JsonOpt[_User] = Undefined
|
|
42
|
+
language: JsonOpt[str] = Undefined
|
|
43
|
+
custom_emoji_id: JsonOpt[str] = Undefined
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def text_mention(offset: int, length: int, user_id: int):
|
|
47
|
+
return MessageEntity(type="text_mention", offset=offset, length=length, user=MessageEntity._User(id=user_id))
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def blockquote(offset: int, length: int):
|
|
51
|
+
return MessageEntity(type="blockquote", offset=offset, length=length)
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def spoiler(offset: int, length: int):
|
|
55
|
+
return MessageEntity(type="spoiler", offset=offset, length=length)
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def bold(offset: int, length: int):
|
|
59
|
+
return MessageEntity(type="bold", offset=offset, length=length)
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def italic(offset: int, length: int):
|
|
63
|
+
return MessageEntity(type="italic", offset=offset, length=length)
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def underline(offset: int, length: int):
|
|
67
|
+
return MessageEntity(type="underline", offset=offset, length=length)
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def len(text: str):
|
|
71
|
+
return len(MessageEntity.encode_text(text)) // 2
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def encode_text(text: str):
|
|
75
|
+
return text.encode("utf-16-le")
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def decode_text(text: bytes):
|
|
79
|
+
return text.decode("utf-16-le")
|
|
80
|
+
|
|
81
|
+
def get_msg_text(self, msg: str):
|
|
82
|
+
text = MessageEntity.encode_text(msg)
|
|
83
|
+
s = self.offset * 2 - 2
|
|
84
|
+
e = s + self.length * 2
|
|
85
|
+
return MessageEntity.decode_text(text[s:e])
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Message(JsonObj):
|
|
89
|
+
# https://core.telegram.org/bots/api#message
|
|
90
|
+
__datetime_parser__ = datetime.fromtimestamp
|
|
91
|
+
message_id: int
|
|
92
|
+
message_thread_id: JsonOpt[int]
|
|
93
|
+
sender: JsonOpt[User]
|
|
94
|
+
chat: Chat
|
|
95
|
+
reply_to_message: JsonOpt["Message"]
|
|
96
|
+
is_topic_message: bool = False
|
|
97
|
+
text: str = ""
|
|
98
|
+
date: datetime
|
|
99
|
+
entities: list[MessageEntity] = []
|
|
100
|
+
|
|
101
|
+
@override
|
|
102
|
+
def _parse(self, key: str, v: Any, json: dict[str, Any]):
|
|
103
|
+
if key == "from":
|
|
104
|
+
return "sender", v
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class InaccessibleMessage(JsonObj):
|
|
108
|
+
# https://core.telegram.org/bots/api#inaccessiblemessage
|
|
109
|
+
__datetime_parser__ = datetime.fromtimestamp
|
|
110
|
+
message_id: int
|
|
111
|
+
chat: Chat
|
|
112
|
+
date: datetime
|
|
113
|
+
message_thread_id = Undefined
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class InlineQuery(JsonObj):
|
|
117
|
+
# https://core.telegram.org/bots/api#inlinequery
|
|
118
|
+
id: str
|
|
119
|
+
sender: User
|
|
120
|
+
query: str
|
|
121
|
+
offset: str
|
|
122
|
+
chat_type: JsonOpt[Literal["sender", "private", "group", "supergroup", "channel"]]
|
|
123
|
+
|
|
124
|
+
@override
|
|
125
|
+
def _parse(self, key: str, v: Any, json: dict[str, Any]):
|
|
126
|
+
if key == "from":
|
|
127
|
+
return "sender", v
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
type MaybeInaccessibleMessage = Message | InaccessibleMessage
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class CallbackQuery(JsonObj):
|
|
134
|
+
# https://core.telegram.org/bots/api#callbackquery
|
|
135
|
+
id: str
|
|
136
|
+
sender: User
|
|
137
|
+
message: JsonOpt[MaybeInaccessibleMessage]
|
|
138
|
+
inline_message_id: JsonOpt[str]
|
|
139
|
+
chat_instance: str
|
|
140
|
+
data: JsonOpt[str]
|
|
141
|
+
game_short_name: JsonOpt[str]
|
|
142
|
+
|
|
143
|
+
@override
|
|
144
|
+
def _parse(self, key: str, v: Any, json: dict[str, Any]):
|
|
145
|
+
if key == "from":
|
|
146
|
+
return "sender", v
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class ChosenInlineResult(JsonObj):
|
|
150
|
+
# https://core.telegram.org/bots/api#choseninlineresult
|
|
151
|
+
result_id: str
|
|
152
|
+
sender: User
|
|
153
|
+
# location: Location
|
|
154
|
+
inline_message_id: JsonOpt[str]
|
|
155
|
+
query: str
|
|
156
|
+
|
|
157
|
+
@override
|
|
158
|
+
def _parse(self, key: str, v: Any, json: dict[str, Any]):
|
|
159
|
+
if key == "from":
|
|
160
|
+
return "sender", v
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class Update(JsonObj):
|
|
164
|
+
# https://core.telegram.org/bots/api#update
|
|
165
|
+
update_id: int
|
|
166
|
+
message: JsonOpt[Message]
|
|
167
|
+
inline_query: JsonOpt[InlineQuery]
|
|
168
|
+
callback_query: JsonOpt[CallbackQuery]
|
|
169
|
+
chosen_inline_result: JsonOpt[ChosenInlineResult]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class InputTextMessageContent(JsonObj):
|
|
173
|
+
# https://core.telegram.org/bots/api#inputtextmessagecontent
|
|
174
|
+
message_text: str
|
|
175
|
+
parse_mode: JsonOpt[Literal["MarkdownV2", "HTML", "Markdown"]] = Undefined
|
|
176
|
+
entities: JsonOpt[list[MessageEntity]] = Undefined
|
|
177
|
+
# link_preview_options: LinkPreviewOptions
|
|
178
|
+
|
|
179
|
+
@staticmethod
|
|
180
|
+
def nw(message_text: str, use_markdown: bool = False):
|
|
181
|
+
return InputTextMessageContent(
|
|
182
|
+
message_text=message_text,
|
|
183
|
+
parse_mode="MarkdownV2" if use_markdown else Undefined
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
type InputMessageContent = InputTextMessageContent
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class CallbackGame(JsonObj):
|
|
191
|
+
# https://core.telegram.org/bots/api#callbackgame
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class CopyTextButton(JsonObj):
|
|
196
|
+
# https://core.telegram.org/bots/api#copytextbutton
|
|
197
|
+
text: str
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class InlineKeyboardButton(JsonObj):
|
|
201
|
+
# https://core.telegram.org/bots/api#inlinekeyboardbutton
|
|
202
|
+
text: str
|
|
203
|
+
url: JsonOpt[str] = Undefined
|
|
204
|
+
callback_data: JsonOpt[str] = Undefined
|
|
205
|
+
# web_app: WebAppInfo
|
|
206
|
+
# login_url: LoginUrl
|
|
207
|
+
switch_inline_query: JsonOpt[str] = Undefined
|
|
208
|
+
switch_inline_query_current_chat: JsonOpt[str] = Undefined
|
|
209
|
+
# switch_inline_query_chosen_chat: SwitchInlineQueryChosenChat
|
|
210
|
+
copy_text: JsonOpt[CopyTextButton] = Undefined
|
|
211
|
+
callback_game: JsonOpt[CallbackGame] = Undefined
|
|
212
|
+
# pay: bool
|
|
213
|
+
|
|
214
|
+
@staticmethod
|
|
215
|
+
def callback(text: str, callback_data: str):
|
|
216
|
+
return InlineKeyboardButton(text=text, callback_data=callback_data)
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
def inline_query_current_chat(text: str, query: str):
|
|
220
|
+
return InlineKeyboardButton(text=text, switch_inline_query_current_chat=query)
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def run_game(text: str):
|
|
224
|
+
return InlineKeyboardButton(text=text, callback_game=CallbackGame())
|
|
225
|
+
|
|
226
|
+
@staticmethod
|
|
227
|
+
def open_url(text: str, url: str):
|
|
228
|
+
return InlineKeyboardButton(text=text, url=url)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class InlineKeyboardMarkup(JsonObj):
|
|
232
|
+
# https://core.telegram.org/bots/api#inlinekeyboardmarkup
|
|
233
|
+
inline_keyboard: list[list[InlineKeyboardButton]]
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class InlineQueryResult:
|
|
237
|
+
# https://core.telegram.org/bots/api#inlinequeryresult
|
|
238
|
+
pass
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class InlineQueryResultArticle(JsonObj, InlineQueryResult):
|
|
242
|
+
# https://core.telegram.org/bots/api#inlinequeryresultarticle
|
|
243
|
+
type: Literal["article"] = JsonObj.field(default="article", init=False)
|
|
244
|
+
id: str
|
|
245
|
+
title: str
|
|
246
|
+
input_message_content: InputMessageContent
|
|
247
|
+
reply_markup: JsonOpt[InlineKeyboardMarkup] = Undefined
|
|
248
|
+
url: JsonOpt[str] = Undefined
|
|
249
|
+
description: JsonOpt[str] = Undefined
|
|
250
|
+
thumbnail_url: JsonOpt[str] = Undefined
|
|
251
|
+
thumbnail_width: JsonOpt[int] = Undefined
|
|
252
|
+
thumbnail_height: JsonOpt[int] = Undefined
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class InlineQueryResultGame(JsonObj, InlineQueryResult):
|
|
256
|
+
# https://core.telegram.org/bots/api#inlinequeryresultgame
|
|
257
|
+
type: Literal["game"] = JsonObj.field(default="game", init=False)
|
|
258
|
+
id: str
|
|
259
|
+
game_short_name: str
|
|
260
|
+
reply_markup: JsonOpt[InlineKeyboardMarkup] = Undefined
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class BotCommand(JsonObj):
|
|
264
|
+
# https://core.telegram.org/bots/api#botcommand
|
|
265
|
+
command: str # 1-32 characters. Can contain only lowercase English letters, digits and underscores.
|
|
266
|
+
description: str # 1-256 characters.
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class ChatMember(JsonObj):
|
|
270
|
+
# https://core.telegram.org/bots/api#chatmember
|
|
271
|
+
status: Literal["creator", "administrator", "member", "restricted", "left", "kicked"]
|
|
272
|
+
user: User
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
BotCommandScopeType = Literal["default", "all_private_chats", "all_group_chats",
|
|
276
|
+
"all_chat_administrators", "chat", "chat_administrators", "chat_member"]
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class BotCommandScope(JsonObj):
|
|
280
|
+
# https://core.telegram.org/bots/api#botcommandscope
|
|
281
|
+
type: BotCommandScopeType
|
|
282
|
+
chat_id: JsonOpt[Union[str, int]] = Undefined
|
|
283
|
+
user_id: JsonOpt[int] = Undefined
|
|
284
|
+
|
|
285
|
+
@staticmethod
|
|
286
|
+
def default():
|
|
287
|
+
return BotCommandScope(type="default")
|
|
288
|
+
|
|
289
|
+
@staticmethod
|
|
290
|
+
def all_private_chats():
|
|
291
|
+
return BotCommandScope(type="all_private_chats")
|
|
292
|
+
|
|
293
|
+
@staticmethod
|
|
294
|
+
def all_group_chats():
|
|
295
|
+
return BotCommandScope(type="all_group_chats")
|
|
296
|
+
|
|
297
|
+
@staticmethod
|
|
298
|
+
def all_chat_administrators():
|
|
299
|
+
return BotCommandScope(type="all_chat_administrators")
|
|
300
|
+
|
|
301
|
+
@staticmethod
|
|
302
|
+
def chat(chat_id: Union[str, int]):
|
|
303
|
+
return BotCommandScope(type="chat", chat_id=chat_id)
|
|
304
|
+
|
|
305
|
+
@staticmethod
|
|
306
|
+
def chat_administrators(chat_id: Union[str, int]):
|
|
307
|
+
return BotCommandScope(type="chat_administrators", chat_id=chat_id)
|
|
308
|
+
|
|
309
|
+
@staticmethod
|
|
310
|
+
def chat_member(chat_id: Union[str, int], user_id: int):
|
|
311
|
+
return BotCommandScope(type="chat_member", chat_id=chat_id, user_id=user_id)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class ReplyParameters(JsonObj):
|
|
315
|
+
# https://core.telegram.org/bots/api#replyparameters
|
|
316
|
+
message_id: int
|
bafser_tgapi/utils.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Type
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from bafser import JsonObj, Undefined, response_msg
|
|
8
|
+
from flask import Flask, g, request
|
|
9
|
+
|
|
10
|
+
from .types import Update
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .bot import Bot
|
|
14
|
+
|
|
15
|
+
bot_token = ""
|
|
16
|
+
bot_name = ""
|
|
17
|
+
webhook_token = ""
|
|
18
|
+
url = ""
|
|
19
|
+
|
|
20
|
+
bot: "Bot | None" = None
|
|
21
|
+
webhook_route = "/webhook"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def setup(config_path: str = "config.txt", botCls: Type["Bot"] | None = None, import_folder: str | None = None, app: Flask | None = None):
|
|
25
|
+
global bot_token, bot_name, webhook_token, url, bot
|
|
26
|
+
try:
|
|
27
|
+
data = read_config(config_path)
|
|
28
|
+
bot_token = data["bot_token"]
|
|
29
|
+
bot_name = data["bot_name"]
|
|
30
|
+
webhook_token = data["webhook_token"]
|
|
31
|
+
url = data["url"].strip("/") + "/"
|
|
32
|
+
except Exception as e:
|
|
33
|
+
logging.error(f"Cant read config\n{e}")
|
|
34
|
+
raise e
|
|
35
|
+
|
|
36
|
+
if import_folder:
|
|
37
|
+
if not os.path.exists(import_folder):
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
def import_dir(path: str):
|
|
41
|
+
import_module = path.replace("/", ".").replace("\\", ".")
|
|
42
|
+
for file in os.listdir(path):
|
|
43
|
+
fpath = os.path.join(path, file)
|
|
44
|
+
if os.path.isdir(fpath):
|
|
45
|
+
import_dir(fpath)
|
|
46
|
+
continue
|
|
47
|
+
if not file.endswith(".py"):
|
|
48
|
+
continue
|
|
49
|
+
module = import_module + "." + file[:-3]
|
|
50
|
+
importlib.import_module(module)
|
|
51
|
+
|
|
52
|
+
import_dir(import_folder)
|
|
53
|
+
|
|
54
|
+
if botCls:
|
|
55
|
+
bot = botCls()
|
|
56
|
+
bot.init()
|
|
57
|
+
|
|
58
|
+
if app:
|
|
59
|
+
app.post(webhook_route)(webhook)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def read_config(path: str):
|
|
63
|
+
data: dict[str, str] = {}
|
|
64
|
+
with open(path) as f:
|
|
65
|
+
for line in f:
|
|
66
|
+
if "=" not in line:
|
|
67
|
+
continue
|
|
68
|
+
i = line.index("=")
|
|
69
|
+
key, value = line[:i], line[i + 1:]
|
|
70
|
+
data[key.strip().replace(" ", "_")] = value.strip()
|
|
71
|
+
return data
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def check_webhook_token(token: str):
|
|
75
|
+
return token == webhook_token
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_url(path: str):
|
|
79
|
+
while path.startswith("/"):
|
|
80
|
+
path = path[1:]
|
|
81
|
+
return url + path
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_bot_name():
|
|
85
|
+
return bot_name
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def process_update(update: Update):
|
|
89
|
+
if not bot:
|
|
90
|
+
raise Exception("tgapi: cant process update without Bot specified in setup")
|
|
91
|
+
bot._process_update(update)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def run_long_polling():
|
|
95
|
+
from .methods import getUpdates
|
|
96
|
+
print("listening for updates...")
|
|
97
|
+
update_id = -1
|
|
98
|
+
while True:
|
|
99
|
+
ok, updates = getUpdates(update_id + 1, 60)
|
|
100
|
+
if not ok:
|
|
101
|
+
print("Error!", updates)
|
|
102
|
+
break
|
|
103
|
+
for update in updates:
|
|
104
|
+
update_id = max(update_id, update.update_id)
|
|
105
|
+
print(f"Update(update_id={update.update_id}, {", ".join(k for k, v in update.items() if Undefined.default(v) and k != "update_id")})")
|
|
106
|
+
process_update(update)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def webhook():
|
|
110
|
+
token = request.headers.get("X-Telegram-Bot-Api-Secret-Token", "")
|
|
111
|
+
if (not check_webhook_token(token)):
|
|
112
|
+
return response_msg("wrong token", 403)
|
|
113
|
+
|
|
114
|
+
values, is_json = g.json
|
|
115
|
+
if not is_json:
|
|
116
|
+
return response_msg("body is not json", 415)
|
|
117
|
+
|
|
118
|
+
logging.info(f"webhook: {values}")
|
|
119
|
+
process_update(Update.new(values).valid())
|
|
120
|
+
return "ok"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def call(method: str, data: JsonObj | dict[str, Any] | None = None, timeout: int | None = None):
|
|
124
|
+
if timeout is not None and timeout <= 0:
|
|
125
|
+
timeout = None
|
|
126
|
+
json = None
|
|
127
|
+
if isinstance(data, dict):
|
|
128
|
+
json = __item_to_json__(data)
|
|
129
|
+
elif data:
|
|
130
|
+
json = data.json()
|
|
131
|
+
try:
|
|
132
|
+
r = requests.post(f"https://api.telegram.org/bot{bot_token}/{method}", json=json, timeout=timeout)
|
|
133
|
+
if not r.ok:
|
|
134
|
+
logging.error(f"tgapi: {method} [{r.status_code}]\t{json}; {r.content}")
|
|
135
|
+
return False, r.json()
|
|
136
|
+
rj = r.json()
|
|
137
|
+
logging.info(f"tgapi: {method}\t{json} -> {rj}")
|
|
138
|
+
return True, rj
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logging.error(f"tgapi call error\n{e}")
|
|
141
|
+
raise Exception("tgapi call error")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def __item_to_json__(item: Any) -> Any:
|
|
145
|
+
if isinstance(item, dict):
|
|
146
|
+
r = {}
|
|
147
|
+
for field, v in item.items():
|
|
148
|
+
v = __item_to_json__(v)
|
|
149
|
+
if v is not None:
|
|
150
|
+
r[field] = v
|
|
151
|
+
return r
|
|
152
|
+
if isinstance(item, (list, tuple)):
|
|
153
|
+
return [__item_to_json__(v) for v in item if v is not None]
|
|
154
|
+
if isinstance(item, JsonObj):
|
|
155
|
+
return item.json()
|
|
156
|
+
return item
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def set_webhook(allowed_updates: list[str] | None = None):
|
|
160
|
+
from .methods import setWebhook
|
|
161
|
+
ok, r = setWebhook(get_url(webhook_route), webhook_token, allowed_updates)
|
|
162
|
+
if not ok:
|
|
163
|
+
raise Exception(f"tgapi: cant set webhook\n{r}")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def configure_webhook(set: bool, allowed_updates: list[str] | None = None, *, config_path: str | None = None):
|
|
167
|
+
global bot_token, bot_name, webhook_token, url
|
|
168
|
+
from .methods import deleteWebhook, setWebhook
|
|
169
|
+
if config_path:
|
|
170
|
+
try:
|
|
171
|
+
config = read_config(config_path)
|
|
172
|
+
bot_token = config["bot_token"]
|
|
173
|
+
bot_name = config["bot_name"]
|
|
174
|
+
webhook_token = config["webhook_token"]
|
|
175
|
+
url = config["url"].strip("/") + "/"
|
|
176
|
+
except Exception as e:
|
|
177
|
+
print(f"Cant read config\n{e}")
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
if set:
|
|
181
|
+
ok, r = setWebhook(get_url(webhook_route), webhook_token, allowed_updates)
|
|
182
|
+
else:
|
|
183
|
+
ok, r = deleteWebhook(True)
|
|
184
|
+
|
|
185
|
+
print(f"{ok}\n {r}")
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bafser_tgapi
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: bafser tg api
|
|
5
|
+
Project-URL: Homepage, https://github.com/MixelTe/bafser_tgapi
|
|
6
|
+
Project-URL: Issues, https://github.com/MixelTe/bafser_tgapi/issues
|
|
7
|
+
Author: Mixel Te
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Requires-Python: ==3.12.*
|
|
13
|
+
Requires-Dist: bafser==2.2.11
|
|
14
|
+
Requires-Dist: requests
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# bafser tgapi
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## usage
|
|
21
|
+
init project: `bafser init_project`
|
|
22
|
+
set webhook: `bafser configure_webhook set`
|
|
23
|
+
delete webhook: `bafser configure_webhook delete`
|
|
24
|
+
|
|
25
|
+
main.py
|
|
26
|
+
```py
|
|
27
|
+
import sys
|
|
28
|
+
|
|
29
|
+
from bafser import AppConfig, create_app
|
|
30
|
+
import bafser_tgapi as tgapi
|
|
31
|
+
|
|
32
|
+
from bot.bot import Bot
|
|
33
|
+
from scripts.init_db import init_db
|
|
34
|
+
|
|
35
|
+
app, run = create_app(__name__, AppConfig(DEV_MODE="dev" in sys.argv))
|
|
36
|
+
|
|
37
|
+
tgapi.setup(
|
|
38
|
+
config_path="config_dev.txt" if __name__ == "__main__" else "config.txt",
|
|
39
|
+
botCls=Bot,
|
|
40
|
+
import_folder="bot",
|
|
41
|
+
app=app,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
run(False, init_db)
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
tgapi.run_long_polling()
|
|
48
|
+
else:
|
|
49
|
+
tgapi.set_webhook()
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
init_db.py
|
|
54
|
+
```py
|
|
55
|
+
from bafser import AppConfig
|
|
56
|
+
from sqlalchemy.orm import Session
|
|
57
|
+
|
|
58
|
+
from data.user import Roles, User
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def init_db(db_sess: Session, config: AppConfig):
|
|
62
|
+
u = User.new(db_sess, 12345, False, "Admin", "", "username", "en")
|
|
63
|
+
u.add_role(u, Roles.admin)
|
|
64
|
+
|
|
65
|
+
db_sess.commit()
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
data.user.py
|
|
70
|
+
```py
|
|
71
|
+
from bafser_tgapi import TgUserBase
|
|
72
|
+
|
|
73
|
+
from data._roles import Roles
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class User(TgUserBase):
|
|
77
|
+
_default_role = Roles.user
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
data.msg.py
|
|
82
|
+
```py
|
|
83
|
+
from bafser_tgapi import MsgBase
|
|
84
|
+
|
|
85
|
+
from data._tables import Tables
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Msg(MsgBase):
|
|
89
|
+
__tablename__ = Tables.Msg
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
bot.py
|
|
94
|
+
```py
|
|
95
|
+
from typing import override
|
|
96
|
+
|
|
97
|
+
from bafser import Log
|
|
98
|
+
import bafser_tgapi as tgapi
|
|
99
|
+
from sqlalchemy.orm import Session
|
|
100
|
+
|
|
101
|
+
from data.user import User
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class Bot(tgapi.BotWithDB[User]):
|
|
105
|
+
@override
|
|
106
|
+
def get_user(self, db_sess: Session, sender: tgapi.User) -> User:
|
|
107
|
+
user = User.get_by_id_tg(db_sess, sender.id)
|
|
108
|
+
if user is None:
|
|
109
|
+
user = User.new_from_data(db_sess, sender)
|
|
110
|
+
if user.username != sender.username:
|
|
111
|
+
old_username = user.username
|
|
112
|
+
user.username = sender.username
|
|
113
|
+
Log.updated(user, user, [("username", old_username, user.username)])
|
|
114
|
+
return user
|
|
115
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
bafser_tgapi/__init__.py,sha256=r7sYqKWWrFfqeD7X3n3HTDSuD77o4qMTxP7JRUhu4hg,307
|
|
2
|
+
bafser_tgapi/bot.py,sha256=c7Y2owbHY93OfR0jpJBUQ7P3ID7Qn9jiRS1Q833aMoc,11582
|
|
3
|
+
bafser_tgapi/bot_with_db.py,sha256=MOBrHlz82MHi6oteITFKUi4bfCw_-WBJliiM13isp_M,868
|
|
4
|
+
bafser_tgapi/methods.py,sha256=jYnqJlgANZYh6xpeF-AUQfVcdrnH1WdGolfdzsC9XG0,6133
|
|
5
|
+
bafser_tgapi/types.py,sha256=vumTJMSc8iOFduUATZnS7fpKfhqJF1tUkdTlUeHHPco,9512
|
|
6
|
+
bafser_tgapi/utils.py,sha256=xIteUekxklgLMjiVSVskzJYei9j8istgr791o4vYVJs,5409
|
|
7
|
+
bafser_tgapi/db/msg.py,sha256=fTSt794LJp_UUxJqgl-cjCs0kpjZwGVvm6thSdY5C5Q,1902
|
|
8
|
+
bafser_tgapi/db/user.py,sha256=YiY9H2bTSSaGRPpSsK2JK5jj2SILdrkr_v1dPXvndc0,3018
|
|
9
|
+
bafser_tgapi-1.0.0.dist-info/METADATA,sha256=Mvp8I2dp1G8Fy4r42LeuZMMwuC_aJT0RAem7WmNNqPQ,2375
|
|
10
|
+
bafser_tgapi-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
11
|
+
bafser_tgapi-1.0.0.dist-info/licenses/LICENSE,sha256=KnwobioUD95na9chjt9X3VNZRoNm373L-htaEgrQkCc,1064
|
|
12
|
+
bafser_tgapi-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 MixelTe
|
|
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.
|