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/__init__.py +59 -0
- simplex_chat/__main__.py +35 -0
- simplex_chat/_native.py +257 -0
- simplex_chat/_version.py +9 -0
- simplex_chat/api.py +704 -0
- simplex_chat/bot.py +707 -0
- simplex_chat/core.py +200 -0
- simplex_chat/filters.py +45 -0
- simplex_chat/py.typed +0 -0
- simplex_chat/types/__init__.py +16 -0
- simplex_chat/types/_commands.py +705 -0
- simplex_chat/types/_events.py +379 -0
- simplex_chat/types/_responses.py +360 -0
- simplex_chat/types/_types.py +3506 -0
- simplex_chat/util.py +128 -0
- simplex_chat-6.5.1.dist-info/METADATA +98 -0
- simplex_chat-6.5.1.dist-info/RECORD +19 -0
- simplex_chat-6.5.1.dist-info/WHEEL +4 -0
- simplex_chat-6.5.1.dist-info/licenses/LICENSE +661 -0
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)
|