bafser-tgapi 1.0.0__tar.gz
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-1.0.0/.github/workflows/main.yml +34 -0
- bafser_tgapi-1.0.0/.gitignore +7 -0
- bafser_tgapi-1.0.0/LICENSE +21 -0
- bafser_tgapi-1.0.0/PKG-INFO +115 -0
- bafser_tgapi-1.0.0/README.md +99 -0
- bafser_tgapi-1.0.0/pyproject.toml +27 -0
- bafser_tgapi-1.0.0/src/bafser_tgapi/__init__.py +7 -0
- bafser_tgapi-1.0.0/src/bafser_tgapi/bot.py +312 -0
- bafser_tgapi-1.0.0/src/bafser_tgapi/bot_with_db.py +27 -0
- bafser_tgapi-1.0.0/src/bafser_tgapi/db/msg.py +51 -0
- bafser_tgapi-1.0.0/src/bafser_tgapi/db/user.py +76 -0
- bafser_tgapi-1.0.0/src/bafser_tgapi/methods.py +187 -0
- bafser_tgapi-1.0.0/src/bafser_tgapi/types.py +316 -0
- bafser_tgapi-1.0.0/src/bafser_tgapi/utils.py +185 -0
- bafser_tgapi-1.0.0/vs.code-workspace +29 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: Publish Python 🐍 distribution 📦 to PyPI
|
|
2
|
+
|
|
3
|
+
on: [push]
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
build:
|
|
7
|
+
name: >-
|
|
8
|
+
Publish Python 🐍 distribution 📦 to PyPI
|
|
9
|
+
if: startsWith(github.ref, 'refs/tags/v') # only publish to PyPI on tag pushes
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment:
|
|
12
|
+
name: pypi
|
|
13
|
+
url: https://pypi.org/p/bafser_tgapi
|
|
14
|
+
permissions:
|
|
15
|
+
id-token: write # IMPORTANT: mandatory for trusted publishing
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
with:
|
|
20
|
+
persist-credentials: false
|
|
21
|
+
- name: Set up Python
|
|
22
|
+
uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: "3.x"
|
|
25
|
+
- name: Install pypa/build
|
|
26
|
+
run: >-
|
|
27
|
+
python3 -m
|
|
28
|
+
pip install
|
|
29
|
+
build
|
|
30
|
+
--user
|
|
31
|
+
- name: Build a binary wheel and a source tarball
|
|
32
|
+
run: python3 -m build
|
|
33
|
+
- name: Publish distribution 📦 to PyPI
|
|
34
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -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.
|
|
@@ -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,99 @@
|
|
|
1
|
+
# bafser tgapi
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
## usage
|
|
5
|
+
init project: `bafser init_project`
|
|
6
|
+
set webhook: `bafser configure_webhook set`
|
|
7
|
+
delete webhook: `bafser configure_webhook delete`
|
|
8
|
+
|
|
9
|
+
main.py
|
|
10
|
+
```py
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
from bafser import AppConfig, create_app
|
|
14
|
+
import bafser_tgapi as tgapi
|
|
15
|
+
|
|
16
|
+
from bot.bot import Bot
|
|
17
|
+
from scripts.init_db import init_db
|
|
18
|
+
|
|
19
|
+
app, run = create_app(__name__, AppConfig(DEV_MODE="dev" in sys.argv))
|
|
20
|
+
|
|
21
|
+
tgapi.setup(
|
|
22
|
+
config_path="config_dev.txt" if __name__ == "__main__" else "config.txt",
|
|
23
|
+
botCls=Bot,
|
|
24
|
+
import_folder="bot",
|
|
25
|
+
app=app,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
run(False, init_db)
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
tgapi.run_long_polling()
|
|
32
|
+
else:
|
|
33
|
+
tgapi.set_webhook()
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
init_db.py
|
|
38
|
+
```py
|
|
39
|
+
from bafser import AppConfig
|
|
40
|
+
from sqlalchemy.orm import Session
|
|
41
|
+
|
|
42
|
+
from data.user import Roles, User
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def init_db(db_sess: Session, config: AppConfig):
|
|
46
|
+
u = User.new(db_sess, 12345, False, "Admin", "", "username", "en")
|
|
47
|
+
u.add_role(u, Roles.admin)
|
|
48
|
+
|
|
49
|
+
db_sess.commit()
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
data.user.py
|
|
54
|
+
```py
|
|
55
|
+
from bafser_tgapi import TgUserBase
|
|
56
|
+
|
|
57
|
+
from data._roles import Roles
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class User(TgUserBase):
|
|
61
|
+
_default_role = Roles.user
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
data.msg.py
|
|
66
|
+
```py
|
|
67
|
+
from bafser_tgapi import MsgBase
|
|
68
|
+
|
|
69
|
+
from data._tables import Tables
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Msg(MsgBase):
|
|
73
|
+
__tablename__ = Tables.Msg
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
bot.py
|
|
78
|
+
```py
|
|
79
|
+
from typing import override
|
|
80
|
+
|
|
81
|
+
from bafser import Log
|
|
82
|
+
import bafser_tgapi as tgapi
|
|
83
|
+
from sqlalchemy.orm import Session
|
|
84
|
+
|
|
85
|
+
from data.user import User
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Bot(tgapi.BotWithDB[User]):
|
|
89
|
+
@override
|
|
90
|
+
def get_user(self, db_sess: Session, sender: tgapi.User) -> User:
|
|
91
|
+
user = User.get_by_id_tg(db_sess, sender.id)
|
|
92
|
+
if user is None:
|
|
93
|
+
user = User.new_from_data(db_sess, sender)
|
|
94
|
+
if user.username != sender.username:
|
|
95
|
+
old_username = user.username
|
|
96
|
+
user.username = sender.username
|
|
97
|
+
Log.updated(user, user, [("username", old_username, user.username)])
|
|
98
|
+
return user
|
|
99
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling >= 1.26"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bafser_tgapi"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Mixel Te" },
|
|
10
|
+
]
|
|
11
|
+
description = "bafser tg api"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = "== 3.12.*"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
]
|
|
18
|
+
license = "MIT"
|
|
19
|
+
license-files = ["LICEN[CS]E*"]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"bafser==2.2.11",
|
|
22
|
+
"requests",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/MixelTe/bafser_tgapi"
|
|
27
|
+
Issues = "https://github.com/MixelTe/bafser_tgapi/issues"
|
|
@@ -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
|
|
@@ -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
|