simplex-chat 6.5.1__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.
simplex_chat/api.py ADDED
@@ -0,0 +1,704 @@
1
+ """Low-level escape-hatch API. Most users go through `Bot` instead."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from typing import Any, Literal
8
+
9
+ from . import _native, core, util
10
+ from .core import MigrationConfirmation
11
+ from .types import CC, CEvt, CR, T
12
+
13
+ # Mirrors Node `ConnReqType` enum (api.ts:15-18) — the two possible outcomes
14
+ # of `api_connect` / `api_connect_active_user` depending on the link kind.
15
+ ConnReqType = Literal["invitation", "contact"]
16
+
17
+
18
+ @dataclass(slots=True)
19
+ class SqliteDb:
20
+ file_prefix: str
21
+ encryption_key: str | None = None
22
+
23
+
24
+ @dataclass(slots=True)
25
+ class PostgresDb:
26
+ connection_string: str
27
+ schema_prefix: str | None = None
28
+
29
+
30
+ Db = SqliteDb | PostgresDb
31
+
32
+
33
+ def _db_to_migrate_args(db: Db) -> tuple[str, str, _native.Backend]:
34
+ """Returns (path-or-prefix, key-or-conn, backend)."""
35
+ if isinstance(db, SqliteDb):
36
+ return (db.file_prefix, db.encryption_key or "", "sqlite")
37
+ if isinstance(db, PostgresDb):
38
+ return (db.schema_prefix or "", db.connection_string, "postgres")
39
+ raise TypeError(f"Unknown db: {db!r}")
40
+
41
+
42
+ class ChatCommandError(Exception):
43
+ def __init__(self, message: str, response: CR.ChatResponse):
44
+ super().__init__(message)
45
+ self.response = response
46
+
47
+
48
+ class ChatApi:
49
+ def __init__(self, ctrl: int):
50
+ self._ctrl: int | None = ctrl
51
+ self._started = False
52
+
53
+ @classmethod
54
+ async def init(
55
+ cls,
56
+ db: Db,
57
+ confirm: MigrationConfirmation = MigrationConfirmation.YES_UP,
58
+ ) -> "ChatApi":
59
+ path_or_prefix, key_or_conn, backend = _db_to_migrate_args(db)
60
+ # Trigger lazy lib load with the right backend BEFORE chat_migrate_init.
61
+ _native.lib_for(backend)
62
+ ctrl = await core.chat_migrate_init(path_or_prefix, key_or_conn, confirm)
63
+ return cls(ctrl)
64
+
65
+ @property
66
+ def ctrl(self) -> int:
67
+ """Opaque controller pointer. Raises if `close()` has been called."""
68
+ if self._ctrl is None:
69
+ raise RuntimeError("ChatApi controller not initialized (close() called?)")
70
+ return self._ctrl
71
+
72
+ @property
73
+ def initialized(self) -> bool:
74
+ """True until `close()` is called. Mirrors Node `ChatApi.initialized`."""
75
+ return self._ctrl is not None
76
+
77
+ @property
78
+ def started(self) -> bool:
79
+ """True between `start_chat()` and the next `stop_chat()` / `close()`."""
80
+ return self._started
81
+
82
+ async def start_chat(self) -> None:
83
+ r = await self.send_chat_cmd(
84
+ CC.StartChat_cmd_string({"mainApp": True, "enableSndFiles": True})
85
+ )
86
+ if r.get("type") not in ("chatStarted", "chatRunning"):
87
+ raise ChatCommandError("error starting chat", r)
88
+ self._started = True
89
+
90
+ async def stop_chat(self) -> None:
91
+ r = await self.send_chat_cmd("/_stop")
92
+ if r.get("type") != "chatStopped":
93
+ raise ChatCommandError("error stopping chat", r)
94
+ self._started = False
95
+
96
+ async def close(self) -> None:
97
+ await core.chat_close_store(self.ctrl)
98
+ self._ctrl = None
99
+ self._started = False
100
+
101
+ async def send_chat_cmd(self, cmd: str) -> CR.ChatResponse:
102
+ return await core.chat_send_cmd(self.ctrl, cmd)
103
+
104
+ async def recv_chat_event(self, wait_us: int = 500_000) -> CEvt.ChatEvent | None:
105
+ return await core.chat_recv_msg_wait(self.ctrl, wait_us)
106
+
107
+ # ------------------------------------------------------------------ #
108
+ # Address commands
109
+ # ------------------------------------------------------------------ #
110
+
111
+ async def api_create_user_address(self, user_id: int) -> T.CreatedConnLink:
112
+ r = await self.send_chat_cmd(CC.APICreateMyAddress_cmd_string({"userId": user_id}))
113
+ if r["type"] == "userContactLinkCreated":
114
+ return r["connLinkContact"]
115
+ raise ChatCommandError("error creating user address", r)
116
+
117
+ async def api_delete_user_address(self, user_id: int) -> None:
118
+ r = await self.send_chat_cmd(CC.APIDeleteMyAddress_cmd_string({"userId": user_id}))
119
+ if r["type"] != "userContactLinkDeleted":
120
+ raise ChatCommandError("error deleting user address", r)
121
+
122
+ async def api_get_user_address(self, user_id: int) -> T.UserContactLink | None:
123
+ try:
124
+ r = await self.send_chat_cmd(CC.APIShowMyAddress_cmd_string({"userId": user_id}))
125
+ if r["type"] == "userContactLink":
126
+ return r["contactLink"]
127
+ raise ChatCommandError("error loading user address", r)
128
+ except core.ChatAPIError as e:
129
+ ce = e.chat_error
130
+ if (
131
+ ce is not None
132
+ and ce.get("type") == "errorStore"
133
+ and ce.get("storeError", {}).get("type") == "userContactLinkNotFound"
134
+ ):
135
+ return None
136
+ raise
137
+
138
+ async def api_set_profile_address(
139
+ self, user_id: int, enable: bool
140
+ ) -> T.UserProfileUpdateSummary:
141
+ r = await self.send_chat_cmd(
142
+ CC.APISetProfileAddress_cmd_string({"userId": user_id, "enable": enable})
143
+ )
144
+ if r["type"] == "userProfileUpdated":
145
+ return r["updateSummary"]
146
+ raise ChatCommandError("error setting profile address", r)
147
+
148
+ async def api_set_address_settings(self, user_id: int, settings: T.AddressSettings) -> None:
149
+ r = await self.send_chat_cmd(
150
+ CC.APISetAddressSettings_cmd_string({"userId": user_id, "settings": settings})
151
+ )
152
+ if r["type"] != "userContactLinkUpdated":
153
+ raise ChatCommandError("error changing user contact address settings", r)
154
+
155
+ # ------------------------------------------------------------------ #
156
+ # Message commands
157
+ # ------------------------------------------------------------------ #
158
+
159
+ async def api_send_messages(
160
+ self,
161
+ chat: list | T.ChatRef | T.ChatInfo,
162
+ messages: list[T.ComposedMessage],
163
+ live_message: bool = False,
164
+ ) -> list[T.AChatItem]:
165
+ if isinstance(chat, list):
166
+ send_ref: T.ChatRef = {"chatType": chat[0], "chatId": chat[1]}
167
+ elif "chatType" in chat and "chatId" in chat:
168
+ send_ref = chat
169
+ else:
170
+ ref = util.chat_info_ref(chat)
171
+ if ref is None:
172
+ raise ValueError("api_send_messages: can't send messages to this chat")
173
+ send_ref = ref
174
+ r = await self.send_chat_cmd(
175
+ CC.APISendMessages_cmd_string(
176
+ {
177
+ "sendRef": send_ref,
178
+ "composedMessages": messages,
179
+ "liveMessage": live_message,
180
+ }
181
+ )
182
+ )
183
+ if r["type"] == "newChatItems":
184
+ return r["chatItems"]
185
+ raise ChatCommandError("unexpected response", r)
186
+
187
+ async def api_send_text_message(
188
+ self,
189
+ chat: list | T.ChatRef | T.ChatInfo,
190
+ text: str,
191
+ in_reply_to: int | None = None,
192
+ ) -> list[T.AChatItem]:
193
+ msg: T.ComposedMessage = {"msgContent": {"type": "text", "text": text}, "mentions": {}}
194
+ if in_reply_to is not None:
195
+ msg["quotedItemId"] = in_reply_to
196
+ return await self.api_send_messages(chat, [msg])
197
+
198
+ async def api_send_text_reply(self, chat_item: T.AChatItem, text: str) -> list[T.AChatItem]:
199
+ return await self.api_send_text_message(
200
+ chat_item["chatInfo"], text, chat_item["chatItem"]["meta"]["itemId"]
201
+ )
202
+
203
+ async def api_update_chat_item(
204
+ self,
205
+ chat_type: T.ChatType,
206
+ chat_id: int,
207
+ chat_item_id: int,
208
+ msg_content: T.MsgContent,
209
+ live_message: bool = False,
210
+ ) -> T.ChatItem:
211
+ r = await self.send_chat_cmd(
212
+ CC.APIUpdateChatItem_cmd_string(
213
+ {
214
+ "chatRef": {"chatType": chat_type, "chatId": chat_id},
215
+ "chatItemId": chat_item_id,
216
+ "liveMessage": live_message,
217
+ "updatedMessage": {"msgContent": msg_content, "mentions": {}},
218
+ }
219
+ )
220
+ )
221
+ if r["type"] == "chatItemUpdated":
222
+ return r["chatItem"]["chatItem"]
223
+ raise ChatCommandError("error updating chat item", r)
224
+
225
+ async def api_delete_chat_items(
226
+ self,
227
+ chat_type: T.ChatType,
228
+ chat_id: int,
229
+ chat_item_ids: list[int],
230
+ delete_mode: T.CIDeleteMode,
231
+ ) -> list[T.ChatItemDeletion]:
232
+ r = await self.send_chat_cmd(
233
+ CC.APIDeleteChatItem_cmd_string(
234
+ {
235
+ "chatRef": {"chatType": chat_type, "chatId": chat_id},
236
+ "chatItemIds": chat_item_ids,
237
+ "deleteMode": delete_mode,
238
+ }
239
+ )
240
+ )
241
+ if r["type"] == "chatItemsDeleted":
242
+ return r["chatItemDeletions"]
243
+ raise ChatCommandError("error deleting chat item", r)
244
+
245
+ async def api_delete_member_chat_item(
246
+ self, group_id: int, chat_item_ids: list[int]
247
+ ) -> list[T.ChatItemDeletion]:
248
+ r = await self.send_chat_cmd(
249
+ CC.APIDeleteMemberChatItem_cmd_string(
250
+ {"groupId": group_id, "chatItemIds": chat_item_ids}
251
+ )
252
+ )
253
+ if r["type"] == "chatItemsDeleted":
254
+ return r["chatItemDeletions"]
255
+ raise ChatCommandError("error deleting member chat item", r)
256
+
257
+ async def api_chat_item_reaction(
258
+ self,
259
+ chat_type: T.ChatType,
260
+ chat_id: int,
261
+ chat_item_id: int,
262
+ add: bool,
263
+ reaction: T.MsgReaction,
264
+ ) -> T.ACIReaction:
265
+ r = await self.send_chat_cmd(
266
+ CC.APIChatItemReaction_cmd_string(
267
+ {
268
+ "chatRef": {"chatType": chat_type, "chatId": chat_id},
269
+ "chatItemId": chat_item_id,
270
+ "add": add,
271
+ "reaction": reaction,
272
+ }
273
+ )
274
+ )
275
+ if r["type"] == "chatItemReaction":
276
+ return r["reaction"]
277
+ raise ChatCommandError("error setting item reaction", r)
278
+
279
+ # ------------------------------------------------------------------ #
280
+ # File commands
281
+ # ------------------------------------------------------------------ #
282
+
283
+ async def api_receive_file(self, file_id: int) -> T.AChatItem:
284
+ r = await self.send_chat_cmd(
285
+ CC.ReceiveFile_cmd_string({"fileId": file_id, "userApprovedRelays": True})
286
+ )
287
+ if r["type"] == "rcvFileAccepted":
288
+ return r["chatItem"]
289
+ raise ChatCommandError("error receiving file", r)
290
+
291
+ async def api_cancel_file(self, file_id: int) -> None:
292
+ r = await self.send_chat_cmd(CC.CancelFile_cmd_string({"fileId": file_id}))
293
+ if r["type"] not in ("sndFileCancelled", "rcvFileCancelled"):
294
+ raise ChatCommandError("error canceling file", r)
295
+
296
+ # ------------------------------------------------------------------ #
297
+ # Group commands
298
+ # ------------------------------------------------------------------ #
299
+
300
+ async def api_add_member(
301
+ self, group_id: int, contact_id: int, member_role: T.GroupMemberRole
302
+ ) -> T.GroupMember:
303
+ r = await self.send_chat_cmd(
304
+ CC.APIAddMember_cmd_string(
305
+ {"groupId": group_id, "contactId": contact_id, "memberRole": member_role}
306
+ )
307
+ )
308
+ if r["type"] == "sentGroupInvitation":
309
+ return r["member"]
310
+ raise ChatCommandError("error adding member", r)
311
+
312
+ async def api_join_group(self, group_id: int) -> T.GroupInfo:
313
+ r = await self.send_chat_cmd(CC.APIJoinGroup_cmd_string({"groupId": group_id}))
314
+ if r["type"] == "userAcceptedGroupSent":
315
+ return r["groupInfo"]
316
+ raise ChatCommandError("error joining group", r)
317
+
318
+ async def api_accept_member(
319
+ self, group_id: int, group_member_id: int, member_role: T.GroupMemberRole
320
+ ) -> T.GroupMember:
321
+ r = await self.send_chat_cmd(
322
+ CC.APIAcceptMember_cmd_string(
323
+ {"groupId": group_id, "groupMemberId": group_member_id, "memberRole": member_role}
324
+ )
325
+ )
326
+ if r["type"] == "memberAccepted":
327
+ return r["member"]
328
+ raise ChatCommandError("error accepting member", r)
329
+
330
+ async def api_set_members_role(
331
+ self, group_id: int, group_member_ids: list[int], member_role: T.GroupMemberRole
332
+ ) -> None:
333
+ r = await self.send_chat_cmd(
334
+ CC.APIMembersRole_cmd_string(
335
+ {"groupId": group_id, "groupMemberIds": group_member_ids, "memberRole": member_role}
336
+ )
337
+ )
338
+ if r["type"] != "membersRoleUser":
339
+ raise ChatCommandError("error setting members role", r)
340
+
341
+ async def api_block_members_for_all(
342
+ self, group_id: int, group_member_ids: list[int], blocked: bool
343
+ ) -> None:
344
+ r = await self.send_chat_cmd(
345
+ CC.APIBlockMembersForAll_cmd_string(
346
+ {"groupId": group_id, "groupMemberIds": group_member_ids, "blocked": blocked}
347
+ )
348
+ )
349
+ if r["type"] != "membersBlockedForAllUser":
350
+ raise ChatCommandError("error blocking members", r)
351
+
352
+ async def api_remove_members(
353
+ self, group_id: int, member_ids: list[int], with_messages: bool = False
354
+ ) -> list[T.GroupMember]:
355
+ r = await self.send_chat_cmd(
356
+ CC.APIRemoveMembers_cmd_string(
357
+ {"groupId": group_id, "groupMemberIds": member_ids, "withMessages": with_messages}
358
+ )
359
+ )
360
+ if r["type"] == "userDeletedMembers":
361
+ return r["members"]
362
+ raise ChatCommandError("error removing member", r)
363
+
364
+ async def api_leave_group(self, group_id: int) -> T.GroupInfo:
365
+ r = await self.send_chat_cmd(CC.APILeaveGroup_cmd_string({"groupId": group_id}))
366
+ if r["type"] == "leftMemberUser":
367
+ return r["groupInfo"]
368
+ raise ChatCommandError("error leaving group", r)
369
+
370
+ async def api_list_members(self, group_id: int) -> list[T.GroupMember]:
371
+ r = await self.send_chat_cmd(CC.APIListMembers_cmd_string({"groupId": group_id}))
372
+ if r["type"] == "groupMembers":
373
+ return r["group"]["members"]
374
+ raise ChatCommandError("error getting group members", r)
375
+
376
+ async def api_new_group(self, user_id: int, group_profile: T.GroupProfile) -> T.GroupInfo:
377
+ r = await self.send_chat_cmd(
378
+ CC.APINewGroup_cmd_string(
379
+ {"userId": user_id, "groupProfile": group_profile, "incognito": False}
380
+ )
381
+ )
382
+ if r["type"] == "groupCreated":
383
+ return r["groupInfo"]
384
+ raise ChatCommandError("error creating group", r)
385
+
386
+ async def api_update_group_profile(
387
+ self, group_id: int, group_profile: T.GroupProfile
388
+ ) -> T.GroupInfo:
389
+ r = await self.send_chat_cmd(
390
+ CC.APIUpdateGroupProfile_cmd_string(
391
+ {"groupId": group_id, "groupProfile": group_profile}
392
+ )
393
+ )
394
+ if r["type"] == "groupUpdated":
395
+ return r["toGroup"]
396
+ raise ChatCommandError("error updating group", r)
397
+
398
+ # ------------------------------------------------------------------ #
399
+ # Group link commands
400
+ # ------------------------------------------------------------------ #
401
+
402
+ async def api_create_group_link(self, group_id: int, member_role: T.GroupMemberRole) -> str:
403
+ r = await self.send_chat_cmd(
404
+ CC.APICreateGroupLink_cmd_string({"groupId": group_id, "memberRole": member_role})
405
+ )
406
+ if r["type"] == "groupLinkCreated":
407
+ link = r["groupLink"]["connLinkContact"]
408
+ return link.get("connShortLink") or link["connFullLink"]
409
+ raise ChatCommandError("error creating group link", r)
410
+
411
+ async def api_set_group_link_member_role(
412
+ self, group_id: int, member_role: T.GroupMemberRole
413
+ ) -> None:
414
+ r = await self.send_chat_cmd(
415
+ CC.APIGroupLinkMemberRole_cmd_string({"groupId": group_id, "memberRole": member_role})
416
+ )
417
+ if r["type"] != "groupLink":
418
+ raise ChatCommandError("error setting group link member role", r)
419
+
420
+ async def api_delete_group_link(self, group_id: int) -> None:
421
+ r = await self.send_chat_cmd(CC.APIDeleteGroupLink_cmd_string({"groupId": group_id}))
422
+ if r["type"] != "groupLinkDeleted":
423
+ raise ChatCommandError("error deleting group link", r)
424
+
425
+ async def api_get_group_link(self, group_id: int) -> T.GroupLink:
426
+ r = await self.send_chat_cmd(CC.APIGetGroupLink_cmd_string({"groupId": group_id}))
427
+ if r["type"] == "groupLink":
428
+ return r["groupLink"]
429
+ raise ChatCommandError("error getting group link", r)
430
+
431
+ async def api_get_group_link_str(self, group_id: int) -> str:
432
+ link = (await self.api_get_group_link(group_id))["connLinkContact"]
433
+ return link.get("connShortLink") or link["connFullLink"]
434
+
435
+ # ------------------------------------------------------------------ #
436
+ # Connection commands
437
+ # ------------------------------------------------------------------ #
438
+
439
+ async def api_create_link(self, user_id: int) -> str:
440
+ r = await self.send_chat_cmd(
441
+ CC.APIAddContact_cmd_string({"userId": user_id, "incognito": False})
442
+ )
443
+ if r["type"] == "invitation":
444
+ link = r["connLinkInvitation"]
445
+ return link.get("connShortLink") or link["connFullLink"]
446
+ raise ChatCommandError("error creating link", r)
447
+
448
+ async def api_connect_plan(
449
+ self, user_id: int, connection_link: str
450
+ ) -> tuple[T.ConnectionPlan, T.CreatedConnLink]:
451
+ r = await self.send_chat_cmd(
452
+ CC.APIConnectPlan_cmd_string(
453
+ {"userId": user_id, "connectionLink": connection_link, "resolveKnown": False}
454
+ )
455
+ )
456
+ if r["type"] == "connectionPlan":
457
+ return (r["connectionPlan"], r["connLink"])
458
+ raise ChatCommandError("error getting connect plan", r)
459
+
460
+ async def api_connect(
461
+ self,
462
+ user_id: int,
463
+ incognito: bool,
464
+ prepared_link: T.CreatedConnLink | None = None,
465
+ ) -> ConnReqType:
466
+ args: CC.APIConnect = {"userId": user_id, "incognito": incognito}
467
+ if prepared_link is not None:
468
+ args["preparedLink_"] = prepared_link
469
+ r = await self.send_chat_cmd(CC.APIConnect_cmd_string(args))
470
+ return self._handle_connect_result(r)
471
+
472
+ async def api_connect_active_user(self, conn_link: str) -> ConnReqType:
473
+ r = await self.send_chat_cmd(
474
+ CC.Connect_cmd_string({"incognito": False, "connLink_": conn_link})
475
+ )
476
+ return self._handle_connect_result(r)
477
+
478
+ def _handle_connect_result(self, r: CR.ChatResponse) -> ConnReqType:
479
+ if r["type"] == "sentConfirmation":
480
+ return "invitation"
481
+ if r["type"] == "sentInvitation":
482
+ return "contact"
483
+ if r["type"] == "contactAlreadyExists":
484
+ raise ChatCommandError("contact already exists", r)
485
+ raise ChatCommandError("connection error", r)
486
+
487
+ async def api_accept_contact_request(self, contact_req_id: int) -> T.Contact:
488
+ r = await self.send_chat_cmd(
489
+ CC.APIAcceptContact_cmd_string({"contactReqId": contact_req_id})
490
+ )
491
+ if r["type"] == "acceptingContactRequest":
492
+ return r["contact"]
493
+ raise ChatCommandError("error accepting contact request", r)
494
+
495
+ async def api_reject_contact_request(self, contact_req_id: int) -> None:
496
+ r = await self.send_chat_cmd(
497
+ CC.APIRejectContact_cmd_string({"contactReqId": contact_req_id})
498
+ )
499
+ if r["type"] != "contactRequestRejected":
500
+ raise ChatCommandError("error rejecting contact request", r)
501
+
502
+ # ------------------------------------------------------------------ #
503
+ # Chat commands
504
+ # ------------------------------------------------------------------ #
505
+
506
+ async def api_list_contacts(self, user_id: int) -> list[T.Contact]:
507
+ r = await self.send_chat_cmd(CC.APIListContacts_cmd_string({"userId": user_id}))
508
+ if r["type"] == "contactsList":
509
+ return r["contacts"]
510
+ raise ChatCommandError("error listing contacts", r)
511
+
512
+ async def api_list_groups(
513
+ self,
514
+ user_id: int,
515
+ contact_id: int | None = None,
516
+ search: str | None = None,
517
+ ) -> list[T.GroupInfo]:
518
+ args: CC.APIListGroups = {"userId": user_id}
519
+ if contact_id is not None:
520
+ args["contactId_"] = contact_id
521
+ if search is not None:
522
+ args["search"] = search
523
+ r = await self.send_chat_cmd(CC.APIListGroups_cmd_string(args))
524
+ if r["type"] == "groupsList":
525
+ return r["groups"]
526
+ raise ChatCommandError("error listing groups", r)
527
+
528
+ async def api_get_chats(
529
+ self,
530
+ user_id: int,
531
+ pagination: T.PaginationByTime,
532
+ query: T.ChatListQuery | None = None,
533
+ pending_connections: bool = False,
534
+ ) -> list[T.AChat]:
535
+ if query is None:
536
+ query = {"type": "filters", "favorite": False, "unread": False}
537
+ r = await self.send_chat_cmd(
538
+ CC.APIGetChats_cmd_string(
539
+ {
540
+ "userId": user_id,
541
+ "pendingConnections": pending_connections,
542
+ "pagination": pagination,
543
+ "query": query,
544
+ }
545
+ )
546
+ )
547
+ if r["type"] == "apiChats":
548
+ return r["chats"]
549
+ raise ChatCommandError("error getting chats", r)
550
+
551
+ async def api_delete_chat(
552
+ self,
553
+ chat_type: T.ChatType,
554
+ chat_id: int,
555
+ delete_mode: T.ChatDeleteMode | None = None,
556
+ ) -> None:
557
+ if delete_mode is None:
558
+ delete_mode = {"type": "full", "notify": True}
559
+ r = await self.send_chat_cmd(
560
+ CC.APIDeleteChat_cmd_string(
561
+ {
562
+ "chatRef": {"chatType": chat_type, "chatId": chat_id},
563
+ "chatDeleteMode": delete_mode,
564
+ }
565
+ )
566
+ )
567
+ if chat_type == "direct" and r["type"] == "contactDeleted":
568
+ return
569
+ if chat_type == "group" and r["type"] == "groupDeletedUser":
570
+ return
571
+ raise ChatCommandError("error deleting chat", r)
572
+
573
+ async def api_set_group_custom_data(
574
+ self, group_id: int, custom_data: dict[str, object] | None = None
575
+ ) -> None:
576
+ args: CC.APISetGroupCustomData = {"groupId": group_id}
577
+ if custom_data is not None:
578
+ args["customData"] = custom_data
579
+ r = await self.send_chat_cmd(CC.APISetGroupCustomData_cmd_string(args))
580
+ if r["type"] != "cmdOk":
581
+ raise ChatCommandError("error setting group custom data", r)
582
+
583
+ async def api_set_contact_custom_data(
584
+ self, contact_id: int, custom_data: dict[str, object] | None = None
585
+ ) -> None:
586
+ args: CC.APISetContactCustomData = {"contactId": contact_id}
587
+ if custom_data is not None:
588
+ args["customData"] = custom_data
589
+ r = await self.send_chat_cmd(CC.APISetContactCustomData_cmd_string(args))
590
+ if r["type"] != "cmdOk":
591
+ raise ChatCommandError("error setting contact custom data", r)
592
+
593
+ async def api_set_auto_accept_member_contacts(self, user_id: int, on_off: bool) -> None:
594
+ r = await self.send_chat_cmd(
595
+ CC.APISetUserAutoAcceptMemberContacts_cmd_string({"userId": user_id, "onOff": on_off})
596
+ )
597
+ if r["type"] != "cmdOk":
598
+ raise ChatCommandError("error setting auto-accept member contacts", r)
599
+
600
+ async def api_get_chat(self, chat_type: T.ChatType, chat_id: int, count: int) -> dict[str, Any]:
601
+ ref = T.ChatType_cmd_string(chat_type) + str(chat_id)
602
+ r = await self.send_chat_cmd(f"/_get chat {ref} count={count}")
603
+ if r["type"] == "apiChat":
604
+ return r["chat"]
605
+ raise ChatCommandError("error getting chat", r)
606
+
607
+ # ------------------------------------------------------------------ #
608
+ # User profile commands
609
+ # ------------------------------------------------------------------ #
610
+
611
+ async def api_get_active_user(self) -> T.User | None:
612
+ try:
613
+ r = await self.send_chat_cmd(CC.ShowActiveUser_cmd_string({}))
614
+ if r["type"] == "activeUser":
615
+ return r["user"]
616
+ raise ChatCommandError("unexpected response", r)
617
+ except core.ChatAPIError as e:
618
+ ce = e.chat_error
619
+ if (
620
+ ce is not None
621
+ and ce.get("type") == "error"
622
+ and ce.get("errorType", {}).get("type") == "noActiveUser"
623
+ ):
624
+ return None
625
+ raise
626
+
627
+ async def api_create_active_user(self, profile: T.Profile | None = None) -> T.User:
628
+ new_user: T.NewUser = {"pastTimestamp": False, "userChatRelay": False}
629
+ if profile is not None:
630
+ new_user["profile"] = profile
631
+ r = await self.send_chat_cmd(CC.CreateActiveUser_cmd_string({"newUser": new_user}))
632
+ if r["type"] == "activeUser":
633
+ return r["user"]
634
+ raise ChatCommandError("unexpected response", r)
635
+
636
+ async def api_list_users(self) -> list[T.UserInfo]:
637
+ r = await self.send_chat_cmd(CC.ListUsers_cmd_string({}))
638
+ if r["type"] == "usersList":
639
+ return r["users"]
640
+ raise ChatCommandError("error listing users", r)
641
+
642
+ async def api_set_active_user(self, user_id: int, view_pwd: str | None = None) -> T.User:
643
+ args: CC.APISetActiveUser = {"userId": user_id}
644
+ if view_pwd is not None:
645
+ args["viewPwd"] = view_pwd
646
+ r = await self.send_chat_cmd(CC.APISetActiveUser_cmd_string(args))
647
+ if r["type"] == "activeUser":
648
+ return r["user"]
649
+ raise ChatCommandError("error setting active user", r)
650
+
651
+ async def api_delete_user(
652
+ self, user_id: int, del_smp_queues: bool, view_pwd: str | None = None
653
+ ) -> None:
654
+ args: CC.APIDeleteUser = {"userId": user_id, "delSMPQueues": del_smp_queues}
655
+ if view_pwd is not None:
656
+ args["viewPwd"] = view_pwd
657
+ r = await self.send_chat_cmd(CC.APIDeleteUser_cmd_string(args))
658
+ if r["type"] != "cmdOk":
659
+ raise ChatCommandError("error deleting user", r)
660
+
661
+ async def api_update_profile(
662
+ self, user_id: int, profile: T.Profile
663
+ ) -> T.UserProfileUpdateSummary | None:
664
+ r = await self.send_chat_cmd(
665
+ CC.APIUpdateProfile_cmd_string({"userId": user_id, "profile": profile})
666
+ )
667
+ if r["type"] == "userProfileNoChange":
668
+ return None
669
+ if r["type"] == "userProfileUpdated":
670
+ return r["updateSummary"]
671
+ raise ChatCommandError("error updating profile", r)
672
+
673
+ async def api_set_contact_prefs(self, contact_id: int, preferences: T.Preferences) -> None:
674
+ r = await self.send_chat_cmd(
675
+ CC.APISetContactPrefs_cmd_string({"contactId": contact_id, "preferences": preferences})
676
+ )
677
+ if r["type"] != "contactPrefsUpdated":
678
+ raise ChatCommandError("error setting contact prefs", r)
679
+
680
+ # ------------------------------------------------------------------ #
681
+ # Member contact commands
682
+ # ------------------------------------------------------------------ #
683
+
684
+ async def api_create_member_contact(self, group_id: int, group_member_id: int) -> T.Contact:
685
+ r = await self.send_chat_cmd(f"/_create member contact #{group_id} {group_member_id}")
686
+ if r["type"] == "newMemberContact":
687
+ return r["contact"]
688
+ raise ChatCommandError("error creating member contact", r)
689
+
690
+ async def api_send_member_contact_invitation(
691
+ self,
692
+ contact_id: int,
693
+ message: T.MsgContent | str | None = None,
694
+ ) -> T.Contact:
695
+ cmd = f"/_invite member contact @{contact_id}"
696
+ if message is not None:
697
+ if isinstance(message, str):
698
+ cmd += f" text {message}"
699
+ else:
700
+ cmd += f" json {json.dumps(message)}"
701
+ r = await self.send_chat_cmd(cmd)
702
+ if r["type"] == "newMemberContactSentInv":
703
+ return r["contact"]
704
+ raise ChatCommandError("error sending member contact invitation", r)