@inline-chat/realtime-sdk 0.0.1 → 0.0.3
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/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { InlineSdkClient } from "./sdk/inline-sdk-client.js";
|
|
2
|
-
export type { InlineSdkClientOptions, InlineSdkState, InlineSdkStateStore, InlineInboundEvent, RpcInputForMethod, RpcResultForMethod, } from "./sdk/types.js";
|
|
2
|
+
export type { InlineSdkClientOptions, InlineSdkSendMessageMedia, InlineSdkSendMessageParams, InlineSdkState, InlineSdkStateStore, InlineSdkUploadFileParams, InlineSdkUploadFileResult, InlineSdkUploadFileType, InlineInboundEvent, RpcInputForMethod, RpcResultForMethod, } from "./sdk/types.js";
|
|
3
3
|
export type { InlineSdkLogger } from "./sdk/logger.js";
|
|
4
4
|
export { JsonFileStateStore } from "./state/json-file-state-store.js";
|
|
5
5
|
export { serializeStateV1, deserializeStateV1 } from "./state/serde.js";
|
|
@@ -9,6 +9,7 @@ export type ProtocolClientOptions = {
|
|
|
9
9
|
transport: Transport;
|
|
10
10
|
getConnectionInit: () => ConnectionInit | null;
|
|
11
11
|
logger?: InlineSdkLogger;
|
|
12
|
+
defaultRpcTimeoutMs?: number | null;
|
|
12
13
|
};
|
|
13
14
|
export declare class ProtocolClient {
|
|
14
15
|
readonly events: AsyncChannel<ClientEvent>;
|
|
@@ -17,7 +18,8 @@ export declare class ProtocolClient {
|
|
|
17
18
|
state: ClientState;
|
|
18
19
|
private readonly log;
|
|
19
20
|
private readonly getConnectionInit;
|
|
20
|
-
private
|
|
21
|
+
private readonly defaultRpcTimeoutMs;
|
|
22
|
+
private pendingRpcRequests;
|
|
21
23
|
private seq;
|
|
22
24
|
private lastTimestamp;
|
|
23
25
|
private sequence;
|
|
@@ -35,7 +37,7 @@ export declare class ProtocolClient {
|
|
|
35
37
|
}): Promise<void>;
|
|
36
38
|
sendRpc(method: Method, input?: RpcCall["input"]): Promise<bigint>;
|
|
37
39
|
callRpc(method: Method, input?: RpcCall["input"], options?: {
|
|
38
|
-
timeoutMs?: number;
|
|
40
|
+
timeoutMs?: number | null;
|
|
39
41
|
}): Promise<RpcResult["result"]>;
|
|
40
42
|
private startListeners;
|
|
41
43
|
private handleTransportMessage;
|
|
@@ -53,10 +55,14 @@ export declare class ProtocolClient {
|
|
|
53
55
|
private generateId;
|
|
54
56
|
private currentTimestamp;
|
|
55
57
|
private completeRpcResult;
|
|
58
|
+
private ensureOpenForRpc;
|
|
56
59
|
private completeRpcError;
|
|
57
|
-
private
|
|
58
|
-
private
|
|
59
|
-
private
|
|
60
|
+
private failPendingRpcRequest;
|
|
61
|
+
private getAndRemovePendingRpcRequest;
|
|
62
|
+
private cancelAllPendingRpcRequests;
|
|
63
|
+
private resolveRpcTimeoutMs;
|
|
64
|
+
private resendPendingRpcRequests;
|
|
65
|
+
private trySendPendingRpcRequest;
|
|
60
66
|
}
|
|
61
67
|
export declare class ProtocolClientError extends Error {
|
|
62
68
|
constructor(code: "not-authorized" | "not-connected" | "rpc-error" | "stopped" | "timeout", details?: {
|
|
@@ -2,6 +2,7 @@ import { ClientMessage, ServerProtocolMessage } from "@inline-chat/protocol/core
|
|
|
2
2
|
import { AsyncChannel } from "../utils/async-channel.js";
|
|
3
3
|
import { PingPongService } from "./ping-pong.js";
|
|
4
4
|
const emptyRpcInput = { oneofKind: undefined };
|
|
5
|
+
const defaultRpcTimeoutMs = 30_000;
|
|
5
6
|
export class ProtocolClient {
|
|
6
7
|
events = new AsyncChannel();
|
|
7
8
|
transport;
|
|
@@ -9,7 +10,8 @@ export class ProtocolClient {
|
|
|
9
10
|
state = "connecting";
|
|
10
11
|
log;
|
|
11
12
|
getConnectionInit;
|
|
12
|
-
|
|
13
|
+
defaultRpcTimeoutMs;
|
|
14
|
+
pendingRpcRequests = new Map();
|
|
13
15
|
seq = 0;
|
|
14
16
|
lastTimestamp = 0;
|
|
15
17
|
sequence = 0;
|
|
@@ -22,6 +24,7 @@ export class ProtocolClient {
|
|
|
22
24
|
this.transport = options.transport;
|
|
23
25
|
this.log = options.logger ?? {};
|
|
24
26
|
this.getConnectionInit = options.getConnectionInit;
|
|
27
|
+
this.defaultRpcTimeoutMs = normalizeRpcTimeoutMs(options.defaultRpcTimeoutMs, defaultRpcTimeoutMs);
|
|
25
28
|
this.pingPong = new PingPongService({ logger: this.log });
|
|
26
29
|
this.pingPong.configure(this);
|
|
27
30
|
this.startListeners();
|
|
@@ -48,6 +51,7 @@ export class ProtocolClient {
|
|
|
48
51
|
await this.transport.reconnect({ skipDelay: options?.skipDelay });
|
|
49
52
|
}
|
|
50
53
|
async sendRpc(method, input = emptyRpcInput) {
|
|
54
|
+
this.ensureOpenForRpc();
|
|
51
55
|
const message = this.wrapMessage({
|
|
52
56
|
oneofKind: "rpcCall",
|
|
53
57
|
rpcCall: { method, input },
|
|
@@ -61,17 +65,20 @@ export class ProtocolClient {
|
|
|
61
65
|
rpcCall: { method, input },
|
|
62
66
|
});
|
|
63
67
|
return await new Promise((resolve, reject) => {
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
const pending = {
|
|
69
|
+
message,
|
|
70
|
+
resolve,
|
|
71
|
+
reject,
|
|
72
|
+
timeoutMs: this.resolveRpcTimeoutMs(options?.timeoutMs),
|
|
73
|
+
sending: false,
|
|
74
|
+
};
|
|
75
|
+
this.pendingRpcRequests.set(message.id, pending);
|
|
76
|
+
if (pending.timeoutMs !== null) {
|
|
77
|
+
pending.timeout = setTimeout(() => {
|
|
78
|
+
this.failPendingRpcRequest(message.id, new ProtocolClientError("timeout"));
|
|
79
|
+
}, pending.timeoutMs);
|
|
74
80
|
}
|
|
81
|
+
this.trySendPendingRpcRequest(message.id);
|
|
75
82
|
});
|
|
76
83
|
}
|
|
77
84
|
async startListeners() {
|
|
@@ -168,6 +175,7 @@ export class ProtocolClient {
|
|
|
168
175
|
}
|
|
169
176
|
this.connectionAttemptNo = 0;
|
|
170
177
|
this.pingPong.start();
|
|
178
|
+
this.resendPendingRpcRequests();
|
|
171
179
|
}
|
|
172
180
|
async connecting() {
|
|
173
181
|
this.state = "connecting";
|
|
@@ -176,7 +184,7 @@ export class ProtocolClient {
|
|
|
176
184
|
async reset() {
|
|
177
185
|
this.pingPong.stop();
|
|
178
186
|
this.stopAuthenticationTimeout();
|
|
179
|
-
this.
|
|
187
|
+
this.cancelAllPendingRpcRequests(new ProtocolClientError("stopped"));
|
|
180
188
|
this.state = "connecting";
|
|
181
189
|
}
|
|
182
190
|
startAuthenticationTimeout() {
|
|
@@ -195,8 +203,8 @@ export class ProtocolClient {
|
|
|
195
203
|
}
|
|
196
204
|
handleClientFailure() {
|
|
197
205
|
this.pingPong.stop();
|
|
198
|
-
this.cancelAllRpcContinuations(new ProtocolClientError("not-connected"));
|
|
199
206
|
this.stopAuthenticationTimeout();
|
|
207
|
+
this.state = "connecting";
|
|
200
208
|
if (this.reconnectionTimer) {
|
|
201
209
|
clearTimeout(this.reconnectionTimer);
|
|
202
210
|
}
|
|
@@ -239,36 +247,80 @@ export class ProtocolClient {
|
|
|
239
247
|
return Math.floor(Date.now() / 1000) - this.epochSeconds;
|
|
240
248
|
}
|
|
241
249
|
completeRpcResult(msgId, rpcResult) {
|
|
242
|
-
const
|
|
243
|
-
|
|
250
|
+
const pending = this.getAndRemovePendingRpcRequest(msgId);
|
|
251
|
+
pending?.resolve(rpcResult);
|
|
252
|
+
}
|
|
253
|
+
ensureOpenForRpc() {
|
|
254
|
+
if (this.state !== "open") {
|
|
255
|
+
throw new ProtocolClientError("not-connected");
|
|
256
|
+
}
|
|
244
257
|
}
|
|
245
258
|
completeRpcError(msgId, rpcError) {
|
|
246
259
|
const error = new ProtocolClientError("rpc-error", { code: rpcError.code, message: rpcError.message });
|
|
247
|
-
const
|
|
248
|
-
|
|
260
|
+
const pending = this.getAndRemovePendingRpcRequest(msgId);
|
|
261
|
+
pending?.reject(error);
|
|
249
262
|
}
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
263
|
+
failPendingRpcRequest(msgId, error) {
|
|
264
|
+
const pending = this.getAndRemovePendingRpcRequest(msgId);
|
|
265
|
+
pending?.reject(error);
|
|
253
266
|
}
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
if (!
|
|
267
|
+
getAndRemovePendingRpcRequest(msgId) {
|
|
268
|
+
const pending = this.pendingRpcRequests.get(msgId);
|
|
269
|
+
if (!pending)
|
|
257
270
|
return null;
|
|
258
|
-
if (
|
|
259
|
-
clearTimeout(
|
|
260
|
-
this.
|
|
261
|
-
return
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
for (const
|
|
265
|
-
|
|
266
|
-
if (
|
|
267
|
-
clearTimeout(
|
|
271
|
+
if (pending.timeout)
|
|
272
|
+
clearTimeout(pending.timeout);
|
|
273
|
+
this.pendingRpcRequests.delete(msgId);
|
|
274
|
+
return pending;
|
|
275
|
+
}
|
|
276
|
+
cancelAllPendingRpcRequests(error) {
|
|
277
|
+
for (const pending of this.pendingRpcRequests.values()) {
|
|
278
|
+
pending.reject(error);
|
|
279
|
+
if (pending.timeout)
|
|
280
|
+
clearTimeout(pending.timeout);
|
|
281
|
+
}
|
|
282
|
+
this.pendingRpcRequests.clear();
|
|
283
|
+
}
|
|
284
|
+
resolveRpcTimeoutMs(timeoutMs) {
|
|
285
|
+
return normalizeRpcTimeoutMs(timeoutMs, this.defaultRpcTimeoutMs);
|
|
286
|
+
}
|
|
287
|
+
resendPendingRpcRequests() {
|
|
288
|
+
for (const msgId of this.pendingRpcRequests.keys()) {
|
|
289
|
+
this.trySendPendingRpcRequest(msgId);
|
|
268
290
|
}
|
|
269
|
-
|
|
291
|
+
}
|
|
292
|
+
trySendPendingRpcRequest(msgId) {
|
|
293
|
+
const pending = this.pendingRpcRequests.get(msgId);
|
|
294
|
+
if (!pending)
|
|
295
|
+
return;
|
|
296
|
+
if (this.state !== "open")
|
|
297
|
+
return;
|
|
298
|
+
if (pending.sending)
|
|
299
|
+
return;
|
|
300
|
+
pending.sending = true;
|
|
301
|
+
void this.transport
|
|
302
|
+
.send(pending.message)
|
|
303
|
+
.catch((error) => {
|
|
304
|
+
this.log.warn?.("Failed to send RPC request; waiting for reconnect", error);
|
|
305
|
+
this.handleClientFailure();
|
|
306
|
+
})
|
|
307
|
+
.finally(() => {
|
|
308
|
+
pending.sending = false;
|
|
309
|
+
});
|
|
270
310
|
}
|
|
271
311
|
}
|
|
312
|
+
const normalizeRpcTimeoutMs = (timeoutMs, fallback) => {
|
|
313
|
+
const resolved = timeoutMs === undefined ? fallback : timeoutMs;
|
|
314
|
+
if (resolved == null)
|
|
315
|
+
return null;
|
|
316
|
+
if (resolved === Number.POSITIVE_INFINITY)
|
|
317
|
+
return null;
|
|
318
|
+
if (!Number.isFinite(resolved))
|
|
319
|
+
return null;
|
|
320
|
+
if (resolved <= 0)
|
|
321
|
+
return null;
|
|
322
|
+
return Math.floor(resolved);
|
|
323
|
+
};
|
|
272
324
|
export class ProtocolClientError extends Error {
|
|
273
325
|
constructor(code, details) {
|
|
274
326
|
super(details?.message ?? code);
|
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Method, type Peer, type RpcCall, type RpcResult } from "@inline-chat/protocol/core";
|
|
2
2
|
import { type InlineIdLike } from "../ids.js";
|
|
3
|
-
import type { InlineSdkClientOptions, InlineInboundEvent, InlineSdkState, MappedMethod, RpcInputForMethod, RpcResultForMethod } from "./types.js";
|
|
4
|
-
type SendMessageTarget = {
|
|
5
|
-
chatId: InlineIdLike;
|
|
6
|
-
userId?: never;
|
|
7
|
-
} | {
|
|
8
|
-
userId: InlineIdLike;
|
|
9
|
-
chatId?: never;
|
|
10
|
-
};
|
|
3
|
+
import type { InlineSdkClientOptions, InlineInboundEvent, InlineSdkSendMessageParams, InlineSdkState, InlineSdkUploadFileParams, InlineSdkUploadFileResult, MappedMethod, RpcInputForMethod, RpcResultForMethod } from "./types.js";
|
|
11
4
|
export declare class InlineSdkClient {
|
|
12
5
|
private readonly options;
|
|
13
6
|
private readonly log;
|
|
7
|
+
private readonly httpBaseUrl;
|
|
8
|
+
private readonly fetchImpl;
|
|
14
9
|
private readonly transport;
|
|
15
10
|
private readonly protocol;
|
|
16
11
|
private readonly eventStream;
|
|
@@ -38,27 +33,22 @@ export declare class InlineSdkClient {
|
|
|
38
33
|
peer?: Peer;
|
|
39
34
|
title: string;
|
|
40
35
|
}>;
|
|
41
|
-
sendMessage(params:
|
|
42
|
-
text: string;
|
|
43
|
-
replyToMsgId?: InlineIdLike;
|
|
44
|
-
parseMarkdown?: boolean;
|
|
45
|
-
sendMode?: "silent";
|
|
46
|
-
entities?: MessageEntities;
|
|
47
|
-
}): Promise<{
|
|
36
|
+
sendMessage(params: InlineSdkSendMessageParams): Promise<{
|
|
48
37
|
messageId: bigint | null;
|
|
49
38
|
}>;
|
|
39
|
+
uploadFile(params: InlineSdkUploadFileParams): Promise<InlineSdkUploadFileResult>;
|
|
50
40
|
sendTyping(params: {
|
|
51
41
|
chatId: InlineIdLike;
|
|
52
42
|
typing: boolean;
|
|
53
43
|
}): Promise<void>;
|
|
54
44
|
invokeRaw(method: Method, input?: RpcCall["input"], options?: {
|
|
55
|
-
timeoutMs?: number;
|
|
45
|
+
timeoutMs?: number | null;
|
|
56
46
|
}): Promise<RpcResult["result"]>;
|
|
57
47
|
invokeUncheckedRaw(method: Method, input?: RpcCall["input"], options?: {
|
|
58
|
-
timeoutMs?: number;
|
|
48
|
+
timeoutMs?: number | null;
|
|
59
49
|
}): Promise<RpcResult["result"]>;
|
|
60
50
|
invoke<M extends MappedMethod>(method: M, input: RpcInputForMethod<M>, options?: {
|
|
61
|
-
timeoutMs?: number;
|
|
51
|
+
timeoutMs?: number | null;
|
|
62
52
|
}): Promise<RpcResultForMethod<M>>;
|
|
63
53
|
private assertMethodInputMatch;
|
|
64
54
|
private assertMethodResultMatch;
|
|
@@ -76,4 +66,3 @@ export declare class InlineSdkClient {
|
|
|
76
66
|
private scheduleStateSave;
|
|
77
67
|
private flushStateSave;
|
|
78
68
|
}
|
|
79
|
-
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GetChatInput, GetMeInput, GetUpdatesInput, GetUpdatesResult_ResultType, GetUpdatesStateInput, InputPeer,
|
|
1
|
+
import { GetChatInput, GetMeInput, GetUpdatesInput, GetUpdatesResult_ResultType, GetUpdatesStateInput, InputPeer, MessageSendMode, Method, UpdateBucket, UpdateComposeAction_ComposeAction, } from "@inline-chat/protocol/core";
|
|
2
2
|
import { asInlineId } from "../ids.js";
|
|
3
3
|
import { AsyncChannel } from "../utils/async-channel.js";
|
|
4
4
|
import { ProtocolClient } from "../realtime/protocol-client.js";
|
|
@@ -8,6 +8,10 @@ import { noopLogger } from "./logger.js";
|
|
|
8
8
|
import { getSdkVersion } from "./sdk-version.js";
|
|
9
9
|
const nowSeconds = () => BigInt(Math.floor(Date.now() / 1000));
|
|
10
10
|
const sdkLayer = 1;
|
|
11
|
+
const defaultApiBaseUrl = "https://api.inline.chat";
|
|
12
|
+
const defaultVideoWidth = 1280;
|
|
13
|
+
const defaultVideoHeight = 720;
|
|
14
|
+
const defaultVideoDuration = 1;
|
|
11
15
|
function extractFirstMessageId(updates) {
|
|
12
16
|
for (const update of updates ?? []) {
|
|
13
17
|
if (update.update.oneofKind === "newMessage") {
|
|
@@ -21,6 +25,8 @@ function extractFirstMessageId(updates) {
|
|
|
21
25
|
export class InlineSdkClient {
|
|
22
26
|
options;
|
|
23
27
|
log;
|
|
28
|
+
httpBaseUrl;
|
|
29
|
+
fetchImpl;
|
|
24
30
|
transport;
|
|
25
31
|
protocol;
|
|
26
32
|
eventStream = new AsyncChannel();
|
|
@@ -35,8 +41,9 @@ export class InlineSdkClient {
|
|
|
35
41
|
constructor(options) {
|
|
36
42
|
this.options = options;
|
|
37
43
|
this.log = options.logger ?? noopLogger;
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
this.httpBaseUrl = normalizeHttpBaseUrl(options.baseUrl ?? defaultApiBaseUrl);
|
|
45
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
46
|
+
const url = resolveRealtimeUrl(this.httpBaseUrl);
|
|
40
47
|
this.transport = options.transport ?? new WebSocketTransport({ url, logger: options.logger });
|
|
41
48
|
this.protocol = new ProtocolClient({
|
|
42
49
|
transport: this.transport,
|
|
@@ -46,6 +53,7 @@ export class InlineSdkClient {
|
|
|
46
53
|
clientVersion: getSdkVersion(),
|
|
47
54
|
}),
|
|
48
55
|
logger: options.logger,
|
|
56
|
+
defaultRpcTimeoutMs: options.rpcTimeoutMs,
|
|
49
57
|
});
|
|
50
58
|
void this.startListeners();
|
|
51
59
|
}
|
|
@@ -143,12 +151,24 @@ export class InlineSdkClient {
|
|
|
143
151
|
if (params.entities != null && params.parseMarkdown != null) {
|
|
144
152
|
throw new Error("sendMessage: provide either `entities` or `parseMarkdown`, not both");
|
|
145
153
|
}
|
|
154
|
+
const hasText = typeof params.text === "string" && params.text.length > 0;
|
|
155
|
+
if (!hasText && params.media == null) {
|
|
156
|
+
throw new Error("sendMessage: provide `text` and/or `media`");
|
|
157
|
+
}
|
|
158
|
+
if (params.parseMarkdown != null && !hasText) {
|
|
159
|
+
throw new Error("sendMessage: `parseMarkdown` requires non-empty `text`");
|
|
160
|
+
}
|
|
161
|
+
if (params.entities != null && !hasText) {
|
|
162
|
+
throw new Error("sendMessage: `entities` requires non-empty `text`");
|
|
163
|
+
}
|
|
146
164
|
const peerId = this.inputPeerFromTarget(params, "sendMessage");
|
|
165
|
+
const media = params.media != null ? toInputMedia(params.media) : undefined;
|
|
147
166
|
const result = await this.invoke(Method.SEND_MESSAGE, {
|
|
148
167
|
oneofKind: "sendMessage",
|
|
149
168
|
sendMessage: {
|
|
150
169
|
peerId,
|
|
151
|
-
message: params.text,
|
|
170
|
+
...(hasText ? { message: params.text } : {}),
|
|
171
|
+
...(media != null ? { media } : {}),
|
|
152
172
|
...(params.replyToMsgId != null ? { replyToMsgId: asInlineId(params.replyToMsgId, "replyToMsgId") } : {}),
|
|
153
173
|
...(params.parseMarkdown != null ? { parseMarkdown: params.parseMarkdown } : {}),
|
|
154
174
|
...(params.entities != null ? { entities: params.entities } : {}),
|
|
@@ -158,6 +178,59 @@ export class InlineSdkClient {
|
|
|
158
178
|
const messageId = extractFirstMessageId(result.sendMessage.updates);
|
|
159
179
|
return { messageId };
|
|
160
180
|
}
|
|
181
|
+
async uploadFile(params) {
|
|
182
|
+
const form = new FormData();
|
|
183
|
+
form.set("type", params.type);
|
|
184
|
+
const fileName = normalizeUploadFileName(params.fileName, params.type);
|
|
185
|
+
const fileContentType = resolveUploadContentType(params.type, params.contentType);
|
|
186
|
+
form.set("file", toBlob(params.file, fileContentType), fileName);
|
|
187
|
+
if (params.thumbnail != null) {
|
|
188
|
+
const thumbnailName = normalizeUploadFileName(params.thumbnailFileName, "photo");
|
|
189
|
+
const thumbnailContentType = resolveUploadContentType("photo", params.thumbnailContentType);
|
|
190
|
+
form.set("thumbnail", toBlob(params.thumbnail, thumbnailContentType), thumbnailName);
|
|
191
|
+
}
|
|
192
|
+
if (params.type === "video") {
|
|
193
|
+
const width = normalizePositiveInt(params.width, "width") ?? defaultVideoWidth;
|
|
194
|
+
const height = normalizePositiveInt(params.height, "height") ?? defaultVideoHeight;
|
|
195
|
+
const duration = normalizePositiveInt(params.duration, "duration") ?? defaultVideoDuration;
|
|
196
|
+
form.set("width", String(width));
|
|
197
|
+
form.set("height", String(height));
|
|
198
|
+
form.set("duration", String(duration));
|
|
199
|
+
}
|
|
200
|
+
const response = await this.fetchImpl(new URL("uploadFile", `${this.httpBaseUrl}/`), {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: {
|
|
203
|
+
authorization: `Bearer ${this.options.token}`,
|
|
204
|
+
},
|
|
205
|
+
body: form,
|
|
206
|
+
});
|
|
207
|
+
const payload = await parseJsonResponse(response);
|
|
208
|
+
if (!response.ok) {
|
|
209
|
+
const detail = describeUploadFailure(payload);
|
|
210
|
+
throw new Error(`uploadFile: request failed with status ${response.status}${detail ? ` (${detail})` : ""}`);
|
|
211
|
+
}
|
|
212
|
+
if (!isRecord(payload) || payload.ok !== true) {
|
|
213
|
+
const detail = describeUploadFailure(payload);
|
|
214
|
+
throw new Error(`uploadFile: API error${detail ? ` (${detail})` : ""}`);
|
|
215
|
+
}
|
|
216
|
+
const result = payload.result;
|
|
217
|
+
if (!isRecord(result)) {
|
|
218
|
+
throw new Error("uploadFile: malformed success payload");
|
|
219
|
+
}
|
|
220
|
+
const fileUniqueId = typeof result.fileUniqueId === "string" ? result.fileUniqueId.trim() : "";
|
|
221
|
+
if (!fileUniqueId) {
|
|
222
|
+
throw new Error("uploadFile: response missing fileUniqueId");
|
|
223
|
+
}
|
|
224
|
+
const photoId = parseOptionalBigInt(result.photoId, "photoId");
|
|
225
|
+
const videoId = parseOptionalBigInt(result.videoId, "videoId");
|
|
226
|
+
const documentId = parseOptionalBigInt(result.documentId, "documentId");
|
|
227
|
+
return {
|
|
228
|
+
fileUniqueId,
|
|
229
|
+
...(photoId != null ? { photoId } : {}),
|
|
230
|
+
...(videoId != null ? { videoId } : {}),
|
|
231
|
+
...(documentId != null ? { documentId } : {}),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
161
234
|
async sendTyping(params) {
|
|
162
235
|
const peerId = InputPeer.create({
|
|
163
236
|
type: { oneofKind: "chat", chat: { chatId: asInlineId(params.chatId, "chatId") } },
|
|
@@ -530,6 +603,138 @@ export class InlineSdkClient {
|
|
|
530
603
|
await this.saveInFlight;
|
|
531
604
|
}
|
|
532
605
|
}
|
|
606
|
+
function toInputMedia(media) {
|
|
607
|
+
switch (media.kind) {
|
|
608
|
+
case "photo":
|
|
609
|
+
return {
|
|
610
|
+
media: {
|
|
611
|
+
oneofKind: "photo",
|
|
612
|
+
photo: {
|
|
613
|
+
photoId: asInlineId(media.photoId, "photoId"),
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
};
|
|
617
|
+
case "video":
|
|
618
|
+
return {
|
|
619
|
+
media: {
|
|
620
|
+
oneofKind: "video",
|
|
621
|
+
video: {
|
|
622
|
+
videoId: asInlineId(media.videoId, "videoId"),
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
};
|
|
626
|
+
case "document":
|
|
627
|
+
return {
|
|
628
|
+
media: {
|
|
629
|
+
oneofKind: "document",
|
|
630
|
+
document: {
|
|
631
|
+
documentId: asInlineId(media.documentId, "documentId"),
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function normalizeHttpBaseUrl(baseUrl) {
|
|
638
|
+
const url = new URL(baseUrl);
|
|
639
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
640
|
+
url.pathname = path || "/";
|
|
641
|
+
return url.toString().replace(/\/$/, "");
|
|
642
|
+
}
|
|
643
|
+
function normalizeUploadFileName(raw, type) {
|
|
644
|
+
const trimmed = raw?.trim();
|
|
645
|
+
if (trimmed)
|
|
646
|
+
return trimmed;
|
|
647
|
+
switch (type) {
|
|
648
|
+
case "photo":
|
|
649
|
+
return "photo.jpg";
|
|
650
|
+
case "video":
|
|
651
|
+
return "video.mp4";
|
|
652
|
+
case "document":
|
|
653
|
+
return "document.bin";
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
function resolveUploadContentType(type, explicit) {
|
|
657
|
+
const trimmed = explicit?.trim();
|
|
658
|
+
if (trimmed)
|
|
659
|
+
return trimmed;
|
|
660
|
+
switch (type) {
|
|
661
|
+
case "photo":
|
|
662
|
+
return "image/jpeg";
|
|
663
|
+
case "video":
|
|
664
|
+
return "video/mp4";
|
|
665
|
+
case "document":
|
|
666
|
+
return "application/octet-stream";
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function toBlob(input, type) {
|
|
670
|
+
if (input instanceof Blob) {
|
|
671
|
+
return input.type === type ? input : new Blob([input], { type });
|
|
672
|
+
}
|
|
673
|
+
return new Blob([input], { type });
|
|
674
|
+
}
|
|
675
|
+
function normalizePositiveInt(value, field) {
|
|
676
|
+
if (value == null)
|
|
677
|
+
return undefined;
|
|
678
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
|
|
679
|
+
throw new Error(`uploadFile: ${field} must be a positive integer`);
|
|
680
|
+
}
|
|
681
|
+
return value;
|
|
682
|
+
}
|
|
683
|
+
async function parseJsonResponse(response) {
|
|
684
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
685
|
+
if (!contentType.includes("application/json")) {
|
|
686
|
+
const text = await response.text();
|
|
687
|
+
return text;
|
|
688
|
+
}
|
|
689
|
+
try {
|
|
690
|
+
return await response.json();
|
|
691
|
+
}
|
|
692
|
+
catch {
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
function isRecord(value) {
|
|
697
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
698
|
+
}
|
|
699
|
+
function describeUploadFailure(payload) {
|
|
700
|
+
if (typeof payload === "string") {
|
|
701
|
+
const trimmed = payload.trim();
|
|
702
|
+
return trimmed ? trimmed : "";
|
|
703
|
+
}
|
|
704
|
+
if (!isRecord(payload))
|
|
705
|
+
return "";
|
|
706
|
+
const description = typeof payload.description === "string" ? payload.description.trim() : "";
|
|
707
|
+
if (description)
|
|
708
|
+
return description;
|
|
709
|
+
const error = typeof payload.error === "string" ? payload.error.trim() : "";
|
|
710
|
+
if (error)
|
|
711
|
+
return error;
|
|
712
|
+
return "";
|
|
713
|
+
}
|
|
714
|
+
function parseOptionalBigInt(value, field) {
|
|
715
|
+
if (value == null)
|
|
716
|
+
return undefined;
|
|
717
|
+
if (typeof value === "bigint")
|
|
718
|
+
return value;
|
|
719
|
+
if (typeof value === "number") {
|
|
720
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || !Number.isSafeInteger(value)) {
|
|
721
|
+
throw new Error(`uploadFile: invalid ${field} in response`);
|
|
722
|
+
}
|
|
723
|
+
return BigInt(value);
|
|
724
|
+
}
|
|
725
|
+
if (typeof value === "string") {
|
|
726
|
+
const trimmed = value.trim();
|
|
727
|
+
if (!trimmed)
|
|
728
|
+
return undefined;
|
|
729
|
+
try {
|
|
730
|
+
return BigInt(trimmed);
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
throw new Error(`uploadFile: invalid ${field} in response`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
throw new Error(`uploadFile: invalid ${field} in response`);
|
|
737
|
+
}
|
|
533
738
|
const resolveRealtimeUrl = (baseUrl) => {
|
|
534
739
|
const url = new URL(baseUrl);
|
|
535
740
|
const isSecure = url.protocol === "https:";
|
package/dist/sdk/types.d.ts
CHANGED
|
@@ -1,14 +1,65 @@
|
|
|
1
|
-
import type { Message, Peer, Reaction, RpcCall, RpcResult, Update, UpdateBucket, UpdatesPayload } from "@inline-chat/protocol/core";
|
|
2
|
-
import type { InlineId } from "../ids.js";
|
|
1
|
+
import type { Message, MessageEntities, Peer, Reaction, RpcCall, RpcResult, Update, UpdateBucket, UpdatesPayload } from "@inline-chat/protocol/core";
|
|
2
|
+
import type { InlineId, InlineIdLike } from "../ids.js";
|
|
3
3
|
import type { InlineUnixSeconds } from "../time.js";
|
|
4
4
|
import type { InlineSdkLogger } from "./logger.js";
|
|
5
5
|
import type { Transport } from "../realtime/transport.js";
|
|
6
6
|
export type InlineSdkClientOptions = {
|
|
7
7
|
baseUrl?: string;
|
|
8
8
|
token: string;
|
|
9
|
+
rpcTimeoutMs?: number | null;
|
|
9
10
|
logger?: InlineSdkLogger;
|
|
10
11
|
state?: InlineSdkStateStore;
|
|
11
12
|
transport?: Transport;
|
|
13
|
+
fetch?: typeof fetch;
|
|
14
|
+
};
|
|
15
|
+
export type InlineSdkSendMessageMedia = {
|
|
16
|
+
kind: "photo";
|
|
17
|
+
photoId: InlineIdLike;
|
|
18
|
+
} | {
|
|
19
|
+
kind: "video";
|
|
20
|
+
videoId: InlineIdLike;
|
|
21
|
+
} | {
|
|
22
|
+
kind: "document";
|
|
23
|
+
documentId: InlineIdLike;
|
|
24
|
+
};
|
|
25
|
+
export type InlineSdkSendMessageParams = {
|
|
26
|
+
chatId: InlineIdLike;
|
|
27
|
+
userId?: never;
|
|
28
|
+
text?: string;
|
|
29
|
+
media?: InlineSdkSendMessageMedia;
|
|
30
|
+
replyToMsgId?: InlineIdLike;
|
|
31
|
+
parseMarkdown?: boolean;
|
|
32
|
+
sendMode?: "silent";
|
|
33
|
+
entities?: MessageEntities;
|
|
34
|
+
} | {
|
|
35
|
+
userId: InlineIdLike;
|
|
36
|
+
chatId?: never;
|
|
37
|
+
text?: string;
|
|
38
|
+
media?: InlineSdkSendMessageMedia;
|
|
39
|
+
replyToMsgId?: InlineIdLike;
|
|
40
|
+
parseMarkdown?: boolean;
|
|
41
|
+
sendMode?: "silent";
|
|
42
|
+
entities?: MessageEntities;
|
|
43
|
+
};
|
|
44
|
+
export type InlineSdkBinaryInput = Blob | Uint8Array | ArrayBuffer | SharedArrayBuffer;
|
|
45
|
+
export type InlineSdkUploadFileType = "photo" | "video" | "document";
|
|
46
|
+
export type InlineSdkUploadFileParams = {
|
|
47
|
+
type: InlineSdkUploadFileType;
|
|
48
|
+
file: InlineSdkBinaryInput;
|
|
49
|
+
fileName?: string;
|
|
50
|
+
contentType?: string;
|
|
51
|
+
thumbnail?: InlineSdkBinaryInput;
|
|
52
|
+
thumbnailFileName?: string;
|
|
53
|
+
thumbnailContentType?: string;
|
|
54
|
+
width?: number;
|
|
55
|
+
height?: number;
|
|
56
|
+
duration?: number;
|
|
57
|
+
};
|
|
58
|
+
export type InlineSdkUploadFileResult = {
|
|
59
|
+
fileUniqueId: string;
|
|
60
|
+
photoId?: bigint;
|
|
61
|
+
videoId?: bigint;
|
|
62
|
+
documentId?: bigint;
|
|
12
63
|
};
|
|
13
64
|
export type InlineInboundEvent = {
|
|
14
65
|
kind: "message.new";
|