emusks 2.0.18 → 2.1.1
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.
- package/README.md +1 -1
- package/package.json +19 -2
- package/src/cycletls.js +2 -1
- package/src/flow.js +69 -5
- package/src/graphql.js +5 -1
- package/src/helpers/index.js +4 -0
- package/src/helpers/jetfuel.js +175 -0
- package/src/helpers/juicebox/chunk-BNv3lrIs.js +1 -0
- package/src/helpers/juicebox/index.js +30 -0
- package/src/helpers/juicebox/juicebox-sdk_bg.wasm +0 -0
- package/src/helpers/juicebox/sdk.js +1 -0
- package/src/helpers/lists.js +6 -2
- package/src/helpers/tweets.js +3 -1
- package/src/helpers/xchat-call-media.js +127 -0
- package/src/helpers/xchat-calls.js +553 -0
- package/src/helpers/xchat-crypto.js +324 -0
- package/src/helpers/xchat-group-calls.js +340 -0
- package/src/helpers/xchat-juicebox.js +41 -0
- package/src/helpers/xchat-queries.js +3 -0
- package/src/helpers/xchat.js +794 -0
- package/src/index.js +2 -0
- package/src/instrumentation.js +124 -0
- package/src/jetfuel.js +92 -0
- package/src/parsers/jetfuel.js +226 -0
- package/src/v1.1.js +0 -11
- package/build/graphql.js +0 -19
- package/build/v1.1.js +0 -28
- package/build/v2.js +0 -28
- package/bun.lock +0 -93
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
import getCycleTLS from "../cycletls.js";
|
|
2
|
+
import {
|
|
3
|
+
b64,
|
|
4
|
+
b64decode,
|
|
5
|
+
buildActionSignature,
|
|
6
|
+
content,
|
|
7
|
+
conversationId1on1,
|
|
8
|
+
decryptBody,
|
|
9
|
+
deleteEventDetail,
|
|
10
|
+
eciesUnwrap,
|
|
11
|
+
eciesWrap,
|
|
12
|
+
encryptBody,
|
|
13
|
+
eventSignature,
|
|
14
|
+
generateKeyMaterial,
|
|
15
|
+
importIdentity,
|
|
16
|
+
messageCreateEvent,
|
|
17
|
+
parseThrift,
|
|
18
|
+
readLeadingSequenceId,
|
|
19
|
+
readStringField,
|
|
20
|
+
recoverIdentityKeys,
|
|
21
|
+
SCREEN_CAPTURE,
|
|
22
|
+
thriftStr,
|
|
23
|
+
} from "./xchat-crypto.js";
|
|
24
|
+
import { loadWebRTC, PeriscopeSession, subscribeIncomingCalls, XChatCall } from "./xchat-calls.js";
|
|
25
|
+
import { extractStreamName, GroupCall } from "./xchat-group-calls.js";
|
|
26
|
+
import { juiceboxRecover, juiceboxRegister } from "./xchat-juicebox.js";
|
|
27
|
+
import { GET_CONVERSATION_PAGE, GET_INITIAL_CHAT_PAGE } from "./xchat-queries.js";
|
|
28
|
+
|
|
29
|
+
const TOKEN_MAP_FRAGMENT = `fragment TokenMapFragment on KeyStoreTokenMap { __typename realm_state realm_state_string max_guess_count recover_threshold register_threshold token_map { __typename key value { __typename token address public_key } } key_store_token_map_json }`;
|
|
30
|
+
|
|
31
|
+
const OPS = {
|
|
32
|
+
GetPublicKeys: {
|
|
33
|
+
id: "GJQbOZALDO5D3Zp2IZhH6w",
|
|
34
|
+
doc: `query GetPublicKeys($ids: [NumericString!]!, $include_juicebox_tokens: Boolean = false ) { user_results_by_rest_ids(rest_ids: $ids, safety_level: XChat) { __typename ... on UserResults { rest_id id result { __typename ... on User { chat_permissions { __typename can_dm can_dm_on_xchat dm_blocking passes_premium_check } ...UserPublicKeysFragment } } } } } ${TOKEN_MAP_FRAGMENT} fragment UserPublicKeysFragment on User { __typename get_public_keys { __typename ... on GetPublicKeysResult { public_keys_with_token_map { __typename public_key_with_metadata { __typename public_key { __typename public_key signing_public_key identity_public_key_signature registration_method } version } token_map @include(if: $include_juicebox_tokens) { __typename ...TokenMapFragment } target_token_map @include(if: $include_juicebox_tokens) { __typename ...TokenMapFragment } } is_managed_pin_user registration_method } ... on GetPublicKeysError { error } } }`,
|
|
35
|
+
},
|
|
36
|
+
GenerateXChatTokenMutation: {
|
|
37
|
+
id: "Qh3fZRjPPtPoHYR_2sCZsA",
|
|
38
|
+
doc: `mutation GenerateXChatTokenMutation { user_get_x_chat_auth_token(safety_level: XChat) { __typename error_code token } }`,
|
|
39
|
+
},
|
|
40
|
+
AddEncryptedConversationKeysMutation: {
|
|
41
|
+
id: "4V1KC8ue2tHHvRuIzeczdg",
|
|
42
|
+
doc: `mutation AddEncryptedConversationKeysMutation($conversation_id: String!, $conversation_key_version: NumericString!, $conversation_participant_keys: [ApiConversationParticipantKeyInput!]!, $base64_encoded_key_rotation: String, $action_signatures: [ActionSignatureInput!], $ttl_msec: NumericString) { xchat_add_encrypted_conversation_key(conversation_id: $conversation_id, conversation_key_version: $conversation_key_version, conversation_participant_keys: $conversation_participant_keys, base64_encoded_key_rotation: $base64_encoded_key_rotation, safety_level: XChat, action_signatures: $action_signatures, ttl_msec: $ttl_msec) { __typename error_code conversation_key_change_sequence_id } }`,
|
|
43
|
+
},
|
|
44
|
+
SendMessageCreateMutation: {
|
|
45
|
+
id: "TWRPP7gnKwV_R8-tE-Dd3Q",
|
|
46
|
+
doc: `mutation SendMessageCreateMutation($conversation_id: String!, $message_id: String!, $conversation_token: String, $encoded_message_create_event: String, $encoded_message_event_signature: String) { xchat_send_create_message_event(conversation_id: $conversation_id, conversation_token: $conversation_token, encoded_message_create_event: $encoded_message_create_event, encoded_message_event_signature: $encoded_message_event_signature, message_id: $message_id, safety_level: XChat) { __typename encoded_message_event } }`,
|
|
47
|
+
},
|
|
48
|
+
AddXChatPublicKeyMutation: {
|
|
49
|
+
id: "CQsk6GRuWAVabyXqqEG1sA",
|
|
50
|
+
doc: `mutation AddXChatPublicKeyMutation($version: NumericString!, $generate_version: Boolean, $public_key: XChatPublicKeyInput!) { user_add_public_key(version: $version, public_key: $public_key, generate_version: $generate_version, safety_level: XChat) { __typename error_code version token_map { __typename ...TokenMapFragment } } } ${TOKEN_MAP_FRAGMENT}`,
|
|
51
|
+
},
|
|
52
|
+
DeleteMessageMutation: {
|
|
53
|
+
id: "4gsDQKEmYkOtvsSIpHXdQA",
|
|
54
|
+
doc: `mutation DeleteMessageMutation($sequence_ids: [String!], $conversation_id: String, $delete_message_action: DeleteMessageActionInput, $action_signatures: [ActionSignatureInput!]) { xchat_delete_messages(safety_level: XChat, sequence_ids: $sequence_ids, conversation_id: $conversation_id, delete_message_action: $delete_message_action, action_signatures: $action_signatures) { __typename error_code } }`,
|
|
55
|
+
},
|
|
56
|
+
DeleteXChatPublicKeyMutation: {
|
|
57
|
+
id: "W5iiIL1MVw4vomq-zLPHUQ",
|
|
58
|
+
doc: `mutation DeleteXChatPublicKeyMutation($version: NumericString!, $clearSigningPublicKeyOnly: Boolean!) { user_delete_public_key(version: $version, clear_signing_public_key_only: $clearSigningPublicKeyOnly, safety_level: XChat) { __typename error_code } }`,
|
|
59
|
+
},
|
|
60
|
+
DmAvPermissionsQuery: {
|
|
61
|
+
id: "kfX5AHDKZrivyHwCaz68mQ",
|
|
62
|
+
doc: `query DmAvPermissionsQuery($recipient_ids: [NumericString!]!) { get_av_permissions(recipient_ids: $recipient_ids, safety_level: DirectMessagesConversationTimeline) { __typename result { __typename can_dm error_code } } }`,
|
|
63
|
+
},
|
|
64
|
+
GetInitialXChatPageQuery: { id: "4hJY4a3hB6CE4C6ROaYeTg", doc: GET_INITIAL_CHAT_PAGE },
|
|
65
|
+
GetConversationPageQuery: { id: "IVlXls9JTnbgQ1gxsGAfJA", doc: GET_CONVERSATION_PAGE },
|
|
66
|
+
MuteConversationMutation: {
|
|
67
|
+
id: "6iDsxSkhGLvdiJpqtAtzTQ",
|
|
68
|
+
doc: `mutation MuteConversationMutation($conversationIds: [String!], $action_signatures: [ActionSignatureInput!]) { xchat_mute_conversation(conversation_ids: $conversationIds, action_signatures: $action_signatures, safety_level: DirectMessagesMutedUsers) { __typename error_code } }`,
|
|
69
|
+
},
|
|
70
|
+
UnmuteConversationMutation: {
|
|
71
|
+
id: "_f8wd8RlQCCysv8yMKeiaw",
|
|
72
|
+
doc: `mutation UnmuteConversationMutation($conversationIds: [String!], $action_signatures: [ActionSignatureInput!]) { xchat_unmute_conversation(conversation_ids: $conversationIds, action_signatures: $action_signatures, safety_level: DirectMessagesMutedUsers) { __typename error_code } }`,
|
|
73
|
+
},
|
|
74
|
+
UpdateConversationTTLMutation: {
|
|
75
|
+
id: "Gu3kCEwNN2V-Az8NDk30Zg",
|
|
76
|
+
doc: `mutation UpdateConversationTTLMutation($conversationId: String!, $ttlMsec: NumericString!, $action_signatures: [ActionSignatureInput!]) { xchat_update_conversation_message_duration(conversation_id: $conversationId, ttl_msec: $ttlMsec, safety_level: XChat, action_signatures: $action_signatures) { __typename error_code } }`,
|
|
77
|
+
},
|
|
78
|
+
RemoveConversationTTLMutation: {
|
|
79
|
+
id: "EqSXvxskUyw99ARuIbhYlg",
|
|
80
|
+
doc: `mutation RemoveConversationTTLMutation($conversationId: String!, $action_signatures: [ActionSignatureInput!]) { xchat_remove_conversation_message_duration(conversation_id: $conversationId, safety_level: XChat, action_signatures: $action_signatures) { __typename error_code } }`,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
async function chatGql(client, name, variables = {}) {
|
|
85
|
+
const op = OPS[name];
|
|
86
|
+
const auth = client.auth;
|
|
87
|
+
if (!auth?.csrfToken) throw new Error("must be logged in to use xchat");
|
|
88
|
+
|
|
89
|
+
const cycleTLS = await getCycleTLS();
|
|
90
|
+
const res = await cycleTLS(
|
|
91
|
+
`https://api.x.com/graphql/${op.id}/${name}`,
|
|
92
|
+
{
|
|
93
|
+
headers: {
|
|
94
|
+
accept: "*/*",
|
|
95
|
+
"accept-language": "en-US,en;q=0.9",
|
|
96
|
+
authorization: `Bearer ${auth.client.bearer}`,
|
|
97
|
+
"content-type": "application/json",
|
|
98
|
+
origin: "https://chat.x.com",
|
|
99
|
+
referer: "https://chat.x.com/",
|
|
100
|
+
"x-csrf-token": auth.csrfToken,
|
|
101
|
+
"x-twitter-active-user": "yes",
|
|
102
|
+
"x-twitter-auth-type": "OAuth2Session",
|
|
103
|
+
"x-twitter-client-language": "en",
|
|
104
|
+
"x-apollo-operation-id": op.id,
|
|
105
|
+
"x-apollo-operation-name": name,
|
|
106
|
+
"apollo-require-preflight": "true",
|
|
107
|
+
cookie: auth.client.headers.cookie + (client.elevatedCookies ? `; ${client.elevatedCookies}` : ""),
|
|
108
|
+
},
|
|
109
|
+
userAgent: auth.client.fingerprints.userAgent,
|
|
110
|
+
ja3: auth.client.fingerprints.ja3,
|
|
111
|
+
ja4r: auth.client.fingerprints.ja4r,
|
|
112
|
+
body: JSON.stringify({ operationName: name, variables, query: op.doc, queryId: op.id }),
|
|
113
|
+
proxy: client.proxy || undefined,
|
|
114
|
+
referrer: "https://chat.x.com/",
|
|
115
|
+
},
|
|
116
|
+
"POST",
|
|
117
|
+
);
|
|
118
|
+
const json = await res.json();
|
|
119
|
+
if (json?.errors?.[0]) {
|
|
120
|
+
throw new Error(json.errors.map((e) => e.message).join(", "));
|
|
121
|
+
}
|
|
122
|
+
return json;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function parseUser(ur) {
|
|
126
|
+
const result = ur?.result;
|
|
127
|
+
const perms = result?.chat_permissions;
|
|
128
|
+
const gpk = result?.get_public_keys;
|
|
129
|
+
const list = gpk?.public_keys_with_token_map ?? [];
|
|
130
|
+
return {
|
|
131
|
+
userId: ur?.rest_id ?? null,
|
|
132
|
+
onXChat: list.length > 0,
|
|
133
|
+
isManagedPinUser: gpk?.is_managed_pin_user ?? null,
|
|
134
|
+
permissions: perms
|
|
135
|
+
? {
|
|
136
|
+
canDm: perms.can_dm,
|
|
137
|
+
canDmOnXChat: perms.can_dm_on_xchat,
|
|
138
|
+
dmBlocking: perms.dm_blocking,
|
|
139
|
+
passesPremiumCheck: perms.passes_premium_check,
|
|
140
|
+
}
|
|
141
|
+
: null,
|
|
142
|
+
keys: list.map((e) => {
|
|
143
|
+
const m = e.public_key_with_metadata;
|
|
144
|
+
return {
|
|
145
|
+
version: m?.version ?? null,
|
|
146
|
+
publicKey: m?.public_key?.public_key ?? null,
|
|
147
|
+
signingPublicKey: m?.public_key?.signing_public_key ?? null,
|
|
148
|
+
identityPublicKeySignature: m?.public_key?.identity_public_key_signature ?? null,
|
|
149
|
+
registrationMethod: m?.public_key?.registration_method ?? null,
|
|
150
|
+
};
|
|
151
|
+
}),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function publicKeys(userIds, opts = {}) {
|
|
156
|
+
const arr = Array.isArray(userIds) ? userIds : [userIds];
|
|
157
|
+
const ids = await Promise.all(arr.map((u) => resolveUserId(this, u)));
|
|
158
|
+
const res = await chatGql(this, "GetPublicKeys", { ids, include_juicebox_tokens: !!opts.includeTokens });
|
|
159
|
+
return (res?.data?.user_results_by_rest_ids ?? []).map(parseUser);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function publicKey(userId) {
|
|
163
|
+
const [u] = await publicKeys.call(this, [userId]);
|
|
164
|
+
return u?.keys?.[0] ?? null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function profile(userId) {
|
|
168
|
+
const [u] = await publicKeys.call(this, [userId]);
|
|
169
|
+
return u ?? null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function permissions(userId) {
|
|
173
|
+
const [u] = await publicKeys.call(this, [userId]);
|
|
174
|
+
return u?.permissions ?? null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function canMessage(userId) {
|
|
178
|
+
const p = await permissions.call(this, userId);
|
|
179
|
+
return !!p?.canDmOnXChat;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function isOnXChat(userId) {
|
|
183
|
+
const [u] = await publicKeys.call(this, [userId]);
|
|
184
|
+
return !!u?.onXChat;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function token() {
|
|
188
|
+
const res = await chatGql(this, "GenerateXChatTokenMutation", {});
|
|
189
|
+
return res?.data?.user_get_x_chat_auth_token?.token ?? null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function gql(name, variables = {}) {
|
|
193
|
+
return chatGql(this, name, variables);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function fingerprint(key) {
|
|
197
|
+
const keyB64 = typeof key === "string" ? key : key?.publicKey;
|
|
198
|
+
if (!keyB64) throw new Error("xchat.fingerprint: pass a public key (SPKI base64) or a key object");
|
|
199
|
+
const bytes = Uint8Array.from(Buffer.from(keyB64, "base64"));
|
|
200
|
+
const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
|
|
201
|
+
const hex = [...hash].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
202
|
+
return hex.match(/.{1,4}/g).join(":");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// --- identity (required to send) ---
|
|
206
|
+
|
|
207
|
+
export async function createIdentity(opts = {}) {
|
|
208
|
+
const selfCustody = opts.selfCustody === true;
|
|
209
|
+
const pin = opts.pin != null ? String(opts.pin) : null;
|
|
210
|
+
if (!selfCustody && !pin) {
|
|
211
|
+
throw new Error("xchat.createIdentity: pass a { pin } to back the identity up to X's realms (the default, like the app), or { selfCustody: true } to skip the PIN backup");
|
|
212
|
+
}
|
|
213
|
+
const registrationMethod = opts.registrationMethod ?? (selfCustody ? "SelfCustody" : "CustomPin");
|
|
214
|
+
const km = await generateKeyMaterial();
|
|
215
|
+
const version = String(opts.version ?? Date.now());
|
|
216
|
+
const res = await chatGql(this, "AddXChatPublicKeyMutation", {
|
|
217
|
+
version,
|
|
218
|
+
generate_version: true,
|
|
219
|
+
public_key: {
|
|
220
|
+
public_key: km.publicKeyB64,
|
|
221
|
+
signing_public_key: km.signingPublicKeyB64,
|
|
222
|
+
identity_public_key_signature: km.identitySignatureB64,
|
|
223
|
+
registration_method: registrationMethod,
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
const result = res?.data?.user_add_public_key;
|
|
227
|
+
if (result?.error_code) throw new Error(`createIdentity failed: ${result.error_code}`);
|
|
228
|
+
const assignedVersion = String(result?.version ?? version);
|
|
229
|
+
|
|
230
|
+
if (!selfCustody) {
|
|
231
|
+
try {
|
|
232
|
+
await juiceboxRegister(result.token_map, pin, km.juiceboxSecret);
|
|
233
|
+
} catch (e) {
|
|
234
|
+
try {
|
|
235
|
+
await chatGql(this, "DeleteXChatPublicKeyMutation", { version: assignedVersion, clearSigningPublicKeyOnly: false });
|
|
236
|
+
} catch {}
|
|
237
|
+
throw new Error(`xchat.createIdentity: PIN backup failed (${e?.message ?? e}); identity rolled back. Pass { selfCustody: true } to skip the PIN backup.`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const identity = {
|
|
242
|
+
userId: String(this.user?.id),
|
|
243
|
+
version: assignedVersion,
|
|
244
|
+
registrationMethod,
|
|
245
|
+
pinBacked: !selfCustody,
|
|
246
|
+
publicKeyB64: km.publicKeyB64,
|
|
247
|
+
signingPublicKeyB64: km.signingPublicKeyB64,
|
|
248
|
+
identityPrivateJwk: km.identityPrivateJwk,
|
|
249
|
+
signingPrivateJwk: km.signingPrivateJwk,
|
|
250
|
+
};
|
|
251
|
+
await loadIdentity.call(this, identity);
|
|
252
|
+
return identity;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// recover your identity from the realms with just your PIN + this session, then load it.
|
|
256
|
+
// after this, you can read and send on all your existing conversations.
|
|
257
|
+
export async function recover(pin) {
|
|
258
|
+
const userId = String(this.user?.id ?? "");
|
|
259
|
+
if (!userId) throw new Error("xchat.recover: log in first");
|
|
260
|
+
const res = await chatGql(this, "GetPublicKeys", { ids: [userId], include_juicebox_tokens: true });
|
|
261
|
+
const entry = res?.data?.user_results_by_rest_ids?.[0]?.result?.get_public_keys?.public_keys_with_token_map?.[0];
|
|
262
|
+
const meta = entry?.public_key_with_metadata;
|
|
263
|
+
if (!meta?.public_key?.public_key) throw new Error("xchat.recover: no published key on this account; call createIdentity first");
|
|
264
|
+
const secret = await juiceboxRecover(entry.token_map, String(pin));
|
|
265
|
+
const { identityKey, signingKey } = await recoverIdentityKeys({
|
|
266
|
+
publicKeyB64: meta.public_key.public_key,
|
|
267
|
+
signingPublicKeyB64: meta.public_key.signing_public_key,
|
|
268
|
+
secret,
|
|
269
|
+
});
|
|
270
|
+
this._xchat = {
|
|
271
|
+
userId,
|
|
272
|
+
version: String(meta.version),
|
|
273
|
+
publicKeySpki: b64decode(meta.public_key.public_key),
|
|
274
|
+
signingPublicKeyB64: meta.public_key.signing_public_key,
|
|
275
|
+
identityKey,
|
|
276
|
+
signingKey,
|
|
277
|
+
cKeys: {},
|
|
278
|
+
};
|
|
279
|
+
return { userId, version: String(meta.version), recovered: true };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export async function loadIdentity(identity) {
|
|
283
|
+
const { identityKey, signingKey } = await importIdentity(identity);
|
|
284
|
+
const userId = String(identity.userId ?? this.user?.id ?? "");
|
|
285
|
+
if (!userId || userId === "undefined") {
|
|
286
|
+
throw new Error("xchat.loadIdentity: unknown userId; log in first or pass identity.userId");
|
|
287
|
+
}
|
|
288
|
+
this._xchat = {
|
|
289
|
+
userId,
|
|
290
|
+
version: String(identity.version),
|
|
291
|
+
publicKeySpki: b64decode(identity.publicKeyB64),
|
|
292
|
+
signingPublicKeyB64: identity.signingPublicKeyB64,
|
|
293
|
+
identityKey,
|
|
294
|
+
signingKey,
|
|
295
|
+
cKeys: {},
|
|
296
|
+
};
|
|
297
|
+
return this._xchat;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function requireIdentity(client) {
|
|
301
|
+
if (!client._xchat) {
|
|
302
|
+
throw new Error("no xchat identity loaded; call client.xchat.createIdentity() or client.xchat.loadIdentity() first");
|
|
303
|
+
}
|
|
304
|
+
return client._xchat;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function ensureConversationKey(client, conversationId, id, recipients) {
|
|
308
|
+
if (id.cKeys[conversationId]) return id.cKeys[conversationId];
|
|
309
|
+
const cKey = crypto.getRandomValues(new Uint8Array(32));
|
|
310
|
+
const version = String(Date.now());
|
|
311
|
+
const participantKeys = [
|
|
312
|
+
{ user_id: id.userId, encrypted_conversation_key: await eciesWrap(cKey, id.publicKeySpki), public_key_version: id.version },
|
|
313
|
+
...(await Promise.all(
|
|
314
|
+
recipients.map(async (r) => ({
|
|
315
|
+
user_id: r.userId,
|
|
316
|
+
encrypted_conversation_key: await eciesWrap(cKey, r.spki),
|
|
317
|
+
public_key_version: String(r.version),
|
|
318
|
+
})),
|
|
319
|
+
)),
|
|
320
|
+
];
|
|
321
|
+
await chatGql(client, "AddEncryptedConversationKeysMutation", {
|
|
322
|
+
conversation_id: conversationId,
|
|
323
|
+
conversation_key_version: version,
|
|
324
|
+
conversation_participant_keys: participantKeys,
|
|
325
|
+
});
|
|
326
|
+
id.cKeys[conversationId] = { key: cKey, version };
|
|
327
|
+
return id.cKeys[conversationId];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function resolveUserId(client, recipient) {
|
|
331
|
+
if (recipient && typeof recipient === "object") return String(recipient.id ?? recipient.userId);
|
|
332
|
+
const s = String(recipient).trim();
|
|
333
|
+
if (/^\d+$/.test(s)) return s;
|
|
334
|
+
const handle = s.replace(/^@/, "").toLowerCase();
|
|
335
|
+
client._xchatUserCache ??= {};
|
|
336
|
+
if (client._xchatUserCache[handle]) return client._xchatUserCache[handle];
|
|
337
|
+
const user = await client.users.getByUsername(handle);
|
|
338
|
+
if (!user?.id) throw new Error(`xchat: could not resolve user "${recipient}"`);
|
|
339
|
+
client._xchatUserCache[handle] = String(user.id);
|
|
340
|
+
return client._xchatUserCache[handle];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// the established conversation has a server-issued token (a JWT); the message signature must
|
|
344
|
+
// include it or the recipient/server rejects the message and it never enters the readable stream.
|
|
345
|
+
async function conversationToken(client, conversationId, id) {
|
|
346
|
+
id.tokens ??= {};
|
|
347
|
+
if (id.tokens[conversationId] !== undefined) return id.tokens[conversationId];
|
|
348
|
+
let token = "";
|
|
349
|
+
try {
|
|
350
|
+
const res = await chatGql(client, "GetConversationPageQuery", {
|
|
351
|
+
conversation_id: conversationId,
|
|
352
|
+
min_local_sequence_id: "9223372036854775807",
|
|
353
|
+
min_conversation_key_version: "9223372036854775807",
|
|
354
|
+
});
|
|
355
|
+
for (const b of res?.data?.get_conversation_page?.encoded_message_events ?? []) {
|
|
356
|
+
const t = readStringField(b, 5);
|
|
357
|
+
if (t) {
|
|
358
|
+
token = t;
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
} catch {}
|
|
363
|
+
id.tokens[conversationId] = token;
|
|
364
|
+
return token;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function sendEntry(client, recipient, holderBytes, opts = {}) {
|
|
368
|
+
const id = requireIdentity(client);
|
|
369
|
+
const recipientId = await resolveUserId(client, recipient);
|
|
370
|
+
const rk = await publicKey.call(client, recipientId);
|
|
371
|
+
if (!rk) throw new Error(`user ${recipientId} is not on XChat`);
|
|
372
|
+
const conversationId = conversationId1on1(id.userId, recipientId);
|
|
373
|
+
const ck = await ensureConversationKey(client, conversationId, id, [
|
|
374
|
+
{ userId: recipientId, spki: b64decode(rk.publicKey), version: rk.version },
|
|
375
|
+
]);
|
|
376
|
+
const token = await conversationToken(client, conversationId, id);
|
|
377
|
+
const frame = encryptBody(holderBytes, ck.key);
|
|
378
|
+
const mce = messageCreateEvent(frame, ck.version, opts);
|
|
379
|
+
const signature = await eventSignature({
|
|
380
|
+
signingKey: id.signingKey,
|
|
381
|
+
signingPublicKeyB64: id.signingPublicKeyB64,
|
|
382
|
+
conversationToken: token,
|
|
383
|
+
senderId: id.userId,
|
|
384
|
+
conversationId,
|
|
385
|
+
conversationKeyVersion: ck.version,
|
|
386
|
+
frame,
|
|
387
|
+
myVersion: id.version,
|
|
388
|
+
});
|
|
389
|
+
const messageId = crypto.randomUUID();
|
|
390
|
+
const res = await chatGql(client, "SendMessageCreateMutation", {
|
|
391
|
+
conversation_id: conversationId,
|
|
392
|
+
message_id: messageId,
|
|
393
|
+
conversation_token: token || null,
|
|
394
|
+
encoded_message_create_event: b64(mce),
|
|
395
|
+
encoded_message_event_signature: signature,
|
|
396
|
+
});
|
|
397
|
+
const event = res?.data?.xchat_send_create_message_event ?? null;
|
|
398
|
+
return {
|
|
399
|
+
conversationId,
|
|
400
|
+
recipientId,
|
|
401
|
+
messageId,
|
|
402
|
+
sequenceId: readLeadingSequenceId(event?.encoded_message_event),
|
|
403
|
+
response: event,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// --- send: text + everything else ---
|
|
408
|
+
|
|
409
|
+
export async function message(recipientId, text, opts = {}) {
|
|
410
|
+
return sendEntry(this, String(recipientId), content.text(text), opts);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export async function react(recipientId, messageSequenceId, emoji, opts = {}) {
|
|
414
|
+
return sendEntry(this, String(recipientId), content.reactionAdd(messageSequenceId, emoji, opts.attachmentId), opts);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export async function unreact(recipientId, messageSequenceId, emoji, opts = {}) {
|
|
418
|
+
return sendEntry(this, String(recipientId), content.reactionRemove(messageSequenceId, emoji, opts.attachmentId), opts);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export async function edit(recipientId, messageSequenceId, newText, opts = {}) {
|
|
422
|
+
return sendEntry(this, String(recipientId), content.edit(messageSequenceId, newText), opts);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export async function markRead(recipientId, sequenceId, opts = {}) {
|
|
426
|
+
return sendEntry(this, recipientId, content.markRead(sequenceId, opts.seenAtMillis ?? Date.now()), opts);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export async function markUnread(recipientId, sequenceId, opts = {}) {
|
|
430
|
+
return sendEntry(this, recipientId, content.markUnread(sequenceId), opts);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export async function pin(recipientId, opts = {}) {
|
|
434
|
+
const conversationId = conversationId1on1(requireIdentity(this).userId, await resolveUserId(this, recipientId));
|
|
435
|
+
return sendEntry(this, recipientId, content.pin(conversationId), opts);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export async function unpin(recipientId, opts = {}) {
|
|
439
|
+
const conversationId = conversationId1on1(requireIdentity(this).userId, await resolveUserId(this, recipientId));
|
|
440
|
+
return sendEntry(this, recipientId, content.unpin(conversationId), opts);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export async function setNickname(recipientId, nickname, opts = {}) {
|
|
444
|
+
const target = await resolveUserId(this, opts.userId ?? recipientId);
|
|
445
|
+
return sendEntry(this, recipientId, content.nickname(target, nickname), opts);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export async function reportScreenCapture(recipientId, opts = {}) {
|
|
449
|
+
const type = opts.recording ? SCREEN_CAPTURE.Recording : SCREEN_CAPTURE.Screenshot;
|
|
450
|
+
return sendEntry(this, recipientId, content.screenCapture(type), opts);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export async function callPermissions(userIds) {
|
|
454
|
+
const arr = Array.isArray(userIds) ? userIds : [userIds];
|
|
455
|
+
const ids = await Promise.all(arr.map((u) => resolveUserId(this, u)));
|
|
456
|
+
const res = await chatGql(this, "DmAvPermissionsQuery", { recipient_ids: ids });
|
|
457
|
+
return res?.data?.get_av_permissions?.result ?? null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// --- audio/video calls (WebRTC over Periscope signaling) ---
|
|
461
|
+
|
|
462
|
+
function periscopeFor(client) {
|
|
463
|
+
client._periscope ??= new PeriscopeSession(client);
|
|
464
|
+
return client._periscope;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function useWebRTC(engine) {
|
|
468
|
+
this._webrtcEngine = engine;
|
|
469
|
+
return this;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function sendCallLifecycle(client, recipientId, kind, broadcastId, data) {
|
|
473
|
+
if (!client._xchat) return;
|
|
474
|
+
try {
|
|
475
|
+
if (kind === "ended") await sendEntry(client, recipientId, content.avCallEnded(broadcastId, data.durationSeconds ?? 0, !!data.audioOnly));
|
|
476
|
+
else if (kind === "missed") await sendEntry(client, recipientId, content.avCallMissed(!!data.audioOnly));
|
|
477
|
+
} catch {}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export async function call(recipient, opts = {}) {
|
|
481
|
+
const selfId = String(this._xchat?.userId ?? this.user?.id ?? "");
|
|
482
|
+
if (!selfId) throw new Error("xchat.call: log in first");
|
|
483
|
+
const recipientId = await resolveUserId(this, recipient);
|
|
484
|
+
const audioOnly = opts.video ? false : (opts.audioOnly ?? true);
|
|
485
|
+
const webrtc = await loadWebRTC(this._webrtcEngine);
|
|
486
|
+
const ps = periscopeFor(this);
|
|
487
|
+
await ps.ensure();
|
|
488
|
+
const created = await ps.broadcastCreate([recipientId], audioOnly);
|
|
489
|
+
const broadcastId = created?.broadcast_id;
|
|
490
|
+
if (!broadcastId) throw new Error(`xchat.call: broadcast create failed: ${JSON.stringify(created)?.slice(0, 200)}`);
|
|
491
|
+
const conversationId = conversationId1on1(selfId, recipientId);
|
|
492
|
+
const callObj = new XChatCall({
|
|
493
|
+
periscope: ps,
|
|
494
|
+
webrtc,
|
|
495
|
+
iceServers: ps.ice,
|
|
496
|
+
role: "host",
|
|
497
|
+
broadcastId,
|
|
498
|
+
sessionUuid: created.session_uuid,
|
|
499
|
+
selfId,
|
|
500
|
+
peerId: recipientId,
|
|
501
|
+
conversationId,
|
|
502
|
+
audioOnly,
|
|
503
|
+
});
|
|
504
|
+
callObj.onLifecycle = (kind, data) => sendCallLifecycle(this, recipientId, kind, broadcastId, data);
|
|
505
|
+
if (opts.announce !== false) sendEntry(this, recipientId, content.avCallStarted(broadcastId, audioOnly)).catch(() => {});
|
|
506
|
+
await callObj.start();
|
|
507
|
+
return callObj;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export async function answerCall(incoming, opts = {}) {
|
|
511
|
+
const webrtc = await loadWebRTC(this._webrtcEngine);
|
|
512
|
+
const ps = periscopeFor(this);
|
|
513
|
+
await ps.ensure();
|
|
514
|
+
const broadcastId = incoming.broadcastId ?? incoming;
|
|
515
|
+
const hostId = String(incoming.hostId ?? opts.hostId ?? "");
|
|
516
|
+
const audioOnly = opts.video ? false : (opts.audioOnly ?? incoming.audioOnly ?? true);
|
|
517
|
+
const joined = await ps.broadcastJoin(broadcastId);
|
|
518
|
+
const selfId = String(this._xchat?.userId ?? this.user?.id ?? "");
|
|
519
|
+
const conversationId = incoming.conversationId ?? (selfId && hostId ? conversationId1on1(selfId, hostId) : null);
|
|
520
|
+
const callObj = new XChatCall({
|
|
521
|
+
periscope: ps,
|
|
522
|
+
webrtc,
|
|
523
|
+
iceServers: ps.ice,
|
|
524
|
+
role: "guest",
|
|
525
|
+
broadcastId,
|
|
526
|
+
sessionUuid: joined?.session_uuid,
|
|
527
|
+
selfId,
|
|
528
|
+
peerId: hostId,
|
|
529
|
+
conversationId,
|
|
530
|
+
audioOnly,
|
|
531
|
+
});
|
|
532
|
+
await callObj.start();
|
|
533
|
+
return callObj;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export async function startGroupCall(opts = {}) {
|
|
537
|
+
const audioOnly = opts.video ? false : (opts.audioOnly ?? true);
|
|
538
|
+
const webrtc = await loadWebRTC(this._webrtcEngine);
|
|
539
|
+
const ps = periscopeFor(this);
|
|
540
|
+
await ps.ensure();
|
|
541
|
+
const invites = opts.invite ? (Array.isArray(opts.invite) ? opts.invite : [opts.invite]) : [];
|
|
542
|
+
const recipientIds = await Promise.all(invites.map((u) => resolveUserId(this, u)));
|
|
543
|
+
const cb = await ps.createBroadcast({ audioOnly, conversationId: opts.conversationId ?? "" });
|
|
544
|
+
const broadcastId = cb?.broadcast?.id;
|
|
545
|
+
if (!broadcastId) throw new Error(`xchat.startGroupCall: createBroadcast failed: ${JSON.stringify(cb)?.slice(0, 200)}`);
|
|
546
|
+
const call = new GroupCall({
|
|
547
|
+
periscope: ps,
|
|
548
|
+
webrtc,
|
|
549
|
+
iceServers: ps.ice,
|
|
550
|
+
janusUrl: cb.webrtc_gw_url,
|
|
551
|
+
jwt: cb.credential,
|
|
552
|
+
sessionUuid: cb.session_uuid,
|
|
553
|
+
streamName: cb.stream_name,
|
|
554
|
+
periscopeUserId: ps.periscopeId,
|
|
555
|
+
roomId: broadcastId,
|
|
556
|
+
audioOnly,
|
|
557
|
+
isHost: true,
|
|
558
|
+
conversationId: opts.conversationId ?? "",
|
|
559
|
+
recipientIds,
|
|
560
|
+
videoCodec: opts.videoCodec,
|
|
561
|
+
});
|
|
562
|
+
await call.start();
|
|
563
|
+
call.broadcastId = broadcastId;
|
|
564
|
+
return call;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export async function joinGroupCall(broadcastId, opts = {}) {
|
|
568
|
+
const audioOnly = opts.video ? false : (opts.audioOnly ?? true);
|
|
569
|
+
const webrtc = await loadWebRTC(this._webrtcEngine);
|
|
570
|
+
const ps = periscopeFor(this);
|
|
571
|
+
await ps.ensure();
|
|
572
|
+
const j = await ps.joinBroadcast(broadcastId, { audioOnly });
|
|
573
|
+
const call = new GroupCall({
|
|
574
|
+
periscope: ps,
|
|
575
|
+
webrtc,
|
|
576
|
+
iceServers: ps.ice,
|
|
577
|
+
janusUrl: j.janusUrl,
|
|
578
|
+
jwt: j.jwt,
|
|
579
|
+
sessionUuid: j.sessionUuid,
|
|
580
|
+
streamName: extractStreamName(j.jwt),
|
|
581
|
+
periscopeUserId: ps.periscopeId,
|
|
582
|
+
roomId: broadcastId,
|
|
583
|
+
audioOnly,
|
|
584
|
+
isHost: false,
|
|
585
|
+
});
|
|
586
|
+
await call.start();
|
|
587
|
+
call.broadcastId = broadcastId;
|
|
588
|
+
return call;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
export function listenForCalls(handler, opts = {}) {
|
|
592
|
+
const selfId = String(this._xchat?.userId ?? this.user?.id ?? "");
|
|
593
|
+
if (!selfId) throw new Error("xchat.listenForCalls: log in first");
|
|
594
|
+
const ps = periscopeFor(this);
|
|
595
|
+
const client = this;
|
|
596
|
+
ps.ensure().catch(() => {});
|
|
597
|
+
return subscribeIncomingCalls(
|
|
598
|
+
this,
|
|
599
|
+
ps,
|
|
600
|
+
selfId,
|
|
601
|
+
(incoming) => {
|
|
602
|
+
handler({
|
|
603
|
+
...incoming,
|
|
604
|
+
accept: (acceptOpts = {}) => answerCall.call(client, incoming, acceptOpts),
|
|
605
|
+
decline: async () => {
|
|
606
|
+
try {
|
|
607
|
+
await ps.broadcastLeave(incoming.broadcastId, null, "User");
|
|
608
|
+
} catch {}
|
|
609
|
+
},
|
|
610
|
+
});
|
|
611
|
+
},
|
|
612
|
+
opts,
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// --- reading (decrypt inbound) ---
|
|
617
|
+
|
|
618
|
+
function detailKind(detail) {
|
|
619
|
+
if (detail[1]) return "message";
|
|
620
|
+
if (detail[3]) return "conversation_key_change";
|
|
621
|
+
if (detail[6]) return "typing";
|
|
622
|
+
if (detail[7]) return "delete";
|
|
623
|
+
if (detail[8]) return "conversation_delete";
|
|
624
|
+
if (detail[9]) return "metadata_change";
|
|
625
|
+
if (detail[12]) return "read";
|
|
626
|
+
if (detail[13]) return "unread";
|
|
627
|
+
return "unknown";
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
async function buildCKeyMap(keyChangeB64s, myUserId, identityKey) {
|
|
631
|
+
const map = {};
|
|
632
|
+
for (const b64v of keyChangeB64s ?? []) {
|
|
633
|
+
try {
|
|
634
|
+
const cke = parseThrift(b64decode(b64v))[7]?.[3];
|
|
635
|
+
if (!cke) continue;
|
|
636
|
+
const version = thriftStr(cke[1]);
|
|
637
|
+
for (const p of cke[2] ?? []) {
|
|
638
|
+
if (thriftStr(p[1]) === String(myUserId)) map[version] = await eciesUnwrap(thriftStr(p[2]), identityKey);
|
|
639
|
+
}
|
|
640
|
+
} catch {}
|
|
641
|
+
}
|
|
642
|
+
return map;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function decodeMessageEvent(b64v, cKeyMap) {
|
|
646
|
+
let ev;
|
|
647
|
+
try {
|
|
648
|
+
ev = parseThrift(b64decode(b64v));
|
|
649
|
+
} catch {
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
const detail = ev[7] ?? {};
|
|
653
|
+
const out = {
|
|
654
|
+
sequenceId: thriftStr(ev[1]),
|
|
655
|
+
messageId: thriftStr(ev[2]),
|
|
656
|
+
senderId: thriftStr(ev[3]),
|
|
657
|
+
conversationId: thriftStr(ev[4]),
|
|
658
|
+
createdAt: thriftStr(ev[6]),
|
|
659
|
+
kind: detailKind(detail),
|
|
660
|
+
};
|
|
661
|
+
const mce = detail[1];
|
|
662
|
+
if (!mce) return out;
|
|
663
|
+
const contents = mce[100];
|
|
664
|
+
const cKey = cKeyMap[thriftStr(mce[101])];
|
|
665
|
+
if (!cKey || !contents) {
|
|
666
|
+
out.encrypted = true;
|
|
667
|
+
return out;
|
|
668
|
+
}
|
|
669
|
+
const plain = decryptBody(contents, cKey);
|
|
670
|
+
if (!plain) {
|
|
671
|
+
out.decryptError = true;
|
|
672
|
+
return out;
|
|
673
|
+
}
|
|
674
|
+
let entry;
|
|
675
|
+
try {
|
|
676
|
+
entry = parseThrift(plain)[1] ?? {};
|
|
677
|
+
} catch {
|
|
678
|
+
return out;
|
|
679
|
+
}
|
|
680
|
+
if (entry[1]) {
|
|
681
|
+
out.kind = "message";
|
|
682
|
+
out.text = thriftStr(entry[1][1]);
|
|
683
|
+
} else if (entry[2]) {
|
|
684
|
+
out.kind = "reaction_add";
|
|
685
|
+
out.targetSequenceId = thriftStr(entry[2][1]);
|
|
686
|
+
out.emoji = thriftStr(entry[2][2]);
|
|
687
|
+
} else if (entry[3]) {
|
|
688
|
+
out.kind = "reaction_remove";
|
|
689
|
+
out.targetSequenceId = thriftStr(entry[3][1]);
|
|
690
|
+
out.emoji = thriftStr(entry[3][2]);
|
|
691
|
+
} else if (entry[4]) {
|
|
692
|
+
out.kind = "edit";
|
|
693
|
+
out.targetSequenceId = thriftStr(entry[4][1]);
|
|
694
|
+
out.text = thriftStr(entry[4][2]);
|
|
695
|
+
} else if (entry[16]) {
|
|
696
|
+
out.kind = "av_call_started";
|
|
697
|
+
out.audioOnly = !!entry[16][1];
|
|
698
|
+
out.broadcastId = thriftStr(entry[16][3]);
|
|
699
|
+
} else if (entry[10]) {
|
|
700
|
+
out.kind = "av_call_ended";
|
|
701
|
+
out.durationSeconds = entry[10][2] != null ? Number(entry[10][2]) : null;
|
|
702
|
+
out.audioOnly = !!entry[10][3];
|
|
703
|
+
out.broadcastId = thriftStr(entry[10][5]);
|
|
704
|
+
} else if (entry[11]) {
|
|
705
|
+
out.kind = "av_call_missed";
|
|
706
|
+
out.audioOnly = !!entry[11][2];
|
|
707
|
+
}
|
|
708
|
+
return out;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
export async function read(recipient, opts = {}) {
|
|
712
|
+
const id = requireIdentity(this);
|
|
713
|
+
const recipientId = await resolveUserId(this, recipient);
|
|
714
|
+
const conversationId = conversationId1on1(id.userId, recipientId);
|
|
715
|
+
// these "min_" params are actually upper bounds; pass a high value to read from the latest down
|
|
716
|
+
const res = await chatGql(this, "GetConversationPageQuery", {
|
|
717
|
+
conversation_id: conversationId,
|
|
718
|
+
min_local_sequence_id: String(opts.before ?? "9223372036854775807"),
|
|
719
|
+
min_conversation_key_version: "9223372036854775807",
|
|
720
|
+
});
|
|
721
|
+
const page = res?.data?.get_conversation_page ?? {};
|
|
722
|
+
const cKeyMap = await buildCKeyMap(page.missing_conversation_key_change_events, id.userId, id.identityKey);
|
|
723
|
+
const cached = id.cKeys[conversationId];
|
|
724
|
+
if (cached) cKeyMap[cached.version] ??= cached.key;
|
|
725
|
+
const messages = (page.encoded_message_events ?? []).map((b) => decodeMessageEvent(b, cKeyMap)).filter(Boolean);
|
|
726
|
+
return { conversationId, hasMore: !!page.has_more, messages };
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export async function conversations(opts = {}) {
|
|
730
|
+
const res = await chatGql(this, "GetInitialXChatPageQuery", {
|
|
731
|
+
max_local_sequence_id: opts.maxLocalSequenceId ?? null,
|
|
732
|
+
query_settings: null,
|
|
733
|
+
message_pull_version: opts.messagePullVersion ?? null,
|
|
734
|
+
});
|
|
735
|
+
const page = res?.data?.get_initial_chat_page ?? {};
|
|
736
|
+
const id = this._xchat;
|
|
737
|
+
return Promise.all(
|
|
738
|
+
(page.items ?? []).map(async (it) => {
|
|
739
|
+
const d = it.conversation_detail ?? {};
|
|
740
|
+
const isGroup = d.__typename === "XChatGroupConversationDetail";
|
|
741
|
+
const conv = {
|
|
742
|
+
conversationId: d.conversation_id,
|
|
743
|
+
type: isGroup ? "group" : "direct",
|
|
744
|
+
isMuted: d.is_muted,
|
|
745
|
+
groupName: d.group_metadata?.group_name,
|
|
746
|
+
participants: ((isGroup ? d.group_members_results : d.participants_results) ?? []).map((p) => p.rest_id),
|
|
747
|
+
latestSequenceId: it.latest_message_sequence_id,
|
|
748
|
+
};
|
|
749
|
+
if (id && it.latest_message_events?.length) {
|
|
750
|
+
try {
|
|
751
|
+
const cKeyMap = await buildCKeyMap(it.latest_conversation_key_change_events, id.userId, id.identityKey);
|
|
752
|
+
const decoded = it.latest_message_events.map((b) => decodeMessageEvent(b, cKeyMap)).filter(Boolean);
|
|
753
|
+
conv.latestMessage = decoded[decoded.length - 1] ?? null;
|
|
754
|
+
} catch {}
|
|
755
|
+
}
|
|
756
|
+
return conv;
|
|
757
|
+
}),
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
export async function deleteMessages(recipient, sequenceIds, opts = {}) {
|
|
762
|
+
const userId = String(this._xchat?.userId ?? this.user?.id ?? "");
|
|
763
|
+
if (!userId) throw new Error("xchat.deleteMessages: log in first");
|
|
764
|
+
const recipientId = await resolveUserId(this, recipient);
|
|
765
|
+
const conversationId = conversationId1on1(userId, recipientId);
|
|
766
|
+
const ids = (Array.isArray(sequenceIds) ? sequenceIds : [sequenceIds]).map(String);
|
|
767
|
+
|
|
768
|
+
let action_signatures = null;
|
|
769
|
+
if (opts.forEveryone) {
|
|
770
|
+
const id = requireIdentity(this);
|
|
771
|
+
const token = await conversationToken(this, conversationId, id);
|
|
772
|
+
action_signatures = [
|
|
773
|
+
await buildActionSignature({
|
|
774
|
+
signingKey: id.signingKey,
|
|
775
|
+
signingPublicKeyB64: id.signingPublicKeyB64,
|
|
776
|
+
typeName: "MessageDeleteEvent",
|
|
777
|
+
conversationToken: token,
|
|
778
|
+
senderId: userId,
|
|
779
|
+
conversationId,
|
|
780
|
+
dataElements: ["2", ...ids], // 2 = DELETE_FOR_ALL, then each sequence id
|
|
781
|
+
eventDetailBytes: deleteEventDetail(ids, 2),
|
|
782
|
+
myVersion: id.version,
|
|
783
|
+
}),
|
|
784
|
+
];
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const res = await chatGql(this, "DeleteMessageMutation", {
|
|
788
|
+
conversation_id: conversationId,
|
|
789
|
+
sequence_ids: ids,
|
|
790
|
+
delete_message_action: opts.forEveryone ? "DeleteForAll" : "DeleteForSelf",
|
|
791
|
+
action_signatures,
|
|
792
|
+
});
|
|
793
|
+
return res?.data?.xchat_delete_messages ?? null;
|
|
794
|
+
}
|