maxapi-python 2.4.1__py3-none-any.whl → 2.5.0__py3-none-any.whl

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 (74) hide show
  1. {maxapi_python-2.4.1.dist-info → maxapi_python-2.5.0.dist-info}/METADATA +17 -9
  2. {maxapi_python-2.4.1.dist-info → maxapi_python-2.5.0.dist-info}/RECORD +74 -45
  3. {maxapi_python-2.4.1.dist-info → maxapi_python-2.5.0.dist-info}/WHEEL +1 -1
  4. pymax/__init__.py +5 -1
  5. pymax/_data/apk_fingerprints.json +272 -0
  6. pymax/api/auth/service.py +1 -0
  7. pymax/api/auth/types.py +3 -0
  8. pymax/api/bots/payloads.py +8 -0
  9. pymax/api/bots/service.py +13 -1
  10. pymax/api/chats/enums.py +1 -0
  11. pymax/api/chats/payloads.py +19 -0
  12. pymax/api/chats/service.py +34 -0
  13. pymax/api/facade.py +2 -0
  14. pymax/api/messages/enums.py +1 -0
  15. pymax/api/messages/payloads.py +47 -1
  16. pymax/api/messages/service.py +308 -24
  17. pymax/api/response.py +1 -1
  18. pymax/api/stories/__init__.py +2 -0
  19. pymax/api/stories/enums.py +19 -0
  20. pymax/api/stories/payloads.py +64 -0
  21. pymax/api/stories/service.py +198 -0
  22. pymax/api/uploads/payloads.py +10 -1
  23. pymax/api/uploads/service.py +94 -7
  24. pymax/app.py +16 -4
  25. pymax/auth/qr.py +11 -1
  26. pymax/base.py +70 -25
  27. pymax/client.py +0 -1
  28. pymax/config.py +38 -32
  29. pymax/di/__init__.py +2 -0
  30. pymax/di/context.py +18 -0
  31. pymax/di/resolver.py +38 -0
  32. pymax/dispatch/__init__.py +7 -0
  33. pymax/dispatch/dispatcher.py +143 -40
  34. pymax/dispatch/enums.py +1 -0
  35. pymax/dispatch/mapping.py +23 -3
  36. pymax/dispatch/middleware.py +32 -0
  37. pymax/dispatch/resolvers.py +4 -0
  38. pymax/dispatch/router.py +86 -34
  39. pymax/filters/__init__.py +15 -0
  40. pymax/filters/base.py +218 -0
  41. pymax/filters/containers/__init__.py +2 -0
  42. pymax/filters/containers/chat.py +240 -0
  43. pymax/filters/containers/message.py +147 -0
  44. pymax/filters/filters.py +211 -0
  45. pymax/filters/magic.py +13 -0
  46. pymax/filters/presets.py +88 -0
  47. pymax/filters/refs.py +56 -0
  48. pymax/formatting/markdown.py +55 -1
  49. pymax/fsm/__init__.py +3 -0
  50. pymax/fsm/context.py +30 -0
  51. pymax/fsm/protocol.py +14 -0
  52. pymax/fsm/state.py +70 -0
  53. pymax/fsm/storage.py +151 -0
  54. pymax/infra/base.py +2 -0
  55. pymax/infra/bots.py +20 -0
  56. pymax/infra/chat.py +28 -0
  57. pymax/infra/message.py +195 -6
  58. pymax/infra/stories.py +205 -0
  59. pymax/integrations/__init__.py +0 -0
  60. pymax/integrations/dishka.py +109 -0
  61. pymax/protocol/enums.py +2 -0
  62. pymax/protocol/tcp/payload.py +0 -2
  63. pymax/types/domain/__init__.py +23 -1
  64. pymax/types/domain/attachments/__init__.py +1 -1
  65. pymax/types/domain/attachments/keyboards/__init__.py +1 -1
  66. pymax/types/domain/attachments/keyboards/enums.py +12 -0
  67. pymax/types/domain/attachments/keyboards/inline.py +74 -3
  68. pymax/types/domain/callback.py +19 -0
  69. pymax/types/domain/comment.py +24 -0
  70. pymax/types/domain/message.py +91 -22
  71. pymax/types/domain/story.py +126 -0
  72. pymax/types/events/reaction.py +28 -4
  73. pymax/versions/catalog.py +1 -1
  74. {maxapi_python-2.4.1.dist-info → maxapi_python-2.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -3,11 +3,15 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import inspect
5
5
  from contextlib import suppress
6
- from typing import TYPE_CHECKING, Any, Generic, TypeVar
6
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar, get_type_hints
7
7
 
8
+ from pydantic import BaseModel
9
+
10
+ from pymax.di import DiContext, DiResolver
11
+ from pymax.filters import Filter
8
12
  from pymax.logging import get_logger
9
13
  from pymax.protocol import InboundFrame
10
- from pymax.types import Chat, MessageDeleteEvent
14
+ from pymax.types import Chat, MessageDeleteEvent, StoryPreview
11
15
  from pymax.types.domain import Message
12
16
  from pymax.types.events import (
13
17
  MessageReadEvent,
@@ -17,7 +21,8 @@ from pymax.types.events import (
17
21
  )
18
22
 
19
23
  from .enums import EventType
20
- from .mapping import EventMapper, EventResolver
24
+ from .mapping import DispatchEvent, EventMapper, EventResolver
25
+ from .middleware import Middleware, MiddlewareNext
21
26
  from .router import (
22
27
  DisconnectCallback,
23
28
  DisconnectDecorator,
@@ -26,7 +31,6 @@ from .router import (
26
31
  ErrorEntry,
27
32
  ErrorScope,
28
33
  ErrorSource,
29
- FilterCallback,
30
34
  HandlerCallback,
31
35
  HandlerDecorator,
32
36
  HandlerEntry,
@@ -45,16 +49,20 @@ if TYPE_CHECKING:
45
49
  logger = get_logger(__name__)
46
50
 
47
51
  ClientT = TypeVar("ClientT", bound="BaseClient")
52
+ EventT = TypeVar("EventT", bound=BaseModel)
48
53
 
49
54
 
50
55
  class Dispatcher(Generic[ClientT]):
51
- def __init__(self, app: App, root_router: Router[ClientT] | None = None) -> None:
56
+ def __init__(self, app: App[ClientT], root_router: Router[ClientT] | None = None) -> None:
52
57
  self.root_router: Router[ClientT] = root_router or Router()
58
+ self.middlewares: list[Middleware[ClientT]] = self.root_router.middlewares
53
59
  self.internal_router: Router[ClientT] = Router()
54
60
  self.resolver = EventResolver()
55
61
  self.mapper = EventMapper(app)
56
62
  self.startup_tasks: list[asyncio.Task[Any]] = []
57
63
  self.client: ClientT | None = None
64
+ self.app = app
65
+ self.di_resolver = DiResolver()
58
66
 
59
67
  def bind_client(self, client: ClientT) -> None:
60
68
  self.client = client
@@ -67,10 +75,16 @@ class Dispatcher(Generic[ClientT]):
67
75
  len(router.children),
68
76
  )
69
77
 
78
+ def add_middleware(self, middleware: Middleware) -> None:
79
+ if middleware not in self.middlewares: # TODO: make better comparison or just remove it
80
+ self.middlewares.append(middleware)
81
+ else:
82
+ logger.warning(f"Middleware {middleware.__class__.__name__} is already registered")
83
+
70
84
  def on_internal(
71
85
  self,
72
86
  event: EventType,
73
- *filters: FilterCallback[Any],
87
+ *filters: Filter[Any, ClientT],
74
88
  ) -> HandlerDecorator[Any, ClientT]:
75
89
  logger.debug(
76
90
  "registering internal handler event=%s filters=%s",
@@ -82,7 +96,7 @@ class Dispatcher(Generic[ClientT]):
82
96
  def on(
83
97
  self,
84
98
  event: EventType,
85
- *filters: FilterCallback[Any],
99
+ *filters: Filter[Any, ClientT],
86
100
  ) -> HandlerDecorator[Any, ClientT]:
87
101
  logger.debug("registering handler event=%s filters=%s", event, len(filters))
88
102
  return self.root_router.on(event, *filters)
@@ -96,57 +110,63 @@ class Dispatcher(Generic[ClientT]):
96
110
 
97
111
  def on_message(
98
112
  self,
99
- *filters: FilterCallback[Message],
113
+ *filters: Filter[Message, ClientT],
100
114
  ) -> HandlerDecorator[Message, ClientT]:
101
115
  logger.debug("registering message handler filters=%s", len(filters))
102
116
  return self.root_router.on_message(*filters)
103
117
 
104
118
  def on_message_edit(
105
119
  self,
106
- *filters: FilterCallback[Message],
120
+ *filters: Filter[Message, ClientT],
107
121
  ) -> HandlerDecorator[Message, ClientT]:
108
122
  logger.debug("registering message edit handler filters=%s", len(filters))
109
123
  return self.root_router.on_message_edit(*filters)
110
124
 
111
125
  def on_message_delete(
112
126
  self,
113
- *filters: FilterCallback[MessageDeleteEvent],
127
+ *filters: Filter[MessageDeleteEvent, ClientT],
114
128
  ) -> HandlerDecorator[MessageDeleteEvent, ClientT]:
115
129
  return self.root_router.on_message_delete(*filters)
116
130
 
117
131
  def on_message_read(
118
132
  self,
119
- *filters: FilterCallback[MessageReadEvent],
133
+ *filters: Filter[MessageReadEvent, ClientT],
120
134
  ) -> HandlerDecorator[MessageReadEvent, ClientT]:
121
135
  return self.root_router.on_message_read(*filters)
122
136
 
123
137
  def on_typing(
124
138
  self,
125
- *filters: FilterCallback[TypingEvent],
139
+ *filters: Filter[TypingEvent, ClientT],
126
140
  ) -> HandlerDecorator[TypingEvent, ClientT]:
127
141
  return self.root_router.on_typing(*filters)
128
142
 
129
143
  def on_presence(
130
144
  self,
131
- *filters: FilterCallback[PresenceEvent],
145
+ *filters: Filter[PresenceEvent, ClientT],
132
146
  ) -> HandlerDecorator[PresenceEvent, ClientT]:
133
147
  return self.root_router.on_presence(*filters)
134
148
 
135
149
  def on_reaction_update(
136
150
  self,
137
- *filters: FilterCallback[ReactionUpdateEvent],
151
+ *filters: Filter[ReactionUpdateEvent, ClientT],
138
152
  ) -> HandlerDecorator[ReactionUpdateEvent, ClientT]:
139
153
  return self.root_router.on_reaction_update(*filters)
140
154
 
141
155
  def on_chat_update(
142
156
  self,
143
- *filters: FilterCallback[Chat],
157
+ *filters: Filter[Chat, ClientT],
144
158
  ) -> HandlerDecorator[Chat, ClientT]:
145
159
  return self.root_router.on_chat_update(*filters)
146
160
 
161
+ def on_story_update(
162
+ self,
163
+ *filters: Filter[StoryPreview, ClientT],
164
+ ) -> HandlerDecorator[StoryPreview, ClientT]:
165
+ return self.root_router.on_story_update(*filters)
166
+
147
167
  def on_raw(
148
168
  self,
149
- *filters: FilterCallback[InboundFrame],
169
+ *filters: Filter[InboundFrame, ClientT],
150
170
  ) -> HandlerDecorator[InboundFrame, ClientT]:
151
171
  return self.root_router.on_raw(*filters)
152
172
 
@@ -234,27 +254,41 @@ class Dispatcher(Generic[ClientT]):
234
254
  self.startup_tasks = []
235
255
 
236
256
  async def dispatch(self, frame: InboundFrame) -> None:
237
- event_type = self.resolver.resolve(frame)
257
+ try:
258
+ event_type = self.resolver.resolve(frame)
238
259
 
239
- if event_type is not None:
240
- logger.debug("dispatching event type=%s", event_type)
241
- event = self.mapper.map(event_type, frame)
242
- await self._dispatch_to_router(self.internal_router, event_type, event)
243
- await self._dispatch_to_router(self.root_router, event_type, event)
244
- else:
245
- logger.debug(
246
- "dispatching raw event only opcode=%s cmd=%s",
247
- frame.opcode,
248
- frame.cmd,
249
- )
260
+ if event_type is not None:
261
+ logger.debug("dispatching event type=%s", event_type)
262
+ event = self.mapper.map(event_type, frame)
250
263
 
251
- await self._dispatch_to_router(self.root_router, EventType.RAW, frame)
264
+ if event:
265
+ await self._dispatch_to_router(
266
+ self.internal_router,
267
+ event_type,
268
+ event,
269
+ apply_middleware=False,
270
+ )
271
+ if self.client:
272
+ async with self.app.fsm_storage.isolate(self.client, event):
273
+ await self._dispatch_to_router(self.root_router, event_type, event)
274
+ else:
275
+ await self._dispatch_to_router(self.root_router, event_type, event)
276
+ else:
277
+ logger.debug(
278
+ "dispatching raw event only opcode=%s cmd=%s",
279
+ frame.opcode,
280
+ frame.cmd,
281
+ )
282
+ finally:
283
+ await self._dispatch_to_router(self.root_router, EventType.RAW, frame)
252
284
 
253
285
  async def _dispatch_to_router(
254
286
  self,
255
287
  router: Router[ClientT],
256
288
  event_type: EventType,
257
- event: Any,
289
+ event: DispatchEvent,
290
+ *,
291
+ apply_middleware: bool = True,
258
292
  ) -> None:
259
293
  for entry in router.handlers.get(event_type, []):
260
294
  try:
@@ -264,24 +298,67 @@ class Dispatcher(Generic[ClientT]):
264
298
  event_type,
265
299
  _callback_name(entry.callback),
266
300
  )
267
- await self._call(entry.callback, event)
268
- except Exception as e: # noqa: PERF203
301
+ if apply_middleware:
302
+ await self._call_with_middleware(entry.callback, event)
303
+ else:
304
+ await self._call(entry.callback, event)
305
+ break
306
+ except Exception as e:
269
307
  handled = await self.emit_error(e, event_type, event, router, entry)
270
308
  if not handled:
271
309
  raise
272
310
 
273
311
  for child in router.children:
274
- await self._dispatch_to_router(child, event_type, event)
312
+ await self._dispatch_to_router(
313
+ child,
314
+ event_type,
315
+ event,
316
+ apply_middleware=apply_middleware,
317
+ )
318
+
319
+ async def _call_with_middleware(
320
+ self,
321
+ callback: HandlerCallback[DispatchEvent, ClientT],
322
+ event: DispatchEvent,
323
+ ) -> Any:
324
+
325
+ async def call_handler(event: DispatchEvent) -> Any:
326
+ return await self._call(callback, event)
327
+
328
+ call_next: MiddlewareNext = call_handler
329
+
330
+ for middleware in reversed(self.middlewares):
331
+ next_ = call_next
332
+
333
+ async def wrapped(
334
+ event: DispatchEvent,
335
+ middleware: Middleware[ClientT] = middleware,
336
+ next_: MiddlewareNext = next_,
337
+ ) -> Any:
338
+ if self.client is None:
339
+ raise RuntimeError("client is not bound")
340
+
341
+ return await middleware(
342
+ self.client,
343
+ event,
344
+ next_,
345
+ )
346
+
347
+ call_next = wrapped
348
+
349
+ return await call_next(event)
275
350
 
276
351
  async def _matches(
277
352
  self,
278
- entry: HandlerEntry[Any, ClientT],
279
- event: Any,
353
+ entry: HandlerEntry[DispatchEvent, ClientT],
354
+ event: DispatchEvent,
280
355
  ) -> bool:
281
356
  for flt in entry.filters:
282
- result = flt(event)
283
- if inspect.isawaitable(result):
284
- result = await result
357
+ if not self.client:
358
+ return False
359
+ filter_match = flt(event, self.client)
360
+ result = await flt.resolve_filter_result(filter_match)
361
+
285
362
  if not result:
286
363
  logger.debug(
287
364
  "handler skipped by filter callback=%s",
@@ -290,11 +367,37 @@ class Dispatcher(Generic[ClientT]):
290
367
  return False
291
368
  return True
292
369
 
293
- async def _call(self, callback: HandlerCallback[Any, ClientT], event: Any) -> Any:
370
+ async def _call(
371
+ self, callback: HandlerCallback[DispatchEvent, ClientT], event: DispatchEvent
372
+ ) -> Any:
294
373
  if self.client is None:
295
374
  raise RuntimeError("client is not bound")
296
375
 
297
- result = callback(event, self.client)
376
+ signature = inspect.signature(callback)
377
+ bound = signature.bind_partial(event, self.client)
378
+ bound.apply_defaults()
379
+
380
+ missing_parameters = [
381
+ (name, parameter)
382
+ for name, parameter in signature.parameters.items()
383
+ if name not in bound.arguments
384
+ ]
385
+
386
+ if missing_parameters:
387
+ hints = get_type_hints(callback, include_extras=True)
388
+
389
+ for name, parameter in missing_parameters:
390
+ annotation = hints.get(name, parameter.annotation)
391
+
392
+ ctx = DiContext(self.app, event, self.client)
393
+ result = await self.di_resolver.resolve(annotation, ctx)
394
+
395
+ if result is None:
396
+ raise ValueError(f"No resolver was found for parameter: `{parameter}`")
397
+
398
+ bound.arguments[name] = result
399
+
400
+ result = callback(*bound.args, **bound.kwargs)
298
401
 
299
402
  if inspect.isawaitable(result):
300
403
  return await result
pymax/dispatch/enums.py CHANGED
@@ -10,6 +10,7 @@ class EventType(str, Enum):
10
10
  PRESENCE = "presence"
11
11
  REACTION_UPDATE = "reaction_update"
12
12
  CHAT_UPDATE = "chat_update"
13
+ STORY_UPDATE = "story_update"
13
14
  USER_UPDATE = "user_update"
14
15
  VIDEO_READY = "video_ready"
15
16
  FILE_READY = "file_ready"
pymax/dispatch/mapping.py CHANGED
@@ -1,13 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections.abc import Callable
4
- from typing import TYPE_CHECKING
4
+ from typing import TYPE_CHECKING, TypeAlias
5
5
 
6
6
  from pymax.api.binding import bind_api_model
7
7
  from pymax.protocol import InboundFrame, Opcode
8
8
  from pymax.protocol.enums import Command
9
9
  from pymax.types import AudioUploadSignal, Chat, MessageDeleteEvent
10
- from pymax.types.domain import Message
10
+ from pymax.types.domain import Message, StoryPreview
11
11
  from pymax.types.events import (
12
12
  FileUploadSignal,
13
13
  MessageReadEvent,
@@ -26,6 +26,7 @@ from .resolvers import (
26
26
  resolve_message_read,
27
27
  resolve_presence,
28
28
  resolve_reaction_update,
29
+ resolve_story_update,
29
30
  resolve_typing,
30
31
  )
31
32
 
@@ -33,6 +34,21 @@ if TYPE_CHECKING:
33
34
  from pymax.app import App
34
35
 
35
36
  Resolver = Callable[[InboundFrame], EventType | None]
37
+ DispatchEvent: TypeAlias = (
38
+ Message
39
+ | Chat
40
+ | MessageDeleteEvent
41
+ | MessageReadEvent
42
+ | TypingEvent
43
+ | PresenceEvent
44
+ | ReactionUpdateEvent
45
+ | StoryPreview
46
+ | VideoUploadSignal
47
+ | FileUploadSignal
48
+ | AudioUploadSignal
49
+ | InboundFrame
50
+ )
51
+
36
52
 
37
53
  EVENT_MAP: dict[Opcode, Resolver] = {
38
54
  Opcode.NOTIF_MESSAGE: resolve_message,
@@ -44,6 +60,8 @@ EVENT_MAP: dict[Opcode, Resolver] = {
44
60
  Opcode.NOTIF_MARK: resolve_message_read,
45
61
  Opcode.NOTIF_PRESENCE: resolve_presence,
46
62
  Opcode.NOTIF_MSG_REACTIONS_CHANGED: resolve_reaction_update,
63
+ Opcode.NOTIF_MSG_YOU_REACTED: resolve_reaction_update,
64
+ Opcode.NOTIF_STORIES_UPDATE: resolve_story_update,
47
65
  }
48
66
 
49
67
 
@@ -68,7 +86,7 @@ class EventMapper:
68
86
  def __init__(self, app: App) -> None:
69
87
  self.app = app
70
88
 
71
- def map(self, event_type: EventType, frame: InboundFrame):
89
+ def map(self, event_type: EventType, frame: InboundFrame) -> DispatchEvent | None:
72
90
  if frame.cmd != Command.REQUEST:
73
91
  return None
74
92
 
@@ -96,6 +114,8 @@ class EventMapper:
96
114
  return PresenceEvent.model_validate(frame.payload)
97
115
  elif event_type == EventType.REACTION_UPDATE:
98
116
  return ReactionUpdateEvent.model_validate(frame.payload)
117
+ elif event_type == EventType.STORY_UPDATE:
118
+ return StoryPreview.model_validate(frame.payload["storiesPreview"])
99
119
  elif event_type == EventType.VIDEO_READY:
100
120
  return VideoUploadSignal.model_validate(frame.payload)
101
121
  elif event_type == EventType.FILE_READY:
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Awaitable, Callable
4
+ from typing import TYPE_CHECKING, Any, Generic, Protocol, TypeAlias, TypeVar
5
+
6
+ from .mapping import DispatchEvent
7
+
8
+ if TYPE_CHECKING:
9
+ from pymax.base import BaseClient
10
+
11
+
12
+ ClientT_contra = TypeVar("ClientT_contra", bound="BaseClient", contravariant=True)
13
+
14
+ MiddlewareNext: TypeAlias = Callable[[DispatchEvent], Awaitable[Any]]
15
+ """Следующий шаг middleware-цепочки, вызываемый как ``await call_next(event)``."""
16
+
17
+
18
+ class Middleware(Protocol, Generic[ClientT_contra]):
19
+ """Асинхронная обертка выбранного обработчика события.
20
+
21
+ Реализуйте ``async __call__(client, event, call_next)`` и зарегистрируйте
22
+ экземпляр через ``client.add_middleware()``. Middleware запускаются после
23
+ фильтров в порядке регистрации для обработчиков всех подключенных роутеров.
24
+ Вызов ``await call_next(event)`` продолжает цепочку и возвращает ее результат.
25
+ """
26
+
27
+ async def __call__(
28
+ self,
29
+ client: ClientT_contra,
30
+ event: DispatchEvent,
31
+ call_next: MiddlewareNext,
32
+ ) -> Any: ...
@@ -36,6 +36,10 @@ def resolve_reaction_update(_: InboundFrame) -> EventType | None:
36
36
  return EventType.REACTION_UPDATE
37
37
 
38
38
 
39
+ def resolve_story_update(_: InboundFrame) -> EventType | None:
40
+ return EventType.STORY_UPDATE
41
+
42
+
39
43
  def resolve_attach(frame: InboundFrame) -> EventType | None:
40
44
  try:
41
45
  FileUploadSignal.model_validate(frame.payload)