fastrub 5.2.2__tar.gz → 5.2.3__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.3}/PKG-INFO +1 -1
- fastrub-5.2.3/fast_rub/core/conversation.py +451 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/forms/manager.py +131 -144
- fastrub-5.2.3/fast_rub/utils/version.py +1 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/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.3}/LICENSE +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/README.md +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/button/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/button/key_pad.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/cli.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/async_sync.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/background.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/client.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/forms/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/forms/config.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/forms/data_form.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/forms/decorators.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/forms/fields.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/helpers.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/hotreload.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/middleware.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/plugins.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/scheduler.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/signals.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/webhook_server.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/db/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/db/database.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/db/message_keeper.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/db/session.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/network/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/network/network.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrightconfig.json +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/client/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/client/client.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/crypto/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/crypto/crypto.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/enums.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/exceptions.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/filters.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/methods/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/methods/methods.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/network/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/network/helper.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/network/network.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/network/socket.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/sessions/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/sessions/sessions.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/types/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/types/socket/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/types/socket/message.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/utils/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/utils/configs.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/utils/reacrions.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/utils/utils.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/type/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/errors.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/exceptions.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/file.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/get_type.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/metadata.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/models/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/models/base_model.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/models/bot.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/models/chat.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/prop_update.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/props.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/update.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/update_button.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/webhook.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/__init__.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/antispam.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/cache.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/colors.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/encryption.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/filters.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/inline_filters.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/logger.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/metadata.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/run_filter.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/snapshot.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/template.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/text_parser.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/utils.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/wait_manager.py +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fastrub.egg-info/SOURCES.txt +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fastrub.egg-info/dependency_links.txt +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fastrub.egg-info/entry_points.txt +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fastrub.egg-info/requires.txt +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/fastrub.egg-info/top_level.txt +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/pyproject.toml +0 -0
- {fastrub-5.2.2 → fastrub-5.2.3}/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.3
|
|
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,451 @@
|
|
|
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
|
+
print(f"[Conv {self.name}] chat={chat_id[:8]} | _is_entry={is_entry}")
|
|
97
|
+
if is_entry:
|
|
98
|
+
print(f"[Conv {self.name}] Entry triggered")
|
|
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
|
+
|
|
105
|
+
current_state = self._user_states.get(chat_id)
|
|
106
|
+
print(f"[Conv {self.name}] chat={chat_id[:8]} | state={current_state} | text={update.text[:30] if update.text else None}")
|
|
107
|
+
|
|
108
|
+
# ۲. حالت ENTRY
|
|
109
|
+
if current_state == "__ENTRY__":
|
|
110
|
+
if self._entry_handler:
|
|
111
|
+
print(f"[Conv {self.name}] entry_handler type: {type(self._entry_handler)}")
|
|
112
|
+
try:
|
|
113
|
+
next_state = self._entry_handler(update, self._user_data.get(chat_id, {}))
|
|
114
|
+
print(f"[Conv {self.name}] next_state BEFORE await: {next_state}")
|
|
115
|
+
if asyncio.iscoroutine(next_state):
|
|
116
|
+
next_state = await next_state
|
|
117
|
+
print(f"[Conv {self.name}] next_state AFTER await: {next_state}")
|
|
118
|
+
else:
|
|
119
|
+
print(f"[Conv {self.name}] next_state (sync): {next_state}")
|
|
120
|
+
except Exception as e:
|
|
121
|
+
print(f"[Conv {self.name}] ERROR in entry_handler: {e}")
|
|
122
|
+
import traceback
|
|
123
|
+
traceback.print_exc()
|
|
124
|
+
self._cleanup(chat_id)
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
if next_state == Conversation.END:
|
|
128
|
+
self._cleanup(chat_id)
|
|
129
|
+
return False
|
|
130
|
+
self._user_states[chat_id] = next_state
|
|
131
|
+
self._set_timeout(chat_id)
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
# ۳. حالتهای معمولی
|
|
135
|
+
if current_state in self._states:
|
|
136
|
+
handler = self._states[current_state]
|
|
137
|
+
print(f"[Conv {self.name}] handler type: {type(handler)} | iscoroutinefunction: {asyncio.iscoroutinefunction(handler)}")
|
|
138
|
+
try:
|
|
139
|
+
result = handler(update, self._user_data.get(chat_id, {}))
|
|
140
|
+
print(f"[Conv {self.name}] result BEFORE await: {result}")
|
|
141
|
+
if asyncio.iscoroutine(result):
|
|
142
|
+
next_state = await result
|
|
143
|
+
print(f"[Conv {self.name}] next_state AFTER await: {next_state}")
|
|
144
|
+
else:
|
|
145
|
+
next_state = result
|
|
146
|
+
print(f"[Conv {self.name}] next_state (sync): {next_state}")
|
|
147
|
+
except Exception as e:
|
|
148
|
+
print(f"[Conv {self.name}] ERROR in state handler: {e}")
|
|
149
|
+
import traceback
|
|
150
|
+
traceback.print_exc()
|
|
151
|
+
self._cleanup(chat_id)
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
if next_state == Conversation.END:
|
|
155
|
+
self._cleanup(chat_id)
|
|
156
|
+
return False
|
|
157
|
+
self._user_states[chat_id] = next_state
|
|
158
|
+
self._set_timeout(chat_id)
|
|
159
|
+
return True
|
|
160
|
+
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
def entry_form(
|
|
164
|
+
self,
|
|
165
|
+
form_class: type,
|
|
166
|
+
commands: list[str] | None = None,
|
|
167
|
+
text: str | None = None,
|
|
168
|
+
filters: Filter | None = None,
|
|
169
|
+
on_start: Callable | None = None,
|
|
170
|
+
on_finish: Callable | None = None,
|
|
171
|
+
on_cancel: Callable | None = None,
|
|
172
|
+
on_error: Callable | None = None,
|
|
173
|
+
on_timeout: Callable | None = None,
|
|
174
|
+
timeout: int = 120,
|
|
175
|
+
allow_back: bool = False,
|
|
176
|
+
allow_skip: bool = False,
|
|
177
|
+
):
|
|
178
|
+
"""
|
|
179
|
+
دکوراتور برای ساخت یه فرم خودکار از روی DataForm.
|
|
180
|
+
|
|
181
|
+
مثال:
|
|
182
|
+
@register.entry_form(UserInfo, on_finish=lambda data: print(data))
|
|
183
|
+
async def done(msg, data):
|
|
184
|
+
await msg.reply(f"ثبت شد: {data}")
|
|
185
|
+
"""
|
|
186
|
+
def decorator(final_handler: Callable):
|
|
187
|
+
form = form_class()
|
|
188
|
+
fields = list(form.get_fields().items())
|
|
189
|
+
|
|
190
|
+
self._form_config = {
|
|
191
|
+
"form_class": form_class,
|
|
192
|
+
"fields": fields,
|
|
193
|
+
"on_start": on_start,
|
|
194
|
+
"on_finish": on_finish,
|
|
195
|
+
"on_cancel": on_cancel,
|
|
196
|
+
"on_error": on_error,
|
|
197
|
+
"on_timeout": on_timeout,
|
|
198
|
+
"timeout": timeout,
|
|
199
|
+
"allow_back": allow_back,
|
|
200
|
+
"allow_skip": allow_skip,
|
|
201
|
+
"final_handler": final_handler,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
self._entry_filters = {
|
|
205
|
+
"commands": commands,
|
|
206
|
+
"text": text,
|
|
207
|
+
"filters": filters
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async def form_entry(update: Update, data: dict):
|
|
211
|
+
"""نقطهٔ ورود فرم — اولین فیلد رو میپرسه"""
|
|
212
|
+
config = self._form_config
|
|
213
|
+
|
|
214
|
+
if config["on_start"]:
|
|
215
|
+
res = config["on_start"]()
|
|
216
|
+
if asyncio.iscoroutine(res):
|
|
217
|
+
await res
|
|
218
|
+
|
|
219
|
+
field_name, field_config = config["fields"][0]
|
|
220
|
+
prompt = field_config.prompt
|
|
221
|
+
keypad = self._build_field_keypad(field_config)
|
|
222
|
+
|
|
223
|
+
await update.reply(prompt, keypad=keypad)
|
|
224
|
+
|
|
225
|
+
return f"__form_{field_name}"
|
|
226
|
+
|
|
227
|
+
self._entry_handler = form_entry
|
|
228
|
+
|
|
229
|
+
for i, (field_name, field_config) in enumerate(fields):
|
|
230
|
+
is_last = (i == len(fields) - 1)
|
|
231
|
+
next_field_name = fields[i + 1][0] if not is_last else None
|
|
232
|
+
|
|
233
|
+
def make_field_handler(idx: int, f_name: str, f_config, next_f_name: str | None, last: bool):
|
|
234
|
+
async def field_handler(update: Update, data: dict):
|
|
235
|
+
config = self._form_config
|
|
236
|
+
text = update.text
|
|
237
|
+
|
|
238
|
+
if text == "/cancel":
|
|
239
|
+
if config["on_cancel"]:
|
|
240
|
+
res = config["on_cancel"]()
|
|
241
|
+
if asyncio.iscoroutine(res):
|
|
242
|
+
await res
|
|
243
|
+
return Conversation.END
|
|
244
|
+
|
|
245
|
+
if text == "/back" and config["allow_back"]:
|
|
246
|
+
prev_idx = idx - 1
|
|
247
|
+
if prev_idx >= 0:
|
|
248
|
+
prev_name, prev_config = config["fields"][prev_idx]
|
|
249
|
+
prompt = prev_config.prompt
|
|
250
|
+
keypad = self._build_field_keypad(prev_config)
|
|
251
|
+
await update.reply(prompt, keypad=keypad)
|
|
252
|
+
return f"__form_{prev_name}"
|
|
253
|
+
prompt = f_config.prompt
|
|
254
|
+
keypad = self._build_field_keypad(f_config)
|
|
255
|
+
await update.reply(prompt, keypad=keypad)
|
|
256
|
+
return f"__form_{f_name}"
|
|
257
|
+
|
|
258
|
+
if text == "/skip" and config["allow_skip"]:
|
|
259
|
+
if last:
|
|
260
|
+
if config["on_finish"]:
|
|
261
|
+
res = config["on_finish"](data)
|
|
262
|
+
if asyncio.iscoroutine(res):
|
|
263
|
+
await res
|
|
264
|
+
await config["final_handler"](update, data)
|
|
265
|
+
return Conversation.END
|
|
266
|
+
next_f_config = config["fields"][idx + 1][1]
|
|
267
|
+
prompt = next_f_config.prompt
|
|
268
|
+
keypad = self._build_field_keypad(next_f_config)
|
|
269
|
+
await update.reply(prompt, keypad=keypad)
|
|
270
|
+
return f"__form_{next_f_name}"
|
|
271
|
+
|
|
272
|
+
validated = self._validate_field(f_config, text)
|
|
273
|
+
|
|
274
|
+
if validated is False:
|
|
275
|
+
err_msg = getattr(f_config, 'invalid_answer', '❌ ورودی نامعتبر است')
|
|
276
|
+
await update.reply(err_msg)
|
|
277
|
+
return f"__form_{f_name}"
|
|
278
|
+
|
|
279
|
+
value = validated
|
|
280
|
+
if hasattr(f_config, 'transform') and f_config.transform:
|
|
281
|
+
try:
|
|
282
|
+
value = f_config.transform(text)
|
|
283
|
+
except Exception:
|
|
284
|
+
value = validated
|
|
285
|
+
|
|
286
|
+
data[f_name] = value
|
|
287
|
+
|
|
288
|
+
if hasattr(f_config, 'func') and f_config.func:
|
|
289
|
+
try:
|
|
290
|
+
res = f_config.func()
|
|
291
|
+
if asyncio.iscoroutine(res):
|
|
292
|
+
await res
|
|
293
|
+
except Exception:
|
|
294
|
+
pass
|
|
295
|
+
|
|
296
|
+
if last:
|
|
297
|
+
if config["on_finish"]:
|
|
298
|
+
try:
|
|
299
|
+
res = config["on_finish"](data)
|
|
300
|
+
if asyncio.iscoroutine(res):
|
|
301
|
+
await res
|
|
302
|
+
except Exception:
|
|
303
|
+
pass
|
|
304
|
+
await config["final_handler"](update, data)
|
|
305
|
+
return Conversation.END
|
|
306
|
+
|
|
307
|
+
next_f_config = config["fields"][idx + 1][1]
|
|
308
|
+
prompt = next_f_config.prompt
|
|
309
|
+
keypad = self._build_field_keypad(next_f_config)
|
|
310
|
+
await update.reply(prompt, keypad=keypad)
|
|
311
|
+
return f"__form_{next_f_name}"
|
|
312
|
+
|
|
313
|
+
return field_handler
|
|
314
|
+
|
|
315
|
+
handler = make_field_handler(
|
|
316
|
+
idx=i,
|
|
317
|
+
f_name=field_name,
|
|
318
|
+
f_config=field_config,
|
|
319
|
+
next_f_name=next_field_name,
|
|
320
|
+
last=is_last
|
|
321
|
+
)
|
|
322
|
+
state_name = f"__form_{field_name}"
|
|
323
|
+
self._states[state_name] = handler
|
|
324
|
+
|
|
325
|
+
return final_handler
|
|
326
|
+
|
|
327
|
+
return decorator
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _build_field_keypad(self, field_config) -> list | None:
|
|
331
|
+
"""ساختن کیبورد برای Number و Choice"""
|
|
332
|
+
from fast_rub.button import KeyPad
|
|
333
|
+
|
|
334
|
+
if hasattr(field_config, 'keypad') and field_config.keypad:
|
|
335
|
+
if field_config.min is not None and field_config.max is not None:
|
|
336
|
+
numbers = list(range(field_config.min, field_config.max + 1))
|
|
337
|
+
if getattr(field_config, 'sort_by', 'asc') == "desc":
|
|
338
|
+
numbers.reverse()
|
|
339
|
+
|
|
340
|
+
keypad = KeyPad()
|
|
341
|
+
row = []
|
|
342
|
+
max_row = getattr(field_config, 'max_btn_row', 4)
|
|
343
|
+
for num in numbers:
|
|
344
|
+
row.append(keypad.simple(str(num), str(num)))
|
|
345
|
+
if len(row) >= max_row:
|
|
346
|
+
keypad.append(*row)
|
|
347
|
+
row = []
|
|
348
|
+
if row:
|
|
349
|
+
keypad.append(*row)
|
|
350
|
+
return keypad.build()
|
|
351
|
+
|
|
352
|
+
if hasattr(field_config, 'options'):
|
|
353
|
+
options = field_config.options
|
|
354
|
+
if callable(options):
|
|
355
|
+
options = options()
|
|
356
|
+
|
|
357
|
+
keypad = KeyPad()
|
|
358
|
+
row = []
|
|
359
|
+
max_row = getattr(field_config, 'max_btn_row', 2)
|
|
360
|
+
for opt in options:
|
|
361
|
+
row.append(keypad.simple(opt, opt))
|
|
362
|
+
if len(row) >= max_row:
|
|
363
|
+
keypad.append(*row)
|
|
364
|
+
row = []
|
|
365
|
+
if row:
|
|
366
|
+
keypad.append(*row)
|
|
367
|
+
return keypad.build()
|
|
368
|
+
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _validate_field(self, field_config, value: str):
|
|
373
|
+
"""اعتبارسنجی فیلد"""
|
|
374
|
+
from .forms.fields import Text, Number, Choice
|
|
375
|
+
|
|
376
|
+
if hasattr(field_config, 'validator') and field_config.validator:
|
|
377
|
+
result = field_config.validator(value)
|
|
378
|
+
if asyncio.iscoroutine(result):
|
|
379
|
+
pass
|
|
380
|
+
if not result:
|
|
381
|
+
return False
|
|
382
|
+
|
|
383
|
+
if isinstance(field_config, Text):
|
|
384
|
+
if field_config.min_len and len(value) < field_config.min_len:
|
|
385
|
+
return False
|
|
386
|
+
if field_config.max_len and len(value) > field_config.max_len:
|
|
387
|
+
return False
|
|
388
|
+
if field_config.email and "@" not in value:
|
|
389
|
+
return False
|
|
390
|
+
if field_config.valid_inputs and value not in field_config.valid_inputs:
|
|
391
|
+
return False
|
|
392
|
+
return value
|
|
393
|
+
|
|
394
|
+
elif isinstance(field_config, Number):
|
|
395
|
+
try:
|
|
396
|
+
num = int(value)
|
|
397
|
+
except ValueError:
|
|
398
|
+
return False
|
|
399
|
+
if field_config.min is not None and num < field_config.min:
|
|
400
|
+
return False
|
|
401
|
+
if field_config.max is not None and num > field_config.max:
|
|
402
|
+
return False
|
|
403
|
+
return num
|
|
404
|
+
|
|
405
|
+
elif isinstance(field_config, Choice):
|
|
406
|
+
options = field_config.options
|
|
407
|
+
if callable(options):
|
|
408
|
+
options = options()
|
|
409
|
+
if value in options:
|
|
410
|
+
return value
|
|
411
|
+
return False
|
|
412
|
+
|
|
413
|
+
return value
|
|
414
|
+
|
|
415
|
+
def _cleanup(self, chat_id: str):
|
|
416
|
+
"""پاکسازی state کاربر بعد از تموم شدن مکالمه"""
|
|
417
|
+
self._user_states.pop(chat_id, None)
|
|
418
|
+
self._user_data.pop(chat_id, None)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
class ConversationManager:
|
|
422
|
+
"""مدیریت چندین مکالمه همزمان"""
|
|
423
|
+
def __init__(self):
|
|
424
|
+
self._conversations: dict[str, Conversation] = {}
|
|
425
|
+
|
|
426
|
+
def add(
|
|
427
|
+
self,
|
|
428
|
+
conversation: Conversation
|
|
429
|
+
):
|
|
430
|
+
self._conversations[conversation.name] = conversation
|
|
431
|
+
|
|
432
|
+
def remove(
|
|
433
|
+
self,
|
|
434
|
+
name: str
|
|
435
|
+
):
|
|
436
|
+
self._conversations.pop(name, None)
|
|
437
|
+
|
|
438
|
+
async def handle(
|
|
439
|
+
self,
|
|
440
|
+
update: Update,
|
|
441
|
+
client: Client
|
|
442
|
+
) -> bool:
|
|
443
|
+
"""همه مکالمات رو چک میکنه، اولویت با اولین مکالمه فعال"""
|
|
444
|
+
chat_id = update.chat_id
|
|
445
|
+
for conv in self._conversations.values():
|
|
446
|
+
if chat_id in conv._user_states:
|
|
447
|
+
return await conv.handle(update, client)
|
|
448
|
+
for conv in self._conversations.values():
|
|
449
|
+
if await conv._is_entry(update):
|
|
450
|
+
return await conv.handle(update, client)
|
|
451
|
+
return False
|