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.
Files changed (97) hide show
  1. {fastrub-5.2.2 → fastrub-5.2.4}/PKG-INFO +1 -1
  2. fastrub-5.2.4/fast_rub/core/conversation.py +437 -0
  3. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/forms/manager.py +131 -144
  4. fastrub-5.2.4/fast_rub/utils/version.py +1 -0
  5. {fastrub-5.2.2 → fastrub-5.2.4}/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.4}/LICENSE +0 -0
  9. {fastrub-5.2.2 → fastrub-5.2.4}/README.md +0 -0
  10. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/__init__.py +0 -0
  11. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/button/__init__.py +0 -0
  12. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/button/key_pad.py +0 -0
  13. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/cli.py +0 -0
  14. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/__init__.py +0 -0
  15. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/async_sync.py +0 -0
  16. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/background.py +0 -0
  17. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/client.py +0 -0
  18. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/forms/__init__.py +0 -0
  19. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/forms/config.py +0 -0
  20. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/forms/data_form.py +0 -0
  21. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/forms/decorators.py +0 -0
  22. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/forms/fields.py +0 -0
  23. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/helpers.py +0 -0
  24. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/hotreload.py +0 -0
  25. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/middleware.py +0 -0
  26. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/plugins.py +0 -0
  27. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/scheduler.py +0 -0
  28. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/signals.py +0 -0
  29. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/core/webhook_server.py +0 -0
  30. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/db/__init__.py +0 -0
  31. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/db/database.py +0 -0
  32. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/db/message_keeper.py +0 -0
  33. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/db/session.py +0 -0
  34. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/network/__init__.py +0 -0
  35. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/network/network.py +0 -0
  36. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrightconfig.json +0 -0
  37. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/__init__.py +0 -0
  38. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/client/__init__.py +0 -0
  39. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/client/client.py +0 -0
  40. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/crypto/__init__.py +0 -0
  41. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/crypto/crypto.py +0 -0
  42. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/enums.py +0 -0
  43. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/exceptions.py +0 -0
  44. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/filters.py +0 -0
  45. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/methods/__init__.py +0 -0
  46. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/methods/methods.py +0 -0
  47. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/network/__init__.py +0 -0
  48. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/network/helper.py +0 -0
  49. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/network/network.py +0 -0
  50. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/network/socket.py +0 -0
  51. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/sessions/__init__.py +0 -0
  52. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/sessions/sessions.py +0 -0
  53. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/types/__init__.py +0 -0
  54. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/types/socket/__init__.py +0 -0
  55. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/types/socket/message.py +0 -0
  56. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/utils/__init__.py +0 -0
  57. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/utils/configs.py +0 -0
  58. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/utils/reacrions.py +0 -0
  59. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/pyrubi/utils/utils.py +0 -0
  60. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/type/__init__.py +0 -0
  61. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/__init__.py +0 -0
  62. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/errors.py +0 -0
  63. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/exceptions.py +0 -0
  64. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/file.py +0 -0
  65. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/get_type.py +0 -0
  66. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/metadata.py +0 -0
  67. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/models/__init__.py +0 -0
  68. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/models/base_model.py +0 -0
  69. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/models/bot.py +0 -0
  70. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/models/chat.py +0 -0
  71. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/prop_update.py +0 -0
  72. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/props.py +0 -0
  73. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/update.py +0 -0
  74. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/update_button.py +0 -0
  75. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/types/webhook.py +0 -0
  76. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/__init__.py +0 -0
  77. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/antispam.py +0 -0
  78. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/cache.py +0 -0
  79. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/colors.py +0 -0
  80. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/encryption.py +0 -0
  81. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/filters.py +0 -0
  82. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/inline_filters.py +0 -0
  83. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/logger.py +0 -0
  84. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/metadata.py +0 -0
  85. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/run_filter.py +0 -0
  86. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/snapshot.py +0 -0
  87. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/template.py +0 -0
  88. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/text_parser.py +0 -0
  89. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/utils.py +0 -0
  90. {fastrub-5.2.2 → fastrub-5.2.4}/fast_rub/utils/wait_manager.py +0 -0
  91. {fastrub-5.2.2 → fastrub-5.2.4}/fastrub.egg-info/SOURCES.txt +0 -0
  92. {fastrub-5.2.2 → fastrub-5.2.4}/fastrub.egg-info/dependency_links.txt +0 -0
  93. {fastrub-5.2.2 → fastrub-5.2.4}/fastrub.egg-info/entry_points.txt +0 -0
  94. {fastrub-5.2.2 → fastrub-5.2.4}/fastrub.egg-info/requires.txt +0 -0
  95. {fastrub-5.2.2 → fastrub-5.2.4}/fastrub.egg-info/top_level.txt +0 -0
  96. {fastrub-5.2.2 → fastrub-5.2.4}/pyproject.toml +0 -0
  97. {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.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 run(self) -> dict | None:
23
- try:
24
- await self._call_callback(self.config.on_start)
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
- except Exception as e:
81
- await self._call_callback(
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 _should_skip(self, field_name: str) -> bool:
88
- field_config = self.fields[self.current_index][1]
32
+ async def handle_update(self, update) -> bool:
33
+ """پردازش آپدیت جدید. برگشت True یعنی فرم هنوز ادامه داره"""
34
+ if not self._running:
35
+ return False
89
36
 
90
- if hasattr(field_config, 'skip_if') and field_config.skip_if:
91
- if field_config.skip_if(self.data):
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
- if hasattr(field_config, 'depends_on') and field_config.depends_on:
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
- return False
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 _ask_field(self, field_name: str, field_config: Text | Number | Choice) -> Any:
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
- msg = await self.update.reply(prompt, keypad=keypad)
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
- def _build_number_keypad(self, field_config: Number) -> list | None:
144
- if field_config.min is None or field_config.max is None:
145
- return None
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
- keypad = KeyPad()
155
- row = []
156
- for num in numbers:
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
- return keypad.build()
165
-
166
- def _build_choice_keypad(self, options: list[str], max_btn_row: int) -> list | None:
167
- keypad = KeyPad()
168
- row = []
169
- for opt in options:
170
- row.append(keypad.simple(opt, opt))
171
- if len(row) >= max_btn_row:
172
- keypad.append(*row)
173
- row = []
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 keypad.build()
135
+ return False
178
136
 
179
- async def _validate(self, field_config: Text | Number | Choice, value: str) -> bool:
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 as e:
253
- await self._call_callback(
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 as e:
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.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