fastrub 5.2.2__tar.gz → 5.2.4__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.
- {fastrub-5.2.2 → fastrub-5.2.4}/PKG-INFO +1 -1
- fastrub-5.2.4/fast_rub/core/conversation.py +437 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/forms/manager.py +131 -144
- fastrub-5.2.4/fast_rub/utils/version.py +1 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fastrub.egg-info/PKG-INFO +1 -1
- fastrub-5.2.2/fast_rub/core/conversation.py +0 -160
- fastrub-5.2.2/fast_rub/utils/version.py +0 -1
- {fastrub-5.2.2 → fastrub-5.2.4}/LICENSE +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/README.md +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/button/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/button/key_pad.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/cli.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/async_sync.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/background.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/client.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/forms/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/forms/config.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/forms/data_form.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/forms/decorators.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/forms/fields.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/helpers.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/hotreload.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/middleware.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/plugins.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/scheduler.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/signals.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/webhook_server.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/db/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/db/database.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/db/message_keeper.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/db/session.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/network/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/network/network.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrightconfig.json +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/client/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/client/client.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/crypto/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/crypto/crypto.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/enums.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/exceptions.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/filters.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/methods/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/methods/methods.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/network/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/network/helper.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/network/network.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/network/socket.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/sessions/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/sessions/sessions.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/types/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/types/socket/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/types/socket/message.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/utils/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/utils/configs.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/utils/reacrions.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/utils/utils.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/type/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/errors.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/exceptions.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/file.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/get_type.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/metadata.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/models/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/models/base_model.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/models/bot.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/models/chat.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/prop_update.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/props.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/update.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/update_button.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/webhook.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/antispam.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/cache.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/colors.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/encryption.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/filters.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/inline_filters.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/logger.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/metadata.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/run_filter.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/snapshot.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/template.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/text_parser.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/utils.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/wait_manager.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fastrub.egg-info/SOURCES.txt +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fastrub.egg-info/dependency_links.txt +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fastrub.egg-info/entry_points.txt +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fastrub.egg-info/requires.txt +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/fastrub.egg-info/top_level.txt +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/pyproject.toml +0 -0
- {fastrub-5.2.2 → fastrub-5.2.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastrub
|
|
3
|
-
Version: 5.2.
|
|
3
|
+
Version: 5.2.4
|
|
4
4
|
Summary: The fastest library for writing Rubika messenger bots both synchronously and asynchronously ⚡
|
|
5
5
|
Author-email: "seyyed mohamad hosein moosavi raja(01)" <mohamadhosein159159@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from ..types import Update
|
|
3
|
+
from .client import Client
|
|
4
|
+
from ..utils.filters import Filter
|
|
5
|
+
from ..utils import _run_filter
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
|
|
9
|
+
class Conversation:
|
|
10
|
+
END = "__END__"
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
name: str = "default",
|
|
15
|
+
timeout: float = 300.0
|
|
16
|
+
):
|
|
17
|
+
self.name = name
|
|
18
|
+
self._states: dict[str, Callable] = {}
|
|
19
|
+
self._entry_handler: Callable | None = None
|
|
20
|
+
self._entry_filters = None
|
|
21
|
+
self._user_states: dict[str, str] = {}
|
|
22
|
+
self._user_data: dict[str, dict] = {}
|
|
23
|
+
self.timeout = timeout
|
|
24
|
+
self._user_timers: dict[str, asyncio.Task] = {}
|
|
25
|
+
self.client = None
|
|
26
|
+
|
|
27
|
+
def _set_timeout(self, chat_id: str):
|
|
28
|
+
if chat_id in self._user_timers:
|
|
29
|
+
self._user_timers[chat_id].cancel()
|
|
30
|
+
|
|
31
|
+
async def _timeout():
|
|
32
|
+
await asyncio.sleep(self.timeout)
|
|
33
|
+
if chat_id in self._user_states:
|
|
34
|
+
self._cleanup(chat_id)
|
|
35
|
+
|
|
36
|
+
self._user_timers[chat_id] = asyncio.create_task(_timeout())
|
|
37
|
+
|
|
38
|
+
def entry(
|
|
39
|
+
self,
|
|
40
|
+
commands: list | None = None,
|
|
41
|
+
text: str | None = None,
|
|
42
|
+
filters: Filter | None = None
|
|
43
|
+
):
|
|
44
|
+
"""دکوراتور برای نقطه ورود مکالمه."""
|
|
45
|
+
def decorator(func):
|
|
46
|
+
self._entry_handler = func
|
|
47
|
+
self._entry_filters = {
|
|
48
|
+
"commands": commands,
|
|
49
|
+
"text": text,
|
|
50
|
+
"filters": filters
|
|
51
|
+
}
|
|
52
|
+
return func
|
|
53
|
+
return decorator
|
|
54
|
+
|
|
55
|
+
async def _is_entry(self, update: Update) -> bool:
|
|
56
|
+
if not self._entry_filters:
|
|
57
|
+
return False
|
|
58
|
+
commands: list[str] | None = self._entry_filters.get("commands")
|
|
59
|
+
text = self._entry_filters.get("text")
|
|
60
|
+
filters: Filter | None = self._entry_filters.get("filters")
|
|
61
|
+
|
|
62
|
+
run_filter = await _run_filter(filters, update)
|
|
63
|
+
if filters and not run_filter:
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
if commands is not None and update.text:
|
|
67
|
+
for cmd in commands:
|
|
68
|
+
if update.text == cmd or update.text == f"/{cmd}":
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
if text is not None and update.text == text:
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
if filters is not None:
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
def state(
|
|
80
|
+
self,
|
|
81
|
+
state_name: str
|
|
82
|
+
):
|
|
83
|
+
"""دکوراتور برای یک حالت (مرحله) از مکالمه"""
|
|
84
|
+
def decorator(func: Callable):
|
|
85
|
+
self._states[state_name] = func
|
|
86
|
+
return func
|
|
87
|
+
return decorator
|
|
88
|
+
|
|
89
|
+
async def handle(self, update: Update, client: Client) -> bool:
|
|
90
|
+
chat_id = update.chat_id
|
|
91
|
+
self.client = client
|
|
92
|
+
|
|
93
|
+
# ۱. کاربر جدید؟
|
|
94
|
+
if chat_id not in self._user_states:
|
|
95
|
+
is_entry = await self._is_entry(update)
|
|
96
|
+
if is_entry:
|
|
97
|
+
self._user_states[chat_id] = "__ENTRY__"
|
|
98
|
+
self._user_data[chat_id] = {}
|
|
99
|
+
self._set_timeout(chat_id)
|
|
100
|
+
else:
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
current_state = self._user_states.get(chat_id)
|
|
104
|
+
|
|
105
|
+
# ۲. حالت ENTRY
|
|
106
|
+
if current_state == "__ENTRY__":
|
|
107
|
+
if self._entry_handler:
|
|
108
|
+
try:
|
|
109
|
+
next_state = self._entry_handler(update, self._user_data.get(chat_id, {}))
|
|
110
|
+
if asyncio.iscoroutine(next_state):
|
|
111
|
+
next_state = await next_state
|
|
112
|
+
except Exception as e:
|
|
113
|
+
import traceback
|
|
114
|
+
traceback.print_exc()
|
|
115
|
+
self._cleanup(chat_id)
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
if next_state == Conversation.END:
|
|
119
|
+
self._cleanup(chat_id)
|
|
120
|
+
return False
|
|
121
|
+
self._user_states[chat_id] = next_state
|
|
122
|
+
self._set_timeout(chat_id)
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
# ۳. حالتهای معمولی
|
|
126
|
+
if current_state in self._states:
|
|
127
|
+
handler = self._states[current_state]
|
|
128
|
+
try:
|
|
129
|
+
result = handler(update, self._user_data.get(chat_id, {}))
|
|
130
|
+
if asyncio.iscoroutine(result):
|
|
131
|
+
next_state = await result
|
|
132
|
+
else:
|
|
133
|
+
next_state = result
|
|
134
|
+
except Exception as e:
|
|
135
|
+
import traceback
|
|
136
|
+
traceback.print_exc()
|
|
137
|
+
self._cleanup(chat_id)
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
if next_state == Conversation.END:
|
|
141
|
+
self._cleanup(chat_id)
|
|
142
|
+
return False
|
|
143
|
+
self._user_states[chat_id] = next_state
|
|
144
|
+
self._set_timeout(chat_id)
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
def entry_form(
|
|
150
|
+
self,
|
|
151
|
+
form_class: type,
|
|
152
|
+
commands: list[str] | None = None,
|
|
153
|
+
text: str | None = None,
|
|
154
|
+
filters: Filter | None = None,
|
|
155
|
+
on_start: Callable | None = None,
|
|
156
|
+
on_finish: Callable | None = None,
|
|
157
|
+
on_cancel: Callable | None = None,
|
|
158
|
+
on_error: Callable | None = None,
|
|
159
|
+
on_timeout: Callable | None = None,
|
|
160
|
+
timeout: int = 120,
|
|
161
|
+
allow_back: bool = False,
|
|
162
|
+
allow_skip: bool = False,
|
|
163
|
+
):
|
|
164
|
+
"""
|
|
165
|
+
دکوراتور برای ساخت یه فرم خودکار از روی DataForm.
|
|
166
|
+
|
|
167
|
+
مثال:
|
|
168
|
+
@register.entry_form(UserInfo, on_finish=lambda data: print(data))
|
|
169
|
+
async def done(msg, data):
|
|
170
|
+
await msg.reply(f"ثبت شد: {data}")
|
|
171
|
+
"""
|
|
172
|
+
def decorator(final_handler: Callable):
|
|
173
|
+
form = form_class()
|
|
174
|
+
fields = list(form.get_fields().items())
|
|
175
|
+
|
|
176
|
+
self._form_config = {
|
|
177
|
+
"form_class": form_class,
|
|
178
|
+
"fields": fields,
|
|
179
|
+
"on_start": on_start,
|
|
180
|
+
"on_finish": on_finish,
|
|
181
|
+
"on_cancel": on_cancel,
|
|
182
|
+
"on_error": on_error,
|
|
183
|
+
"on_timeout": on_timeout,
|
|
184
|
+
"timeout": timeout,
|
|
185
|
+
"allow_back": allow_back,
|
|
186
|
+
"allow_skip": allow_skip,
|
|
187
|
+
"final_handler": final_handler,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
self._entry_filters = {
|
|
191
|
+
"commands": commands,
|
|
192
|
+
"text": text,
|
|
193
|
+
"filters": filters
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async def form_entry(update: Update, data: dict):
|
|
197
|
+
"""نقطهٔ ورود فرم — اولین فیلد رو میپرسه"""
|
|
198
|
+
config = self._form_config
|
|
199
|
+
|
|
200
|
+
if config["on_start"]:
|
|
201
|
+
res = config["on_start"]()
|
|
202
|
+
if asyncio.iscoroutine(res):
|
|
203
|
+
await res
|
|
204
|
+
|
|
205
|
+
field_name, field_config = config["fields"][0]
|
|
206
|
+
prompt = field_config.prompt
|
|
207
|
+
keypad = self._build_field_keypad(field_config)
|
|
208
|
+
|
|
209
|
+
await update.reply(prompt, keypad=keypad)
|
|
210
|
+
|
|
211
|
+
return f"__form_{field_name}"
|
|
212
|
+
|
|
213
|
+
self._entry_handler = form_entry
|
|
214
|
+
|
|
215
|
+
for i, (field_name, field_config) in enumerate(fields):
|
|
216
|
+
is_last = (i == len(fields) - 1)
|
|
217
|
+
next_field_name = fields[i + 1][0] if not is_last else None
|
|
218
|
+
|
|
219
|
+
def make_field_handler(idx: int, f_name: str, f_config, next_f_name: str | None, last: bool):
|
|
220
|
+
async def field_handler(update: Update, data: dict):
|
|
221
|
+
config = self._form_config
|
|
222
|
+
text = update.text
|
|
223
|
+
|
|
224
|
+
if text == "/cancel":
|
|
225
|
+
if config["on_cancel"]:
|
|
226
|
+
res = config["on_cancel"]()
|
|
227
|
+
if asyncio.iscoroutine(res):
|
|
228
|
+
await res
|
|
229
|
+
return Conversation.END
|
|
230
|
+
|
|
231
|
+
if text == "/back" and config["allow_back"]:
|
|
232
|
+
prev_idx = idx - 1
|
|
233
|
+
if prev_idx >= 0:
|
|
234
|
+
prev_name, prev_config = config["fields"][prev_idx]
|
|
235
|
+
prompt = prev_config.prompt
|
|
236
|
+
keypad = self._build_field_keypad(prev_config)
|
|
237
|
+
await update.reply(prompt, keypad=keypad)
|
|
238
|
+
return f"__form_{prev_name}"
|
|
239
|
+
prompt = f_config.prompt
|
|
240
|
+
keypad = self._build_field_keypad(f_config)
|
|
241
|
+
await update.reply(prompt, keypad=keypad)
|
|
242
|
+
return f"__form_{f_name}"
|
|
243
|
+
|
|
244
|
+
if text == "/skip" and config["allow_skip"]:
|
|
245
|
+
if last:
|
|
246
|
+
if config["on_finish"]:
|
|
247
|
+
res = config["on_finish"](data)
|
|
248
|
+
if asyncio.iscoroutine(res):
|
|
249
|
+
await res
|
|
250
|
+
await config["final_handler"](update, data)
|
|
251
|
+
return Conversation.END
|
|
252
|
+
next_f_config = config["fields"][idx + 1][1]
|
|
253
|
+
prompt = next_f_config.prompt
|
|
254
|
+
keypad = self._build_field_keypad(next_f_config)
|
|
255
|
+
await update.reply(prompt, keypad=keypad)
|
|
256
|
+
return f"__form_{next_f_name}"
|
|
257
|
+
|
|
258
|
+
validated = self._validate_field(f_config, text)
|
|
259
|
+
|
|
260
|
+
if validated is False:
|
|
261
|
+
err_msg = getattr(f_config, 'invalid_answer', '❌ ورودی نامعتبر است')
|
|
262
|
+
await update.reply(err_msg)
|
|
263
|
+
return f"__form_{f_name}"
|
|
264
|
+
|
|
265
|
+
value = validated
|
|
266
|
+
if hasattr(f_config, 'transform') and f_config.transform:
|
|
267
|
+
try:
|
|
268
|
+
value = f_config.transform(text)
|
|
269
|
+
except Exception:
|
|
270
|
+
value = validated
|
|
271
|
+
|
|
272
|
+
data[f_name] = value
|
|
273
|
+
|
|
274
|
+
if hasattr(f_config, 'func') and f_config.func:
|
|
275
|
+
try:
|
|
276
|
+
res = f_config.func()
|
|
277
|
+
if asyncio.iscoroutine(res):
|
|
278
|
+
await res
|
|
279
|
+
except Exception:
|
|
280
|
+
pass
|
|
281
|
+
|
|
282
|
+
if last:
|
|
283
|
+
if config["on_finish"]:
|
|
284
|
+
try:
|
|
285
|
+
res = config["on_finish"](data)
|
|
286
|
+
if asyncio.iscoroutine(res):
|
|
287
|
+
await res
|
|
288
|
+
except Exception:
|
|
289
|
+
pass
|
|
290
|
+
await config["final_handler"](update, data)
|
|
291
|
+
return Conversation.END
|
|
292
|
+
|
|
293
|
+
next_f_config = config["fields"][idx + 1][1]
|
|
294
|
+
prompt = next_f_config.prompt
|
|
295
|
+
keypad = self._build_field_keypad(next_f_config)
|
|
296
|
+
await update.reply(prompt, keypad=keypad)
|
|
297
|
+
return f"__form_{next_f_name}"
|
|
298
|
+
|
|
299
|
+
return field_handler
|
|
300
|
+
|
|
301
|
+
handler = make_field_handler(
|
|
302
|
+
idx=i,
|
|
303
|
+
f_name=field_name,
|
|
304
|
+
f_config=field_config,
|
|
305
|
+
next_f_name=next_field_name,
|
|
306
|
+
last=is_last
|
|
307
|
+
)
|
|
308
|
+
state_name = f"__form_{field_name}"
|
|
309
|
+
self._states[state_name] = handler
|
|
310
|
+
|
|
311
|
+
return final_handler
|
|
312
|
+
|
|
313
|
+
return decorator
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _build_field_keypad(self, field_config) -> list | None:
|
|
317
|
+
"""ساختن کیبورد برای Number و Choice"""
|
|
318
|
+
from fast_rub.button import KeyPad
|
|
319
|
+
|
|
320
|
+
if hasattr(field_config, 'keypad') and field_config.keypad:
|
|
321
|
+
if field_config.min is not None and field_config.max is not None:
|
|
322
|
+
numbers = list(range(field_config.min, field_config.max + 1))
|
|
323
|
+
if getattr(field_config, 'sort_by', 'asc') == "desc":
|
|
324
|
+
numbers.reverse()
|
|
325
|
+
|
|
326
|
+
keypad = KeyPad()
|
|
327
|
+
row = []
|
|
328
|
+
max_row = getattr(field_config, 'max_btn_row', 4)
|
|
329
|
+
for num in numbers:
|
|
330
|
+
row.append(keypad.simple(str(num), str(num)))
|
|
331
|
+
if len(row) >= max_row:
|
|
332
|
+
keypad.append(*row)
|
|
333
|
+
row = []
|
|
334
|
+
if row:
|
|
335
|
+
keypad.append(*row)
|
|
336
|
+
return keypad.build()
|
|
337
|
+
|
|
338
|
+
if hasattr(field_config, 'options'):
|
|
339
|
+
options = field_config.options
|
|
340
|
+
if callable(options):
|
|
341
|
+
options = options()
|
|
342
|
+
|
|
343
|
+
keypad = KeyPad()
|
|
344
|
+
row = []
|
|
345
|
+
max_row = getattr(field_config, 'max_btn_row', 2)
|
|
346
|
+
for opt in options:
|
|
347
|
+
row.append(keypad.simple(opt, opt))
|
|
348
|
+
if len(row) >= max_row:
|
|
349
|
+
keypad.append(*row)
|
|
350
|
+
row = []
|
|
351
|
+
if row:
|
|
352
|
+
keypad.append(*row)
|
|
353
|
+
return keypad.build()
|
|
354
|
+
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _validate_field(self, field_config, value: str):
|
|
359
|
+
"""اعتبارسنجی فیلد"""
|
|
360
|
+
from .forms.fields import Text, Number, Choice
|
|
361
|
+
|
|
362
|
+
if hasattr(field_config, 'validator') and field_config.validator:
|
|
363
|
+
result = field_config.validator(value)
|
|
364
|
+
if asyncio.iscoroutine(result):
|
|
365
|
+
pass
|
|
366
|
+
if not result:
|
|
367
|
+
return False
|
|
368
|
+
|
|
369
|
+
if isinstance(field_config, Text):
|
|
370
|
+
if field_config.min_len and len(value) < field_config.min_len:
|
|
371
|
+
return False
|
|
372
|
+
if field_config.max_len and len(value) > field_config.max_len:
|
|
373
|
+
return False
|
|
374
|
+
if field_config.email and "@" not in value:
|
|
375
|
+
return False
|
|
376
|
+
if field_config.valid_inputs and value not in field_config.valid_inputs:
|
|
377
|
+
return False
|
|
378
|
+
return value
|
|
379
|
+
|
|
380
|
+
elif isinstance(field_config, Number):
|
|
381
|
+
try:
|
|
382
|
+
num = int(value)
|
|
383
|
+
except ValueError:
|
|
384
|
+
return False
|
|
385
|
+
if field_config.min is not None and num < field_config.min:
|
|
386
|
+
return False
|
|
387
|
+
if field_config.max is not None and num > field_config.max:
|
|
388
|
+
return False
|
|
389
|
+
return num
|
|
390
|
+
|
|
391
|
+
elif isinstance(field_config, Choice):
|
|
392
|
+
options = field_config.options
|
|
393
|
+
if callable(options):
|
|
394
|
+
options = options()
|
|
395
|
+
if value in options:
|
|
396
|
+
return value
|
|
397
|
+
return False
|
|
398
|
+
|
|
399
|
+
return value
|
|
400
|
+
|
|
401
|
+
def _cleanup(self, chat_id: str):
|
|
402
|
+
"""پاکسازی state کاربر بعد از تموم شدن مکالمه"""
|
|
403
|
+
self._user_states.pop(chat_id, None)
|
|
404
|
+
self._user_data.pop(chat_id, None)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
class ConversationManager:
|
|
408
|
+
"""مدیریت چندین مکالمه همزمان"""
|
|
409
|
+
def __init__(self):
|
|
410
|
+
self._conversations: dict[str, Conversation] = {}
|
|
411
|
+
|
|
412
|
+
def add(
|
|
413
|
+
self,
|
|
414
|
+
conversation: Conversation
|
|
415
|
+
):
|
|
416
|
+
self._conversations[conversation.name] = conversation
|
|
417
|
+
|
|
418
|
+
def remove(
|
|
419
|
+
self,
|
|
420
|
+
name: str
|
|
421
|
+
):
|
|
422
|
+
self._conversations.pop(name, None)
|
|
423
|
+
|
|
424
|
+
async def handle(
|
|
425
|
+
self,
|
|
426
|
+
update: Update,
|
|
427
|
+
client: Client
|
|
428
|
+
) -> bool:
|
|
429
|
+
"""همه مکالمات رو چک میکنه، اولویت با اولین مکالمه فعال"""
|
|
430
|
+
chat_id = update.chat_id
|
|
431
|
+
for conv in self._conversations.values():
|
|
432
|
+
if chat_id in conv._user_states:
|
|
433
|
+
return await conv.handle(update, client)
|
|
434
|
+
for conv in self._conversations.values():
|
|
435
|
+
if await conv._is_entry(update):
|
|
436
|
+
return await conv.handle(update, client)
|
|
437
|
+
return False
|
|
@@ -18,165 +18,123 @@ class FormManager:
|
|
|
18
18
|
self.wait_manager = update._client.wait_manager
|
|
19
19
|
self.chat_id = update.chat_id
|
|
20
20
|
self._retry_counts: dict[str, int] = {}
|
|
21
|
+
self._running = False
|
|
22
|
+
self._current_callback: Callable | None = None
|
|
21
23
|
|
|
22
|
-
async def
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
while self.current_index < len(self.fields):
|
|
27
|
-
field_name, field_config = self.fields[self.current_index]
|
|
28
|
-
|
|
29
|
-
if self._should_skip(field_name):
|
|
30
|
-
self.current_index += 1
|
|
31
|
-
continue
|
|
32
|
-
|
|
33
|
-
await self._call_callback(field_config.pre_func)
|
|
34
|
-
|
|
35
|
-
value = await self._ask_field(field_name, field_config)
|
|
36
|
-
|
|
37
|
-
if value is None:
|
|
38
|
-
if self._is_timeout(field_name, field_config):
|
|
39
|
-
await self._call_callback(
|
|
40
|
-
self.config.on_timeout or self.form.on_timeout,
|
|
41
|
-
field_name
|
|
42
|
-
)
|
|
43
|
-
await self._cancel()
|
|
44
|
-
return None
|
|
45
|
-
|
|
46
|
-
if value == "__BACK__":
|
|
47
|
-
if self.current_index > 0:
|
|
48
|
-
self.current_index -= 1
|
|
49
|
-
continue
|
|
50
|
-
|
|
51
|
-
if value == "__SKIP__":
|
|
52
|
-
self.current_index += 1
|
|
53
|
-
continue
|
|
54
|
-
|
|
55
|
-
if value == "__CANCEL__":
|
|
56
|
-
await self._cancel()
|
|
57
|
-
return None
|
|
58
|
-
|
|
59
|
-
if not await self._validate(field_config, value):
|
|
60
|
-
self._retry_counts[field_name] = self._retry_counts.get(field_name, 0) + 1
|
|
61
|
-
if self._retry_counts[field_name] <= field_config.retry:
|
|
62
|
-
continue
|
|
63
|
-
await self._cancel()
|
|
64
|
-
return None
|
|
65
|
-
|
|
66
|
-
self._retry_counts[field_name] = 0
|
|
67
|
-
|
|
68
|
-
if field_config.transform:
|
|
69
|
-
value = field_config.transform(value)
|
|
70
|
-
|
|
71
|
-
self.data[field_name] = value
|
|
72
|
-
|
|
73
|
-
await self._call_func(field_config.func, field_config, value, field_name)
|
|
74
|
-
|
|
75
|
-
self.current_index += 1
|
|
76
|
-
|
|
77
|
-
await self._call_callback(self.config.on_finish, self.data)
|
|
78
|
-
return self.data
|
|
24
|
+
async def start(self, callback: Callable):
|
|
25
|
+
"""شروع فرم. callback وقتی همه دادهها جمع شد صدا زده میشه"""
|
|
26
|
+
self._current_callback = callback
|
|
27
|
+
self._running = True
|
|
79
28
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
self.config.on_error or self.form.on_error,
|
|
83
|
-
e
|
|
84
|
-
)
|
|
85
|
-
return None
|
|
29
|
+
await self._call_callback(self.config.on_start)
|
|
30
|
+
await self._ask_next_field()
|
|
86
31
|
|
|
87
|
-
def
|
|
88
|
-
|
|
32
|
+
async def handle_update(self, update) -> bool:
|
|
33
|
+
"""پردازش آپدیت جدید. برگشت True یعنی فرم هنوز ادامه داره"""
|
|
34
|
+
if not self._running:
|
|
35
|
+
return False
|
|
89
36
|
|
|
90
|
-
if
|
|
91
|
-
|
|
37
|
+
if update.chat_id != self.chat_id:
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
text = update.text
|
|
41
|
+
|
|
42
|
+
if text == "/cancel":
|
|
43
|
+
await self._cancel()
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
if text == "/back" and self.config.allow_back and self.current_index > 0:
|
|
47
|
+
self.current_index -= 1
|
|
48
|
+
self._retry_counts[self.fields[self.current_index][0]] = 0
|
|
49
|
+
await self._ask_next_field()
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
if text == "/skip" and self.config.allow_skip:
|
|
53
|
+
self.current_index += 1
|
|
54
|
+
await self._ask_next_field()
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
field_name, field_config = self.fields[self.current_index]
|
|
58
|
+
|
|
59
|
+
if not await self._validate(field_config, text):
|
|
60
|
+
self._retry_counts[field_name] = self._retry_counts.get(field_name, 0) + 1
|
|
61
|
+
if self._retry_counts[field_name] <= field_config.retry:
|
|
62
|
+
await self._ask_next_field()
|
|
92
63
|
return True
|
|
64
|
+
await self._cancel()
|
|
65
|
+
return False
|
|
93
66
|
|
|
94
|
-
|
|
95
|
-
for dep_field, expected in field_config.depends_on.items():
|
|
96
|
-
if dep_field not in self.data:
|
|
97
|
-
return True
|
|
98
|
-
actual = self.data[dep_field]
|
|
99
|
-
if callable(expected):
|
|
100
|
-
if not expected(actual):
|
|
101
|
-
return True
|
|
102
|
-
elif actual != expected:
|
|
103
|
-
return True
|
|
67
|
+
self._retry_counts[field_name] = 0
|
|
104
68
|
|
|
105
|
-
|
|
69
|
+
value = text
|
|
70
|
+
if field_config.transform:
|
|
71
|
+
value = field_config.transform(value)
|
|
72
|
+
|
|
73
|
+
self.data[field_name] = value
|
|
74
|
+
|
|
75
|
+
await self._call_func(field_config.func, field_config, value, field_name)
|
|
76
|
+
|
|
77
|
+
self.current_index += 1
|
|
78
|
+
|
|
79
|
+
if self.current_index >= len(self.fields):
|
|
80
|
+
await self._call_callback(self.config.on_finish, self.data)
|
|
81
|
+
if self._current_callback:
|
|
82
|
+
await self._current_callback(self.update, self.data)
|
|
83
|
+
self._running = False
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
await self._ask_next_field()
|
|
87
|
+
return True
|
|
106
88
|
|
|
107
|
-
async def
|
|
89
|
+
async def _ask_next_field(self):
|
|
90
|
+
"""پرسیدن فیلد بعدی"""
|
|
91
|
+
if self.current_index >= len(self.fields):
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
field_name, field_config = self.fields[self.current_index]
|
|
95
|
+
|
|
96
|
+
if self._should_skip(field_name):
|
|
97
|
+
self.current_index += 1
|
|
98
|
+
await self._ask_next_field()
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
await self._call_callback(field_config.pre_func)
|
|
102
|
+
|
|
108
103
|
prompt = field_config.prompt
|
|
109
104
|
keypad = None
|
|
110
105
|
|
|
111
106
|
if isinstance(field_config, Number) and field_config.keypad:
|
|
112
107
|
keypad = self._build_number_keypad(field_config)
|
|
113
|
-
|
|
114
108
|
elif isinstance(field_config, Choice):
|
|
115
109
|
options = field_config.options
|
|
116
110
|
if callable(options):
|
|
117
111
|
options = options()
|
|
118
112
|
keypad = self._build_choice_keypad(options, field_config.max_btn_row)
|
|
119
113
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
try:
|
|
123
|
-
reply = await self.wait_manager.wait_for_message(
|
|
124
|
-
self.chat_id,
|
|
125
|
-
timeout=field_config.timeout
|
|
126
|
-
)
|
|
127
|
-
except asyncio.TimeoutError:
|
|
128
|
-
if field_config.default is not None:
|
|
129
|
-
return field_config.default
|
|
130
|
-
return None
|
|
131
|
-
|
|
132
|
-
text = reply.text
|
|
133
|
-
|
|
134
|
-
if text == "/cancel":
|
|
135
|
-
return "__CANCEL__"
|
|
136
|
-
if text == "/back" and self.config.allow_back:
|
|
137
|
-
return "__BACK__"
|
|
138
|
-
if text == "/skip" and self.config.allow_skip:
|
|
139
|
-
return "__SKIP__"
|
|
140
|
-
|
|
141
|
-
return text
|
|
114
|
+
await self.update.reply(prompt, keypad=keypad)
|
|
142
115
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
numbers = list(range(field_config.min, field_config.max + 1))
|
|
148
|
-
|
|
149
|
-
if field_config.sort_by == "desc":
|
|
150
|
-
numbers.reverse()
|
|
151
|
-
elif field_config.sort_by == "none":
|
|
152
|
-
pass
|
|
116
|
+
|
|
117
|
+
def _should_skip(self, field_name: str) -> bool:
|
|
118
|
+
field_config = self.fields[self.current_index][1]
|
|
153
119
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
row.append(keypad.simple(str(num), str(num)))
|
|
158
|
-
if len(row) >= field_config.max_btn_row:
|
|
159
|
-
keypad.append(*row)
|
|
160
|
-
row = []
|
|
161
|
-
if row:
|
|
162
|
-
keypad.append(*row)
|
|
120
|
+
if hasattr(field_config, 'skip_if') and field_config.skip_if:
|
|
121
|
+
if field_config.skip_if(self.data):
|
|
122
|
+
return True
|
|
163
123
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if row:
|
|
175
|
-
keypad.append(*row)
|
|
124
|
+
if hasattr(field_config, 'depends_on') and field_config.depends_on:
|
|
125
|
+
for dep_field, expected in field_config.depends_on.items():
|
|
126
|
+
if dep_field not in self.data:
|
|
127
|
+
return True
|
|
128
|
+
actual = self.data[dep_field]
|
|
129
|
+
if callable(expected):
|
|
130
|
+
if not expected(actual):
|
|
131
|
+
return True
|
|
132
|
+
elif actual != expected:
|
|
133
|
+
return True
|
|
176
134
|
|
|
177
|
-
return
|
|
135
|
+
return False
|
|
178
136
|
|
|
179
|
-
async def _validate(self, field_config
|
|
137
|
+
async def _validate(self, field_config, value: str) -> bool:
|
|
180
138
|
if field_config.validator:
|
|
181
139
|
result = field_config.validator(value)
|
|
182
140
|
if asyncio.iscoroutine(result):
|
|
@@ -230,6 +188,40 @@ class FormManager:
|
|
|
230
188
|
if hasattr(field_config, 'invalid_answer'):
|
|
231
189
|
await self.update.reply(field_config.invalid_answer)
|
|
232
190
|
|
|
191
|
+
def _build_number_keypad(self, field_config: Number) -> list | None:
|
|
192
|
+
if field_config.min is None or field_config.max is None:
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
numbers = list(range(field_config.min, field_config.max + 1))
|
|
196
|
+
|
|
197
|
+
if field_config.sort_by == "desc":
|
|
198
|
+
numbers.reverse()
|
|
199
|
+
|
|
200
|
+
keypad = KeyPad()
|
|
201
|
+
row = []
|
|
202
|
+
for num in numbers:
|
|
203
|
+
row.append(keypad.simple(str(num), str(num)))
|
|
204
|
+
if len(row) >= field_config.max_btn_row:
|
|
205
|
+
keypad.append(*row)
|
|
206
|
+
row = []
|
|
207
|
+
if row:
|
|
208
|
+
keypad.append(*row)
|
|
209
|
+
|
|
210
|
+
return keypad.build()
|
|
211
|
+
|
|
212
|
+
def _build_choice_keypad(self, options: list[str], max_btn_row: int) -> list | None:
|
|
213
|
+
keypad = KeyPad()
|
|
214
|
+
row = []
|
|
215
|
+
for opt in options:
|
|
216
|
+
row.append(keypad.simple(opt, opt))
|
|
217
|
+
if len(row) >= max_btn_row:
|
|
218
|
+
keypad.append(*row)
|
|
219
|
+
row = []
|
|
220
|
+
if row:
|
|
221
|
+
keypad.append(*row)
|
|
222
|
+
|
|
223
|
+
return keypad.build()
|
|
224
|
+
|
|
233
225
|
async def _call_func(self, func: Callable | None, field_config, value: Any, field_name: str):
|
|
234
226
|
if not func:
|
|
235
227
|
return
|
|
@@ -249,11 +241,8 @@ class FormManager:
|
|
|
249
241
|
result = func(*args, **kwargs)
|
|
250
242
|
if asyncio.iscoroutine(result):
|
|
251
243
|
await result
|
|
252
|
-
except Exception
|
|
253
|
-
|
|
254
|
-
self.config.on_error or self.form.on_error,
|
|
255
|
-
e
|
|
256
|
-
)
|
|
244
|
+
except Exception:
|
|
245
|
+
pass
|
|
257
246
|
|
|
258
247
|
async def _call_callback(self, callback: Callable | None, *args):
|
|
259
248
|
if not callback:
|
|
@@ -262,11 +251,9 @@ class FormManager:
|
|
|
262
251
|
result = callback(*args)
|
|
263
252
|
if asyncio.iscoroutine(result):
|
|
264
253
|
await result
|
|
265
|
-
except Exception
|
|
254
|
+
except Exception:
|
|
266
255
|
pass
|
|
267
256
|
|
|
268
257
|
async def _cancel(self):
|
|
258
|
+
self._running = False
|
|
269
259
|
await self._call_callback(self.config.on_cancel or self.form.on_cancel)
|
|
270
|
-
|
|
271
|
-
def _is_timeout(self, field_name: str, field_config) -> bool:
|
|
272
|
-
return field_config.default is None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
v = "5.2.4"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastrub
|
|
3
|
-
Version: 5.2.
|
|
3
|
+
Version: 5.2.4
|
|
4
4
|
Summary: The fastest library for writing Rubika messenger bots both synchronously and asynchronously ⚡
|
|
5
5
|
Author-email: "seyyed mohamad hosein moosavi raja(01)" <mohamadhosein159159@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
from collections.abc import Callable
|
|
2
|
-
from ..types import Update
|
|
3
|
-
from .client import Client
|
|
4
|
-
from ..utils.filters import Filter
|
|
5
|
-
from ..utils import _run_filter
|
|
6
|
-
|
|
7
|
-
import asyncio
|
|
8
|
-
|
|
9
|
-
class Conversation:
|
|
10
|
-
END = "__END__"
|
|
11
|
-
|
|
12
|
-
def __init__(
|
|
13
|
-
self,
|
|
14
|
-
name: str = "default",
|
|
15
|
-
timeout: float = 300.0
|
|
16
|
-
):
|
|
17
|
-
self.name = name
|
|
18
|
-
self._states: dict[str, Callable] = {}
|
|
19
|
-
self._entry_handler: Callable | None = None
|
|
20
|
-
self._entry_filters = None
|
|
21
|
-
self._user_states: dict[str, str] = {}
|
|
22
|
-
self._user_data: dict[str, dict] = {}
|
|
23
|
-
self.timeout = timeout
|
|
24
|
-
self._user_timers: dict[str, asyncio.Task] = {}
|
|
25
|
-
self.client = None
|
|
26
|
-
|
|
27
|
-
def _set_timeout(self, chat_id: str):
|
|
28
|
-
if chat_id in self._user_timers:
|
|
29
|
-
self._user_timers[chat_id].cancel()
|
|
30
|
-
|
|
31
|
-
async def _timeout():
|
|
32
|
-
await asyncio.sleep(self.timeout)
|
|
33
|
-
if chat_id in self._user_states:
|
|
34
|
-
self._cleanup(chat_id)
|
|
35
|
-
|
|
36
|
-
self._user_timers[chat_id] = asyncio.create_task(_timeout())
|
|
37
|
-
|
|
38
|
-
def entry(
|
|
39
|
-
self,
|
|
40
|
-
commands: list | None = None,
|
|
41
|
-
text: str | None = None,
|
|
42
|
-
filters: Filter | None = None
|
|
43
|
-
):
|
|
44
|
-
"""دکوراتور برای نقطه ورود مکالمه."""
|
|
45
|
-
def decorator(func):
|
|
46
|
-
self._entry_handler = func
|
|
47
|
-
self._entry_filters = {
|
|
48
|
-
"commands": commands,
|
|
49
|
-
"text": text,
|
|
50
|
-
"filters": filters
|
|
51
|
-
}
|
|
52
|
-
return func
|
|
53
|
-
return decorator
|
|
54
|
-
|
|
55
|
-
async def _is_entry(self, update: Update) -> bool:
|
|
56
|
-
if not self._entry_filters:
|
|
57
|
-
return False
|
|
58
|
-
commands: list[str] | None = self._entry_filters.get("commands")
|
|
59
|
-
text = self._entry_filters.get("text")
|
|
60
|
-
filters: Filter | None = self._entry_filters.get("filters")
|
|
61
|
-
|
|
62
|
-
run_filter = await _run_filter(filters, update)
|
|
63
|
-
if filters and not run_filter:
|
|
64
|
-
return False
|
|
65
|
-
|
|
66
|
-
if commands is not None and update.text:
|
|
67
|
-
for cmd in commands:
|
|
68
|
-
if update.text == cmd or update.text == f"/{cmd}":
|
|
69
|
-
return True
|
|
70
|
-
|
|
71
|
-
if text is not None and update.text == text:
|
|
72
|
-
return True
|
|
73
|
-
|
|
74
|
-
if filters is not None:
|
|
75
|
-
return True
|
|
76
|
-
|
|
77
|
-
return False
|
|
78
|
-
|
|
79
|
-
def state(
|
|
80
|
-
self,
|
|
81
|
-
state_name: str
|
|
82
|
-
):
|
|
83
|
-
"""دکوراتور برای یک حالت (مرحله) از مکالمه"""
|
|
84
|
-
def decorator(func: Callable):
|
|
85
|
-
self._states[state_name] = func
|
|
86
|
-
return func
|
|
87
|
-
return decorator
|
|
88
|
-
|
|
89
|
-
async def handle(
|
|
90
|
-
self,
|
|
91
|
-
update: Update,
|
|
92
|
-
client: Client
|
|
93
|
-
) -> bool:
|
|
94
|
-
"""پردازش یه آپدیت توی مکالمه.برمیگردونه True اگه مکالمه هنوز ادامه داره، False اگه تموم شده."""
|
|
95
|
-
chat_id = update.chat_id
|
|
96
|
-
self.client = client
|
|
97
|
-
if chat_id not in self._user_states:
|
|
98
|
-
if await self._is_entry(update):
|
|
99
|
-
self._user_states[chat_id] = "__ENTRY__"
|
|
100
|
-
self._user_data[chat_id] = {}
|
|
101
|
-
self._set_timeout(chat_id)
|
|
102
|
-
else:
|
|
103
|
-
return False
|
|
104
|
-
current_state = self._user_states.get(chat_id)
|
|
105
|
-
if current_state == "__ENTRY__":
|
|
106
|
-
if self._entry_handler:
|
|
107
|
-
next_state = await self._entry_handler(update, self._user_data.get(chat_id, {}))
|
|
108
|
-
if next_state == Conversation.END:
|
|
109
|
-
self._cleanup(chat_id)
|
|
110
|
-
return False
|
|
111
|
-
self._user_states[chat_id] = next_state
|
|
112
|
-
self._set_timeout(chat_id)
|
|
113
|
-
return True
|
|
114
|
-
if current_state in self._states:
|
|
115
|
-
handler = self._states[current_state]
|
|
116
|
-
next_state = await handler(update, self._user_data.get(chat_id, {}))
|
|
117
|
-
if next_state == Conversation.END:
|
|
118
|
-
self._cleanup(chat_id)
|
|
119
|
-
return False
|
|
120
|
-
self._user_states[chat_id] = next_state
|
|
121
|
-
return True
|
|
122
|
-
return False
|
|
123
|
-
|
|
124
|
-
def _cleanup(self, chat_id: str):
|
|
125
|
-
"""پاکسازی state کاربر بعد از تموم شدن مکالمه"""
|
|
126
|
-
self._user_states.pop(chat_id, None)
|
|
127
|
-
self._user_data.pop(chat_id, None)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
class ConversationManager:
|
|
131
|
-
"""مدیریت چندین مکالمه همزمان"""
|
|
132
|
-
def __init__(self):
|
|
133
|
-
self._conversations: dict[str, Conversation] = {}
|
|
134
|
-
|
|
135
|
-
def add(
|
|
136
|
-
self,
|
|
137
|
-
conversation: Conversation
|
|
138
|
-
):
|
|
139
|
-
self._conversations[conversation.name] = conversation
|
|
140
|
-
|
|
141
|
-
def remove(
|
|
142
|
-
self,
|
|
143
|
-
name: str
|
|
144
|
-
):
|
|
145
|
-
self._conversations.pop(name, None)
|
|
146
|
-
|
|
147
|
-
async def handle(
|
|
148
|
-
self,
|
|
149
|
-
update: Update,
|
|
150
|
-
client: Client
|
|
151
|
-
) -> bool:
|
|
152
|
-
"""همه مکالمات رو چک میکنه، اولویت با اولین مکالمه فعال"""
|
|
153
|
-
chat_id = update.chat_id
|
|
154
|
-
for conv in self._conversations.values():
|
|
155
|
-
if chat_id in conv._user_states:
|
|
156
|
-
return await conv.handle(update, client)
|
|
157
|
-
for conv in self._conversations.values():
|
|
158
|
-
if await conv._is_entry(update):
|
|
159
|
-
return await conv.handle(update, client)
|
|
160
|
-
return False
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
v = "5.2.2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|