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.
@@ -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)
@@ -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()
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.