@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 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 { MessageEntities, Method, type Peer, type RpcCall, type RpcResult } from "@inline-chat/protocol/core";
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: SendMessageTarget & {
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, MessageEntities, MessageSendMode, Method, UpdateBucket, UpdateComposeAction_ComposeAction, } from "@inline-chat/protocol/core";
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
- const baseUrl = options.baseUrl ?? "https://api.inline.chat";
39
- const url = resolveRealtimeUrl(baseUrl);
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:";
@@ -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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inline-chat/realtime-sdk",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "files": [