maxapi-python 2.1.3__py3-none-any.whl → 2.3.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 (64) hide show
  1. {maxapi_python-2.1.3.dist-info → maxapi_python-2.3.0.dist-info}/METADATA +3 -11
  2. {maxapi_python-2.1.3.dist-info → maxapi_python-2.3.0.dist-info}/RECORD +64 -59
  3. pymax/__init__.py +18 -3
  4. pymax/api/auth/payloads.py +7 -0
  5. pymax/api/auth/service.py +33 -30
  6. pymax/api/binding.py +57 -0
  7. pymax/api/chats/payloads.py +6 -0
  8. pymax/api/chats/service.py +52 -47
  9. pymax/api/messages/enums.py +1 -0
  10. pymax/api/messages/payloads.py +16 -1
  11. pymax/api/messages/service.py +78 -34
  12. pymax/api/models.py +4 -6
  13. pymax/api/response.py +2 -2
  14. pymax/api/self/service.py +17 -26
  15. pymax/api/session/payloads.py +2 -9
  16. pymax/api/session/service.py +1 -3
  17. pymax/api/uploads/payloads.py +3 -9
  18. pymax/api/uploads/service.py +33 -99
  19. pymax/api/users/payloads.py +22 -0
  20. pymax/api/users/service.py +22 -17
  21. pymax/app.py +28 -6
  22. pymax/auth/qr.py +3 -9
  23. pymax/auth/sms.py +23 -11
  24. pymax/base.py +86 -4
  25. pymax/client.py +2 -1
  26. pymax/client_web.py +1 -2
  27. pymax/config.py +42 -3
  28. pymax/connection/connection.py +2 -0
  29. pymax/connection/readers/tcp.py +1 -3
  30. pymax/dispatch/__init__.py +12 -1
  31. pymax/dispatch/dispatcher.py +170 -34
  32. pymax/dispatch/enums.py +5 -0
  33. pymax/dispatch/mapping.py +34 -11
  34. pymax/dispatch/resolvers.py +18 -0
  35. pymax/dispatch/router.py +120 -4
  36. pymax/formatting/markdown.py +22 -13
  37. pymax/infra/chat.py +33 -0
  38. pymax/infra/message.py +69 -2
  39. pymax/infra/user.py +12 -1
  40. pymax/logging.py +2 -0
  41. pymax/protocol/tcp/compression.py +1 -3
  42. pymax/protocol/tcp/framing.py +1 -3
  43. pymax/protocol/ws/protocol.py +3 -9
  44. pymax/session/protocol.py +2 -6
  45. pymax/session/store.py +19 -24
  46. pymax/telemetry/navigation.py +1 -3
  47. pymax/telemetry/service.py +5 -17
  48. pymax/transport/tcp.py +1 -3
  49. pymax/types/domain/__init__.py +1 -1
  50. pymax/types/domain/attachments/unknown.py +1 -3
  51. pymax/types/domain/auth.py +24 -2
  52. pymax/types/domain/chat.py +58 -1
  53. pymax/types/domain/message.py +28 -2
  54. pymax/types/domain/presence.py +3 -3
  55. pymax/types/domain/sync.py +5 -21
  56. pymax/types/domain/user.py +8 -0
  57. pymax/types/events/__init__.py +4 -0
  58. pymax/types/events/mark.py +23 -0
  59. pymax/types/events/message.py +57 -5
  60. pymax/types/events/presence.py +15 -0
  61. pymax/types/events/reaction.py +21 -0
  62. pymax/types/events/typing.py +14 -0
  63. {maxapi_python-2.1.3.dist-info → maxapi_python-2.3.0.dist-info}/WHEEL +0 -0
  64. {maxapi_python-2.1.3.dist-info → maxapi_python-2.3.0.dist-info}/licenses/LICENSE +0 -0
pymax/config.py CHANGED
@@ -84,6 +84,21 @@ class DeviceConfig(BaseModel):
84
84
  client_session_id: int = Field(default_factory=lambda: randint(1, 70))
85
85
 
86
86
 
87
+ class RegistrationConfig(BaseModel):
88
+ """Данные профиля для регистрации нового аккаунта по SMS.
89
+
90
+ Передайте объект через ``ExtraConfig.registration_config``. Он используется
91
+ только если после подтверждения SMS-кода Max вернул токен регистрации.
92
+
93
+ Args:
94
+ first_name: Имя нового пользователя.
95
+ last_name: Фамилия нового пользователя.
96
+ """
97
+
98
+ first_name: str
99
+ last_name: str | None = None
100
+
101
+
87
102
  class ClientConfig(BaseModel):
88
103
  model_config = ConfigDict(arbitrary_types_allowed=True)
89
104
 
@@ -93,6 +108,7 @@ class ClientConfig(BaseModel):
93
108
  device: DeviceConfig
94
109
  token: str | None = None
95
110
  proxy: str | None = None
111
+ registration_config: RegistrationConfig | None = None
96
112
 
97
113
  host: str = "api.oneme.ru"
98
114
  port: int = 443
@@ -109,9 +125,7 @@ class ClientConfig(BaseModel):
109
125
 
110
126
  def ensure_config(self) -> None:
111
127
  if not self.phone:
112
- raise ValueError(
113
- "Phone must be provided when no saved session exists."
114
- )
128
+ raise ValueError("Phone must be provided when no saved session exists.")
115
129
 
116
130
 
117
131
  class ExtraConfig(BaseModel):
@@ -122,6 +136,8 @@ class ExtraConfig(BaseModel):
122
136
 
123
137
  Args:
124
138
  token: Готовый token для создания сессии без SMS/QR.
139
+ registration_config: Имя и фамилия для автоматического завершения
140
+ регистрации нового аккаунта по SMS.
125
141
  host: TCP host Max API.
126
142
  port: TCP port Max API.
127
143
  url: WebSocket URL для ``WebClient``.
@@ -156,6 +172,7 @@ class ExtraConfig(BaseModel):
156
172
  model_config = ConfigDict(arbitrary_types_allowed=True)
157
173
 
158
174
  token: str | None = None
175
+ registration_config: RegistrationConfig | None = None
159
176
 
160
177
  host: str = "api.oneme.ru"
161
178
  port: int = 443
@@ -221,3 +238,25 @@ class ExtraConfig(BaseModel):
221
238
  device_locale=locale,
222
239
  header_user_agent=DEFAULT_WEB_HEADER_USER_AGENT,
223
240
  )
241
+
242
+
243
+ # ignore. for future upd
244
+
245
+ # class TcpOptions(BaseModel):
246
+ # host: str = "api.oneme.ru"
247
+ # port: int = 443
248
+ # use_ssl: bool = True
249
+ # proxy: str | None = None
250
+
251
+
252
+ # class RuntimeOptions(BaseModel):
253
+ # request_timeout: float = 30.0
254
+ # reconnect: bool = True
255
+ # reconnect_delay: float = 1.0
256
+
257
+
258
+ # class DeviceOptions(BaseModel):
259
+ # device_id: str | None = None
260
+ # device_type: DeviceType = DeviceType.ANDROID
261
+ # user_agent: MobileUserAgentPayload | None = None
262
+ # mt_instance_id: str = Field(default_factory=lambda: str(uuid4()))
@@ -183,7 +183,9 @@ class ConnectionManager:
183
183
  except Exception as e:
184
184
  exc = ConnectionError(f"Connection error: {e}")
185
185
  logger.exception("connection receive loop failed")
186
+
186
187
  self.requests.cancel_all(exc=exc)
188
+
187
189
  self._connection_lost = True
188
190
  self._mark_closed(exc)
189
191
  raise e
@@ -8,9 +8,7 @@ logger = get_logger(__name__)
8
8
 
9
9
 
10
10
  class TCPReader(BaseReader):
11
- def __init__(
12
- self, transport: TCPTransport, framer: TcpPacketFramer
13
- ) -> None:
11
+ def __init__(self, transport: TCPTransport, framer: TcpPacketFramer) -> None:
14
12
  super().__init__()
15
13
  self.transport = transport
16
14
  self.framer = framer
@@ -1,10 +1,21 @@
1
1
  from .dispatcher import Dispatcher
2
2
  from .enums import EventType
3
- from .router import ClientRouter, Router
3
+ from .router import (
4
+ ClientRouter,
5
+ DisconnectCallback,
6
+ DisconnectDecorator,
7
+ ErrorContext,
8
+ ErrorScope,
9
+ Router,
10
+ )
4
11
 
5
12
  __all__ = (
6
13
  "ClientRouter",
14
+ "DisconnectCallback",
15
+ "DisconnectDecorator",
7
16
  "Dispatcher",
17
+ "ErrorContext",
18
+ "ErrorScope",
8
19
  "EventType",
9
20
  "Router",
10
21
  )
@@ -9,15 +9,29 @@ from pymax.logging import get_logger
9
9
  from pymax.protocol import InboundFrame
10
10
  from pymax.types import Chat, MessageDeleteEvent
11
11
  from pymax.types.domain import Message
12
+ from pymax.types.events import (
13
+ MessageReadEvent,
14
+ PresenceEvent,
15
+ ReactionUpdateEvent,
16
+ TypingEvent,
17
+ )
12
18
 
13
19
  from .enums import EventType
14
20
  from .mapping import EventMapper, EventResolver
15
21
  from .router import (
22
+ DisconnectCallback,
23
+ DisconnectDecorator,
24
+ ErrorContext,
25
+ ErrorDecorator,
26
+ ErrorEntry,
27
+ ErrorScope,
28
+ ErrorSource,
16
29
  FilterCallback,
17
30
  HandlerCallback,
18
31
  HandlerDecorator,
19
32
  HandlerEntry,
20
33
  Router,
34
+ StartCallback,
21
35
  StartDecorator,
22
36
  )
23
37
 
@@ -25,17 +39,16 @@ if TYPE_CHECKING:
25
39
  from collections.abc import Generator
26
40
 
27
41
  from pymax.app import App
42
+ from pymax.base import BaseClient
28
43
 
29
44
 
30
45
  logger = get_logger(__name__)
31
46
 
32
- ClientT = TypeVar("ClientT")
47
+ ClientT = TypeVar("ClientT", bound="BaseClient")
33
48
 
34
49
 
35
50
  class Dispatcher(Generic[ClientT]):
36
- def __init__(
37
- self, app: App, root_router: Router[ClientT] | None = None
38
- ) -> None:
51
+ def __init__(self, app: App, root_router: Router[ClientT] | None = None) -> None:
39
52
  self.root_router: Router[ClientT] = root_router or Router()
40
53
  self.internal_router: Router[ClientT] = Router()
41
54
  self.resolver = EventResolver()
@@ -71,11 +84,16 @@ class Dispatcher(Generic[ClientT]):
71
84
  event: EventType,
72
85
  *filters: FilterCallback[Any],
73
86
  ) -> HandlerDecorator[Any, ClientT]:
74
- logger.debug(
75
- "registering handler event=%s filters=%s", event, len(filters)
76
- )
87
+ logger.debug("registering handler event=%s filters=%s", event, len(filters))
77
88
  return self.root_router.on(event, *filters)
78
89
 
90
+ def on_error(self, scope: ErrorScope = ErrorScope.GLOBAL) -> ErrorDecorator[ClientT]:
91
+ return self.root_router.on_error(scope)
92
+
93
+ def on_disconnect(self) -> DisconnectDecorator:
94
+ """Регистрирует обработчик сетевого отключения на root router."""
95
+ return self.root_router.on_disconnect()
96
+
79
97
  def on_message(
80
98
  self,
81
99
  *filters: FilterCallback[Message],
@@ -87,9 +105,7 @@ class Dispatcher(Generic[ClientT]):
87
105
  self,
88
106
  *filters: FilterCallback[Message],
89
107
  ) -> HandlerDecorator[Message, ClientT]:
90
- logger.debug(
91
- "registering message edit handler filters=%s", len(filters)
92
- )
108
+ logger.debug("registering message edit handler filters=%s", len(filters))
93
109
  return self.root_router.on_message_edit(*filters)
94
110
 
95
111
  def on_message_delete(
@@ -98,6 +114,30 @@ class Dispatcher(Generic[ClientT]):
98
114
  ) -> HandlerDecorator[MessageDeleteEvent, ClientT]:
99
115
  return self.root_router.on_message_delete(*filters)
100
116
 
117
+ def on_message_read(
118
+ self,
119
+ *filters: FilterCallback[MessageReadEvent],
120
+ ) -> HandlerDecorator[MessageReadEvent, ClientT]:
121
+ return self.root_router.on_message_read(*filters)
122
+
123
+ def on_typing(
124
+ self,
125
+ *filters: FilterCallback[TypingEvent],
126
+ ) -> HandlerDecorator[TypingEvent, ClientT]:
127
+ return self.root_router.on_typing(*filters)
128
+
129
+ def on_presence(
130
+ self,
131
+ *filters: FilterCallback[PresenceEvent],
132
+ ) -> HandlerDecorator[PresenceEvent, ClientT]:
133
+ return self.root_router.on_presence(*filters)
134
+
135
+ def on_reaction_update(
136
+ self,
137
+ *filters: FilterCallback[ReactionUpdateEvent],
138
+ ) -> HandlerDecorator[ReactionUpdateEvent, ClientT]:
139
+ return self.root_router.on_reaction_update(*filters)
140
+
101
141
  def on_chat_update(
102
142
  self,
103
143
  *filters: FilterCallback[Chat],
@@ -116,30 +156,68 @@ class Dispatcher(Generic[ClientT]):
116
156
  def iter_routers(self) -> Generator[Router[ClientT], Any, None]:
117
157
  yield from self._iter_router(self.root_router)
118
158
 
119
- def _iter_router(
120
- self, router: Router[ClientT]
121
- ) -> Generator[Router[ClientT], Any, None]:
159
+ def _iter_router(self, router: Router[ClientT]) -> Generator[Router[ClientT], Any, None]:
122
160
  yield router
123
161
 
124
162
  for child in router.children:
125
163
  yield from self._iter_router(child)
126
164
 
127
- async def emit_start(self, client: ClientT) -> None:
128
- tasks: list[asyncio.Task[Any]] = []
165
+ def iter_error_entries(
166
+ self,
167
+ ) -> Generator[tuple[Router[ClientT], ErrorEntry[ClientT]], Any, None]:
168
+ for router in self.iter_routers():
169
+ for entry in router.error_handlers:
170
+ yield router, entry
129
171
 
172
+ def iter_disconnect_handlers(self) -> Generator[DisconnectCallback, Any, None]:
173
+ """Итерирует обработчики disconnect по root router и его детям."""
130
174
  for router in self.iter_routers():
131
- handler = router.on_start_handler
132
- if handler is None:
175
+ yield from router.disconnect_handlers
176
+
177
+ def iter_error_handlers(
178
+ self,
179
+ failed_router: Router[ClientT],
180
+ ) -> Generator[ErrorEntry[ClientT], Any, None]:
181
+ for owner_router, entry in self.iter_error_entries():
182
+ if entry.scope is ErrorScope.LOCAL and owner_router is not failed_router:
133
183
  continue
134
184
 
135
- result = handler(client)
185
+ yield entry
136
186
 
137
- if inspect.iscoroutine(result):
138
- task = asyncio.create_task(result)
187
+ async def emit_start(self, client: ClientT) -> None:
188
+ tasks: list[asyncio.Task[Any]] = []
189
+
190
+ for router in self.iter_routers(): # TODO: create iter_on_start_handlers
191
+ for handler in router.on_start_handlers:
192
+ task = asyncio.create_task(self._run_start_handler(router, handler, client))
139
193
  task.add_done_callback(_log_task_error)
140
194
  tasks.append(task)
141
195
 
142
- self.startup_tasks = tasks
196
+ self.startup_tasks.extend(tasks)
197
+
198
+ async def _run_start_handler(
199
+ self,
200
+ router: Router[ClientT],
201
+ handler: StartCallback[ClientT],
202
+ client: ClientT,
203
+ ) -> None:
204
+ try:
205
+ result = handler(client)
206
+
207
+ if inspect.isawaitable(result):
208
+ await result
209
+
210
+ except Exception as e:
211
+ handled = await self.emit_error(
212
+ e,
213
+ EventType.ON_START,
214
+ None,
215
+ router,
216
+ handler,
217
+ )
218
+
219
+ if not handled:
220
+ raise
143
221
 
144
222
  async def stop_startup_tasks(self) -> None:
145
223
  if not self.startup_tasks:
@@ -161,9 +239,7 @@ class Dispatcher(Generic[ClientT]):
161
239
  if event_type is not None:
162
240
  logger.debug("dispatching event type=%s", event_type)
163
241
  event = self.mapper.map(event_type, frame)
164
- await self._dispatch_to_router(
165
- self.internal_router, event_type, event
166
- )
242
+ await self._dispatch_to_router(self.internal_router, event_type, event)
167
243
  await self._dispatch_to_router(self.root_router, event_type, event)
168
244
  else:
169
245
  logger.debug(
@@ -181,13 +257,18 @@ class Dispatcher(Generic[ClientT]):
181
257
  event: Any,
182
258
  ) -> None:
183
259
  for entry in router.handlers.get(event_type, []):
184
- if await self._matches(entry, event):
185
- logger.debug(
186
- "calling handler event=%s callback=%s",
187
- event_type,
188
- _callback_name(entry.callback),
189
- )
190
- await self._call(entry.callback, event)
260
+ try:
261
+ if await self._matches(entry, event):
262
+ logger.debug(
263
+ "calling handler event=%s callback=%s",
264
+ event_type,
265
+ _callback_name(entry.callback),
266
+ )
267
+ await self._call(entry.callback, event)
268
+ except Exception as e: # noqa: PERF203
269
+ handled = await self.emit_error(e, event_type, event, router, entry)
270
+ if not handled:
271
+ raise
191
272
 
192
273
  for child in router.children:
193
274
  await self._dispatch_to_router(child, event_type, event)
@@ -209,9 +290,7 @@ class Dispatcher(Generic[ClientT]):
209
290
  return False
210
291
  return True
211
292
 
212
- async def _call(
213
- self, callback: HandlerCallback[Any, ClientT], event: Any
214
- ) -> Any:
293
+ async def _call(self, callback: HandlerCallback[Any, ClientT], event: Any) -> Any:
215
294
  if self.client is None:
216
295
  raise RuntimeError("client is not bound")
217
296
 
@@ -222,6 +301,63 @@ class Dispatcher(Generic[ClientT]):
222
301
 
223
302
  return result
224
303
 
304
+ async def emit_error(
305
+ self,
306
+ exception: Exception,
307
+ event_type: EventType,
308
+ event: Any,
309
+ router: Router[ClientT],
310
+ handler: ErrorSource[ClientT] | None,
311
+ ) -> bool:
312
+ client = self.client
313
+ handled = False
314
+
315
+ if client is None:
316
+ raise RuntimeError("client is not bound to dispatcher")
317
+
318
+ ctx = ErrorContext[ClientT](
319
+ client=client,
320
+ event_type=event_type,
321
+ event=event,
322
+ router=router,
323
+ handler=handler,
324
+ )
325
+ for entry in self.iter_error_handlers(router):
326
+ handled = True
327
+ try:
328
+ result = entry.callback(exception, ctx)
329
+
330
+ if inspect.isawaitable(result):
331
+ await result
332
+ except Exception as e:
333
+ logger.exception("Error while error handling: %s", e)
334
+ return False
335
+
336
+ return handled
337
+
338
+ async def emit_disconnect(
339
+ self,
340
+ exception: Exception,
341
+ reconnect: bool,
342
+ delay: float,
343
+ ) -> None:
344
+ """Вызывает обработчики потери соединения.
345
+
346
+ Ошибки внутри disconnect-handler-ов логируются и не прерывают reconnect.
347
+ """
348
+
349
+ if self.client is None:
350
+ raise RuntimeError("client is not bound to dispatcher")
351
+
352
+ for handler in self.iter_disconnect_handlers():
353
+ try:
354
+ result = handler(exception, reconnect, delay)
355
+
356
+ if inspect.isawaitable(result):
357
+ await result
358
+ except Exception as e:
359
+ logger.exception("Error during disconnect handling: %s", e)
360
+
225
361
 
226
362
  def _callback_name(callback: Any) -> str:
227
363
  return getattr(
pymax/dispatch/enums.py CHANGED
@@ -5,8 +5,13 @@ class EventType(str, Enum):
5
5
  MESSAGE_NEW = "message_new"
6
6
  MESSAGE_EDIT = "message_edit"
7
7
  MESSAGE_DELETE = "message_delete"
8
+ MESSAGE_READ = "message_read"
9
+ TYPING = "typing"
10
+ PRESENCE = "presence"
11
+ REACTION_UPDATE = "reaction_update"
8
12
  CHAT_UPDATE = "chat_update"
9
13
  USER_UPDATE = "user_update"
10
14
  VIDEO_READY = "video_ready"
11
15
  FILE_READY = "file_ready"
12
16
  RAW = "raw"
17
+ ON_START = "on_start"
pymax/dispatch/mapping.py CHANGED
@@ -3,11 +3,19 @@ from __future__ import annotations
3
3
  from collections.abc import Callable
4
4
  from typing import TYPE_CHECKING
5
5
 
6
+ from pymax.api.binding import bind_api_model
6
7
  from pymax.protocol import InboundFrame, Opcode
7
8
  from pymax.protocol.enums import Command
8
9
  from pymax.types import Chat, MessageDeleteEvent
9
10
  from pymax.types.domain import Message
10
- from pymax.types.events import FileUploadSignal, VideoUploadSignal
11
+ from pymax.types.events import (
12
+ FileUploadSignal,
13
+ MessageReadEvent,
14
+ PresenceEvent,
15
+ ReactionUpdateEvent,
16
+ TypingEvent,
17
+ VideoUploadSignal,
18
+ )
11
19
 
12
20
  from .enums import EventType
13
21
  from .resolvers import (
@@ -15,6 +23,10 @@ from .resolvers import (
15
23
  resolve_chat,
16
24
  resolve_message,
17
25
  resolve_message_delete,
26
+ resolve_message_read,
27
+ resolve_presence,
28
+ resolve_reaction_update,
29
+ resolve_typing,
18
30
  )
19
31
 
20
32
  if TYPE_CHECKING:
@@ -28,6 +40,10 @@ EVENT_MAP: dict[Opcode, Resolver] = {
28
40
  Opcode.NOTIF_CHAT: resolve_chat,
29
41
  Opcode.NOTIF_MSG_DELETE: resolve_message_delete,
30
42
  Opcode.NOTIF_ATTACH: resolve_attach,
43
+ Opcode.NOTIF_TYPING: resolve_typing,
44
+ Opcode.NOTIF_MARK: resolve_message_read,
45
+ Opcode.NOTIF_PRESENCE: resolve_presence,
46
+ Opcode.NOTIF_MSG_REACTIONS_CHANGED: resolve_reaction_update,
31
47
  }
32
48
 
33
49
 
@@ -58,21 +74,28 @@ class EventMapper:
58
74
 
59
75
  if frame.payload:
60
76
  if event_type in (EventType.MESSAGE_NEW, EventType.MESSAGE_EDIT):
61
- return Message.model_validate(frame.payload).bind(
62
- self.app.api.messages
77
+ return bind_api_model(
78
+ self.app,
79
+ Message.model_validate(frame.payload),
63
80
  )
64
81
  elif event_type == EventType.CHAT_UPDATE:
65
- return Chat.model_validate(frame.payload["chat"]).bind(
66
- self.app.api.messages,
67
- self.app.api.chats,
82
+ return bind_api_model(
83
+ self.app,
84
+ Chat.model_validate(frame.payload["chat"]),
68
85
  )
69
86
  elif event_type == EventType.MESSAGE_DELETE:
70
- model = MessageDeleteEvent.model_validate(frame.payload)
71
- model.chat.bind(
72
- self.app.api.messages,
73
- self.app.api.chats,
87
+ return bind_api_model(
88
+ self.app,
89
+ MessageDeleteEvent.model_validate(frame.payload),
74
90
  )
75
- return model
91
+ elif event_type == EventType.MESSAGE_READ:
92
+ return MessageReadEvent.model_validate(frame.payload)
93
+ elif event_type == EventType.TYPING:
94
+ return TypingEvent.model_validate(frame.payload)
95
+ elif event_type == EventType.PRESENCE:
96
+ return PresenceEvent.model_validate(frame.payload)
97
+ elif event_type == EventType.REACTION_UPDATE:
98
+ return ReactionUpdateEvent.model_validate(frame.payload)
76
99
  elif event_type == EventType.VIDEO_READY:
77
100
  return VideoUploadSignal.model_validate(frame.payload)
78
101
  elif event_type == EventType.FILE_READY:
@@ -20,6 +20,22 @@ def resolve_message_delete(_: InboundFrame) -> EventType | None:
20
20
  return EventType.MESSAGE_DELETE
21
21
 
22
22
 
23
+ def resolve_message_read(_: InboundFrame) -> EventType | None:
24
+ return EventType.MESSAGE_READ
25
+
26
+
27
+ def resolve_typing(_: InboundFrame) -> EventType | None:
28
+ return EventType.TYPING
29
+
30
+
31
+ def resolve_presence(_: InboundFrame) -> EventType | None:
32
+ return EventType.PRESENCE
33
+
34
+
35
+ def resolve_reaction_update(_: InboundFrame) -> EventType | None:
36
+ return EventType.REACTION_UPDATE
37
+
38
+
23
39
  def resolve_attach(frame: InboundFrame) -> EventType | None:
24
40
  try:
25
41
  FileUploadSignal.model_validate(frame.payload)
@@ -45,6 +61,8 @@ def resolve_message(frame: InboundFrame) -> EventType | None:
45
61
 
46
62
  if model.status == MessageStatus.EDITED:
47
63
  return EventType.MESSAGE_EDIT
64
+ if model.status == MessageStatus.REMOVED:
65
+ return EventType.MESSAGE_DELETE
48
66
  else:
49
67
  return EventType.MESSAGE_NEW
50
68
  except ValidationError: