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.
Files changed (97) hide show
  1. {fastrub-5.2.2 → fastrub-5.2.3}/PKG-INFO +1 -1
  2. fastrub-5.2.3/fast_rub/core/conversation.py +451 -0
  3. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/forms/manager.py +131 -144
  4. fastrub-5.2.3/fast_rub/utils/version.py +1 -0
  5. {fastrub-5.2.2 → fastrub-5.2.3}/fastrub.egg-info/PKG-INFO +1 -1
  6. fastrub-5.2.2/fast_rub/core/conversation.py +0 -160
  7. fastrub-5.2.2/fast_rub/utils/version.py +0 -1
  8. {fastrub-5.2.2 → fastrub-5.2.3}/LICENSE +0 -0
  9. {fastrub-5.2.2 → fastrub-5.2.3}/README.md +0 -0
  10. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/__init__.py +0 -0
  11. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/button/__init__.py +0 -0
  12. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/button/key_pad.py +0 -0
  13. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/cli.py +0 -0
  14. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/__init__.py +0 -0
  15. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/async_sync.py +0 -0
  16. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/background.py +0 -0
  17. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/client.py +0 -0
  18. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/forms/__init__.py +0 -0
  19. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/forms/config.py +0 -0
  20. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/forms/data_form.py +0 -0
  21. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/forms/decorators.py +0 -0
  22. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/forms/fields.py +0 -0
  23. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/helpers.py +0 -0
  24. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/hotreload.py +0 -0
  25. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/middleware.py +0 -0
  26. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/plugins.py +0 -0
  27. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/scheduler.py +0 -0
  28. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/signals.py +0 -0
  29. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/core/webhook_server.py +0 -0
  30. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/db/__init__.py +0 -0
  31. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/db/database.py +0 -0
  32. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/db/message_keeper.py +0 -0
  33. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/db/session.py +0 -0
  34. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/network/__init__.py +0 -0
  35. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/network/network.py +0 -0
  36. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrightconfig.json +0 -0
  37. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/__init__.py +0 -0
  38. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/client/__init__.py +0 -0
  39. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/client/client.py +0 -0
  40. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/crypto/__init__.py +0 -0
  41. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/crypto/crypto.py +0 -0
  42. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/enums.py +0 -0
  43. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/exceptions.py +0 -0
  44. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/filters.py +0 -0
  45. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/methods/__init__.py +0 -0
  46. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/methods/methods.py +0 -0
  47. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/network/__init__.py +0 -0
  48. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/network/helper.py +0 -0
  49. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/network/network.py +0 -0
  50. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/network/socket.py +0 -0
  51. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/sessions/__init__.py +0 -0
  52. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/sessions/sessions.py +0 -0
  53. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/types/__init__.py +0 -0
  54. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/types/socket/__init__.py +0 -0
  55. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/types/socket/message.py +0 -0
  56. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/utils/__init__.py +0 -0
  57. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/utils/configs.py +0 -0
  58. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/utils/reacrions.py +0 -0
  59. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/pyrubi/utils/utils.py +0 -0
  60. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/type/__init__.py +0 -0
  61. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/__init__.py +0 -0
  62. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/errors.py +0 -0
  63. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/exceptions.py +0 -0
  64. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/file.py +0 -0
  65. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/get_type.py +0 -0
  66. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/metadata.py +0 -0
  67. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/models/__init__.py +0 -0
  68. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/models/base_model.py +0 -0
  69. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/models/bot.py +0 -0
  70. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/models/chat.py +0 -0
  71. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/prop_update.py +0 -0
  72. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/props.py +0 -0
  73. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/update.py +0 -0
  74. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/update_button.py +0 -0
  75. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/types/webhook.py +0 -0
  76. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/__init__.py +0 -0
  77. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/antispam.py +0 -0
  78. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/cache.py +0 -0
  79. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/colors.py +0 -0
  80. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/encryption.py +0 -0
  81. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/filters.py +0 -0
  82. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/inline_filters.py +0 -0
  83. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/logger.py +0 -0
  84. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/metadata.py +0 -0
  85. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/run_filter.py +0 -0
  86. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/snapshot.py +0 -0
  87. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/template.py +0 -0
  88. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/text_parser.py +0 -0
  89. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/utils.py +0 -0
  90. {fastrub-5.2.2 → fastrub-5.2.3}/fast_rub/utils/wait_manager.py +0 -0
  91. {fastrub-5.2.2 → fastrub-5.2.3}/fastrub.egg-info/SOURCES.txt +0 -0
  92. {fastrub-5.2.2 → fastrub-5.2.3}/fastrub.egg-info/dependency_links.txt +0 -0
  93. {fastrub-5.2.2 → fastrub-5.2.3}/fastrub.egg-info/entry_points.txt +0 -0
  94. {fastrub-5.2.2 → fastrub-5.2.3}/fastrub.egg-info/requires.txt +0 -0
  95. {fastrub-5.2.2 → fastrub-5.2.3}/fastrub.egg-info/top_level.txt +0 -0
  96. {fastrub-5.2.2 → fastrub-5.2.3}/pyproject.toml +0 -0
  97. {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.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