@inline-chat/realtime-sdk 0.0.1 → 0.0.2
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 +1 -1
- package/dist/sdk/inline-sdk-client.d.ts +6 -17
- package/dist/sdk/inline-sdk-client.js +208 -4
- package/dist/sdk/types.d.ts +52 -2
- package/package.json +1 -1
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";
|
|
@@ -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,15 +33,10 @@ 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;
|
|
@@ -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,
|
|
@@ -143,12 +150,24 @@ export class InlineSdkClient {
|
|
|
143
150
|
if (params.entities != null && params.parseMarkdown != null) {
|
|
144
151
|
throw new Error("sendMessage: provide either `entities` or `parseMarkdown`, not both");
|
|
145
152
|
}
|
|
153
|
+
const hasText = typeof params.text === "string" && params.text.length > 0;
|
|
154
|
+
if (!hasText && params.media == null) {
|
|
155
|
+
throw new Error("sendMessage: provide `text` and/or `media`");
|
|
156
|
+
}
|
|
157
|
+
if (params.parseMarkdown != null && !hasText) {
|
|
158
|
+
throw new Error("sendMessage: `parseMarkdown` requires non-empty `text`");
|
|
159
|
+
}
|
|
160
|
+
if (params.entities != null && !hasText) {
|
|
161
|
+
throw new Error("sendMessage: `entities` requires non-empty `text`");
|
|
162
|
+
}
|
|
146
163
|
const peerId = this.inputPeerFromTarget(params, "sendMessage");
|
|
164
|
+
const media = params.media != null ? toInputMedia(params.media) : undefined;
|
|
147
165
|
const result = await this.invoke(Method.SEND_MESSAGE, {
|
|
148
166
|
oneofKind: "sendMessage",
|
|
149
167
|
sendMessage: {
|
|
150
168
|
peerId,
|
|
151
|
-
message: params.text,
|
|
169
|
+
...(hasText ? { message: params.text } : {}),
|
|
170
|
+
...(media != null ? { media } : {}),
|
|
152
171
|
...(params.replyToMsgId != null ? { replyToMsgId: asInlineId(params.replyToMsgId, "replyToMsgId") } : {}),
|
|
153
172
|
...(params.parseMarkdown != null ? { parseMarkdown: params.parseMarkdown } : {}),
|
|
154
173
|
...(params.entities != null ? { entities: params.entities } : {}),
|
|
@@ -158,6 +177,59 @@ export class InlineSdkClient {
|
|
|
158
177
|
const messageId = extractFirstMessageId(result.sendMessage.updates);
|
|
159
178
|
return { messageId };
|
|
160
179
|
}
|
|
180
|
+
async uploadFile(params) {
|
|
181
|
+
const form = new FormData();
|
|
182
|
+
form.set("type", params.type);
|
|
183
|
+
const fileName = normalizeUploadFileName(params.fileName, params.type);
|
|
184
|
+
const fileContentType = resolveUploadContentType(params.type, params.contentType);
|
|
185
|
+
form.set("file", toBlob(params.file, fileContentType), fileName);
|
|
186
|
+
if (params.thumbnail != null) {
|
|
187
|
+
const thumbnailName = normalizeUploadFileName(params.thumbnailFileName, "photo");
|
|
188
|
+
const thumbnailContentType = resolveUploadContentType("photo", params.thumbnailContentType);
|
|
189
|
+
form.set("thumbnail", toBlob(params.thumbnail, thumbnailContentType), thumbnailName);
|
|
190
|
+
}
|
|
191
|
+
if (params.type === "video") {
|
|
192
|
+
const width = normalizePositiveInt(params.width, "width") ?? defaultVideoWidth;
|
|
193
|
+
const height = normalizePositiveInt(params.height, "height") ?? defaultVideoHeight;
|
|
194
|
+
const duration = normalizePositiveInt(params.duration, "duration") ?? defaultVideoDuration;
|
|
195
|
+
form.set("width", String(width));
|
|
196
|
+
form.set("height", String(height));
|
|
197
|
+
form.set("duration", String(duration));
|
|
198
|
+
}
|
|
199
|
+
const response = await this.fetchImpl(new URL("uploadFile", `${this.httpBaseUrl}/`), {
|
|
200
|
+
method: "POST",
|
|
201
|
+
headers: {
|
|
202
|
+
authorization: `Bearer ${this.options.token}`,
|
|
203
|
+
},
|
|
204
|
+
body: form,
|
|
205
|
+
});
|
|
206
|
+
const payload = await parseJsonResponse(response);
|
|
207
|
+
if (!response.ok) {
|
|
208
|
+
const detail = describeUploadFailure(payload);
|
|
209
|
+
throw new Error(`uploadFile: request failed with status ${response.status}${detail ? ` (${detail})` : ""}`);
|
|
210
|
+
}
|
|
211
|
+
if (!isRecord(payload) || payload.ok !== true) {
|
|
212
|
+
const detail = describeUploadFailure(payload);
|
|
213
|
+
throw new Error(`uploadFile: API error${detail ? ` (${detail})` : ""}`);
|
|
214
|
+
}
|
|
215
|
+
const result = payload.result;
|
|
216
|
+
if (!isRecord(result)) {
|
|
217
|
+
throw new Error("uploadFile: malformed success payload");
|
|
218
|
+
}
|
|
219
|
+
const fileUniqueId = typeof result.fileUniqueId === "string" ? result.fileUniqueId.trim() : "";
|
|
220
|
+
if (!fileUniqueId) {
|
|
221
|
+
throw new Error("uploadFile: response missing fileUniqueId");
|
|
222
|
+
}
|
|
223
|
+
const photoId = parseOptionalBigInt(result.photoId, "photoId");
|
|
224
|
+
const videoId = parseOptionalBigInt(result.videoId, "videoId");
|
|
225
|
+
const documentId = parseOptionalBigInt(result.documentId, "documentId");
|
|
226
|
+
return {
|
|
227
|
+
fileUniqueId,
|
|
228
|
+
...(photoId != null ? { photoId } : {}),
|
|
229
|
+
...(videoId != null ? { videoId } : {}),
|
|
230
|
+
...(documentId != null ? { documentId } : {}),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
161
233
|
async sendTyping(params) {
|
|
162
234
|
const peerId = InputPeer.create({
|
|
163
235
|
type: { oneofKind: "chat", chat: { chatId: asInlineId(params.chatId, "chatId") } },
|
|
@@ -530,6 +602,138 @@ export class InlineSdkClient {
|
|
|
530
602
|
await this.saveInFlight;
|
|
531
603
|
}
|
|
532
604
|
}
|
|
605
|
+
function toInputMedia(media) {
|
|
606
|
+
switch (media.kind) {
|
|
607
|
+
case "photo":
|
|
608
|
+
return {
|
|
609
|
+
media: {
|
|
610
|
+
oneofKind: "photo",
|
|
611
|
+
photo: {
|
|
612
|
+
photoId: asInlineId(media.photoId, "photoId"),
|
|
613
|
+
},
|
|
614
|
+
},
|
|
615
|
+
};
|
|
616
|
+
case "video":
|
|
617
|
+
return {
|
|
618
|
+
media: {
|
|
619
|
+
oneofKind: "video",
|
|
620
|
+
video: {
|
|
621
|
+
videoId: asInlineId(media.videoId, "videoId"),
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
};
|
|
625
|
+
case "document":
|
|
626
|
+
return {
|
|
627
|
+
media: {
|
|
628
|
+
oneofKind: "document",
|
|
629
|
+
document: {
|
|
630
|
+
documentId: asInlineId(media.documentId, "documentId"),
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function normalizeHttpBaseUrl(baseUrl) {
|
|
637
|
+
const url = new URL(baseUrl);
|
|
638
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
639
|
+
url.pathname = path || "/";
|
|
640
|
+
return url.toString().replace(/\/$/, "");
|
|
641
|
+
}
|
|
642
|
+
function normalizeUploadFileName(raw, type) {
|
|
643
|
+
const trimmed = raw?.trim();
|
|
644
|
+
if (trimmed)
|
|
645
|
+
return trimmed;
|
|
646
|
+
switch (type) {
|
|
647
|
+
case "photo":
|
|
648
|
+
return "photo.jpg";
|
|
649
|
+
case "video":
|
|
650
|
+
return "video.mp4";
|
|
651
|
+
case "document":
|
|
652
|
+
return "document.bin";
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
function resolveUploadContentType(type, explicit) {
|
|
656
|
+
const trimmed = explicit?.trim();
|
|
657
|
+
if (trimmed)
|
|
658
|
+
return trimmed;
|
|
659
|
+
switch (type) {
|
|
660
|
+
case "photo":
|
|
661
|
+
return "image/jpeg";
|
|
662
|
+
case "video":
|
|
663
|
+
return "video/mp4";
|
|
664
|
+
case "document":
|
|
665
|
+
return "application/octet-stream";
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function toBlob(input, type) {
|
|
669
|
+
if (input instanceof Blob) {
|
|
670
|
+
return input.type === type ? input : new Blob([input], { type });
|
|
671
|
+
}
|
|
672
|
+
return new Blob([input], { type });
|
|
673
|
+
}
|
|
674
|
+
function normalizePositiveInt(value, field) {
|
|
675
|
+
if (value == null)
|
|
676
|
+
return undefined;
|
|
677
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
|
|
678
|
+
throw new Error(`uploadFile: ${field} must be a positive integer`);
|
|
679
|
+
}
|
|
680
|
+
return value;
|
|
681
|
+
}
|
|
682
|
+
async function parseJsonResponse(response) {
|
|
683
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
684
|
+
if (!contentType.includes("application/json")) {
|
|
685
|
+
const text = await response.text();
|
|
686
|
+
return text;
|
|
687
|
+
}
|
|
688
|
+
try {
|
|
689
|
+
return await response.json();
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
function isRecord(value) {
|
|
696
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
697
|
+
}
|
|
698
|
+
function describeUploadFailure(payload) {
|
|
699
|
+
if (typeof payload === "string") {
|
|
700
|
+
const trimmed = payload.trim();
|
|
701
|
+
return trimmed ? trimmed : "";
|
|
702
|
+
}
|
|
703
|
+
if (!isRecord(payload))
|
|
704
|
+
return "";
|
|
705
|
+
const description = typeof payload.description === "string" ? payload.description.trim() : "";
|
|
706
|
+
if (description)
|
|
707
|
+
return description;
|
|
708
|
+
const error = typeof payload.error === "string" ? payload.error.trim() : "";
|
|
709
|
+
if (error)
|
|
710
|
+
return error;
|
|
711
|
+
return "";
|
|
712
|
+
}
|
|
713
|
+
function parseOptionalBigInt(value, field) {
|
|
714
|
+
if (value == null)
|
|
715
|
+
return undefined;
|
|
716
|
+
if (typeof value === "bigint")
|
|
717
|
+
return value;
|
|
718
|
+
if (typeof value === "number") {
|
|
719
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || !Number.isSafeInteger(value)) {
|
|
720
|
+
throw new Error(`uploadFile: invalid ${field} in response`);
|
|
721
|
+
}
|
|
722
|
+
return BigInt(value);
|
|
723
|
+
}
|
|
724
|
+
if (typeof value === "string") {
|
|
725
|
+
const trimmed = value.trim();
|
|
726
|
+
if (!trimmed)
|
|
727
|
+
return undefined;
|
|
728
|
+
try {
|
|
729
|
+
return BigInt(trimmed);
|
|
730
|
+
}
|
|
731
|
+
catch {
|
|
732
|
+
throw new Error(`uploadFile: invalid ${field} in response`);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
throw new Error(`uploadFile: invalid ${field} in response`);
|
|
736
|
+
}
|
|
533
737
|
const resolveRealtimeUrl = (baseUrl) => {
|
|
534
738
|
const url = new URL(baseUrl);
|
|
535
739
|
const isSecure = url.protocol === "https:";
|
package/dist/sdk/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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";
|
|
@@ -9,6 +9,56 @@ export type InlineSdkClientOptions = {
|
|
|
9
9
|
logger?: InlineSdkLogger;
|
|
10
10
|
state?: InlineSdkStateStore;
|
|
11
11
|
transport?: Transport;
|
|
12
|
+
fetch?: typeof fetch;
|
|
13
|
+
};
|
|
14
|
+
export type InlineSdkSendMessageMedia = {
|
|
15
|
+
kind: "photo";
|
|
16
|
+
photoId: InlineIdLike;
|
|
17
|
+
} | {
|
|
18
|
+
kind: "video";
|
|
19
|
+
videoId: InlineIdLike;
|
|
20
|
+
} | {
|
|
21
|
+
kind: "document";
|
|
22
|
+
documentId: InlineIdLike;
|
|
23
|
+
};
|
|
24
|
+
export type InlineSdkSendMessageParams = {
|
|
25
|
+
chatId: InlineIdLike;
|
|
26
|
+
userId?: never;
|
|
27
|
+
text?: string;
|
|
28
|
+
media?: InlineSdkSendMessageMedia;
|
|
29
|
+
replyToMsgId?: InlineIdLike;
|
|
30
|
+
parseMarkdown?: boolean;
|
|
31
|
+
sendMode?: "silent";
|
|
32
|
+
entities?: MessageEntities;
|
|
33
|
+
} | {
|
|
34
|
+
userId: InlineIdLike;
|
|
35
|
+
chatId?: never;
|
|
36
|
+
text?: string;
|
|
37
|
+
media?: InlineSdkSendMessageMedia;
|
|
38
|
+
replyToMsgId?: InlineIdLike;
|
|
39
|
+
parseMarkdown?: boolean;
|
|
40
|
+
sendMode?: "silent";
|
|
41
|
+
entities?: MessageEntities;
|
|
42
|
+
};
|
|
43
|
+
export type InlineSdkBinaryInput = Blob | Uint8Array | ArrayBuffer | SharedArrayBuffer;
|
|
44
|
+
export type InlineSdkUploadFileType = "photo" | "video" | "document";
|
|
45
|
+
export type InlineSdkUploadFileParams = {
|
|
46
|
+
type: InlineSdkUploadFileType;
|
|
47
|
+
file: InlineSdkBinaryInput;
|
|
48
|
+
fileName?: string;
|
|
49
|
+
contentType?: string;
|
|
50
|
+
thumbnail?: InlineSdkBinaryInput;
|
|
51
|
+
thumbnailFileName?: string;
|
|
52
|
+
thumbnailContentType?: string;
|
|
53
|
+
width?: number;
|
|
54
|
+
height?: number;
|
|
55
|
+
duration?: number;
|
|
56
|
+
};
|
|
57
|
+
export type InlineSdkUploadFileResult = {
|
|
58
|
+
fileUniqueId: string;
|
|
59
|
+
photoId?: bigint;
|
|
60
|
+
videoId?: bigint;
|
|
61
|
+
documentId?: bigint;
|
|
12
62
|
};
|
|
13
63
|
export type InlineInboundEvent = {
|
|
14
64
|
kind: "message.new";
|