rubika 1.0.0

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.
Files changed (54) hide show
  1. package/README.md +54 -0
  2. package/package.json +19 -0
  3. package/src/bot.ts +197 -0
  4. package/src/filters.ts +92 -0
  5. package/src/index.ts +8 -0
  6. package/src/methods/advanced/builder.ts +23 -0
  7. package/src/methods/advanced/index.ts +3 -0
  8. package/src/methods/bot/getMe.ts +8 -0
  9. package/src/methods/bot/index.ts +3 -0
  10. package/src/methods/chat/getChat.ts +7 -0
  11. package/src/methods/chat/index.ts +3 -0
  12. package/src/methods/files/_sendFile.ts +47 -0
  13. package/src/methods/files/getFile.ts +7 -0
  14. package/src/methods/files/index.ts +6 -0
  15. package/src/methods/files/requestSendFile.ts +8 -0
  16. package/src/methods/files/uploadFile.ts +88 -0
  17. package/src/methods/index.ts +223 -0
  18. package/src/methods/messages/deleteMessage.ts +14 -0
  19. package/src/methods/messages/editChatKeypad.ts +20 -0
  20. package/src/methods/messages/editMessageKeypad.ts +19 -0
  21. package/src/methods/messages/editMessageText.ts +16 -0
  22. package/src/methods/messages/forwardMessage.ts +18 -0
  23. package/src/methods/messages/index.ts +36 -0
  24. package/src/methods/messages/sendContact.ts +40 -0
  25. package/src/methods/messages/sendFile.ts +30 -0
  26. package/src/methods/messages/sendGif.ts +30 -0
  27. package/src/methods/messages/sendImage.ts +30 -0
  28. package/src/methods/messages/sendLocation.ts +38 -0
  29. package/src/methods/messages/sendMessage.ts +41 -0
  30. package/src/methods/messages/sendMusic.ts +30 -0
  31. package/src/methods/messages/sendPoll.ts +12 -0
  32. package/src/methods/messages/sendSticker.ts +38 -0
  33. package/src/methods/messages/sendVideo.ts +30 -0
  34. package/src/methods/messages/sendVoice.ts +30 -0
  35. package/src/methods/settings/index.ts +4 -0
  36. package/src/methods/settings/setCommands.ts +9 -0
  37. package/src/methods/settings/updateBotEndpoints.ts +15 -0
  38. package/src/methods/utilities/getUpdates.ts +7 -0
  39. package/src/methods/utilities/handleUpdates.ts +49 -0
  40. package/src/methods/utilities/index.ts +7 -0
  41. package/src/methods/utilities/polling.ts +63 -0
  42. package/src/methods/utilities/run.ts +21 -0
  43. package/src/methods/utilities/start.ts +23 -0
  44. package/src/methods/utilities/webhook.ts +55 -0
  45. package/src/network.ts +46 -0
  46. package/src/types/enums.ts +125 -0
  47. package/src/types/handlers.ts +16 -0
  48. package/src/types/interfaces.ts +263 -0
  49. package/src/types/methods.ts +21 -0
  50. package/src/types/utils.ts +9 -0
  51. package/src/utils/checkFilter.ts +19 -0
  52. package/src/utils/parser.ts +127 -0
  53. package/src/utils/prompt.ts +29 -0
  54. package/src/utils.ts +13 -0
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ <p align="center">
2
+ <h1 style="color:#fff;">› rubika-bot Framework</h1>
3
+ <p><b>فریم‌ورک قدرتمند و پرسرعت جاوااسکریپت برای ربات‌های روبیکا</b></p>
4
+ </p>
5
+
6
+ ---
7
+
8
+ ## › معرفی rubika-bot
9
+
10
+ **rubika-bot** یک فریم‌ورک سبک، مدرن و کاملاً **غیرهمزمان** برای توسعه ربات‌های روبیکا است.
11
+ با معماری **Filter-Base** و **Type-Safe**، شما می‌توانید:
12
+
13
+ - ربات‌هایی سریع و کم‌حجم بسازید
14
+ - سیستم فرمان‌دهی و مدیریت پیام حرفه‌ای داشته باشید
15
+ - با قابلیت **اسکِیبل و قابل گسترش**، پروژه‌های بزرگ هم مدیریت کنید
16
+
17
+ ---
18
+
19
+ ## › نصب و شروع سریع
20
+
21
+ ```bash
22
+ bun add rubika-bot
23
+ ```
24
+
25
+ ## › نمونه کد ساده
26
+
27
+ ```js
28
+ const { Bot, Filters } = require("rubika-bot");
29
+
30
+ const bot = new Bot("rubika-bot");
31
+
32
+ bot.command("/start", async (ctx, bot) => {
33
+ await bot.sendMessage(ctx.chat_id, "🤖 ربات استارت شد");
34
+ });
35
+
36
+ bot.on("message", [Filters.isText], async (ctx, bot) => {
37
+ await bot.sendMessage(ctx.chat_id, "سلام 😎");
38
+ });
39
+
40
+ bot.run();
41
+ ```
42
+
43
+ ## › ویژگی‌های کلیدی
44
+
45
+ | ویژگی | توضیح |
46
+ | -------------- | ---------------------------------------------- |
47
+ | ⚡ Super-Speed | استفاده از معماری غیرهمزمان برای پاسخ‌دهی سریع |
48
+ | 🛡️ Type-Safe | کمک به جلوگیری از خطاهای متداول جاوااسکریپت |
49
+ | 🔧 Filter-Base | سیستم فیلترینگ قدرتمند برای مدیریت پیام‌ها |
50
+ | 📂 Modular | ساختار ماژولار و قابل گسترش |
51
+
52
+ ## 🌐 لینک‌های مهم
53
+
54
+ - [لینک گیتهاب](https://github.com/hadi-rostami/rubika-bot)
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "rubika",
3
+ "version": "1.0.0",
4
+ "author": "Hadi Rostami",
5
+ "main": "src/index.ts",
6
+ "description": "A modern library for Rubika built with Bun.",
7
+ "files": [
8
+ "src/",
9
+ "README.md"
10
+ ],
11
+ "keywords": [
12
+ "javascript",
13
+ "Rubika",
14
+ "bun",
15
+ "rubika"
16
+ ],
17
+ "license": "MIT",
18
+ "type": "module"
19
+ }
package/src/bot.ts ADDED
@@ -0,0 +1,197 @@
1
+ import Network from "./network";
2
+ import Methods from "./methods";
3
+ import type { BotInfo, Update } from "./types/interfaces";
4
+ import type { ContextMap, Handler, NestedFilter } from "./types/handlers";
5
+
6
+ class Bot extends Methods {
7
+ protected initialize: boolean = false;
8
+ protected network: Network;
9
+ public BASE_URL: string;
10
+ public bot?: BotInfo;
11
+
12
+ public handlers: {
13
+ [K in keyof ContextMap]: Handler<ContextMap[K]>[];
14
+ } = { inline: [], update: [] };
15
+
16
+ /**
17
+ * نمونه‌ای از کلاس ربات را ایجاد می‌کند.
18
+ *
19
+ * این سازنده اصلی‌ترین بخش برای شروع کار با ربات است.
20
+ * با دریافت توکن و تنظیم زمان انتظار، پایه‌ی ارتباط با ای‌پی‌ای روبیکا را فراهم می‌کند.
21
+ *
22
+ * @param token - توکن اختصاصی ربات که برای احراز هویت در ای‌پی‌ای روبیکا استفاده می‌شود.
23
+ * @param timeout - زمان انتظار برای درخواست‌ها به سرور (بر حسب میلی‌ثانیه). پیش‌فرض 10000 میلی‌ثانیه.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const bot = new Bot("your-token-here", 15000);
28
+ * ```
29
+ */
30
+ constructor(public token: string, timeout: number = 10000) {
31
+ super();
32
+ this.BASE_URL = `https://botapi.rubika.ir/v3/${token}`;
33
+ this.network = new Network(this.BASE_URL, timeout);
34
+
35
+ this.start();
36
+ }
37
+
38
+ /**
39
+ * یک رویداد (event) را ثبت می‌کند و با ورودی‌های مشخص، دستورات را اجرا می‌کند.
40
+ *
41
+ * این متد به شما امکان می‌دهد تا به ازای یک نوع خاص از رویداد (مثل پیام جدید، ورود کاربر و غیره)
42
+ * یک تابع پردازش (handler) تعیین کنید که زمان رخ دادن آن نوع از رویداد فراخوانی می‌شود.
43
+ *
44
+ * @param type - نوع رویدادی که می‌خواهید به آن پاسخ دهید (مثلاً "inline"، "update" و غیره).
45
+ * @param handler - (اختیاری) در صورتی که فیلتر وجود داشته باشد، این تابع پردازش خواهد بود.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * bot.on("inline", (ctx) => {
50
+ * console.log("پیام جدید دریافت شد:", ctx.text);
51
+ * });
52
+ *
53
+ * bot.on("update", [filter.isText], (ctx) => {
54
+ * console.log("پیام شامل متن است:", ctx.text);
55
+ * });
56
+ * ```
57
+ */
58
+ on<T extends keyof typeof this.handlers>(
59
+ type: T,
60
+ handler: (ctx: ContextMap[T], bot: Bot) => Promise<void>
61
+ ): void;
62
+
63
+ /**
64
+ * یک رویداد (event) را ثبت می‌کند و با ورودی‌های مشخص، دستورات را اجرا می‌کند.
65
+ *
66
+ * این متد به شما امکان می‌دهد تا به ازای یک نوع خاص از رویداد (مثل پیام جدید، ورود کاربر و غیره)
67
+ * یک تابع پردازش (handler) تعیین کنید که زمان رخ دادن آن نوع از رویداد فراخوانی می‌شود.
68
+ *
69
+ * @param type - نوع رویدادی که می‌خواهید به آن پاسخ دهید (مثلاً "inline"، "update" و غیره).
70
+ * @param filters - می‌تواند چند فیلتر یا تابع پردازش باشد.
71
+ * @param handler - (اختیاری) در صورتی که فیلتر وجود داشته باشد، این تابع پردازش خواهد بود.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * bot.on("inline", (ctx) => {
76
+ * console.log("پیام جدید دریافت شد:", ctx.text);
77
+ * });
78
+ *
79
+ * bot.on("update", [filter.isText], (ctx) => {
80
+ * console.log("پیام شامل متن است:", ctx.text);
81
+ * });
82
+ * ```
83
+ */
84
+ on<T extends keyof typeof this.handlers>(
85
+ type: T,
86
+ filters: NestedFilter<ContextMap[T]>,
87
+ handler: (ctx: ContextMap[T], bot: Bot) => Promise<void>
88
+ ): void;
89
+
90
+ on<T extends keyof typeof this.handlers>(
91
+ type: T,
92
+ filtersOrHandler:
93
+ | NestedFilter<ContextMap[T]>
94
+ | ((ctx: ContextMap[T], bot: Bot) => Promise<void>),
95
+ maybeHandler?: (ctx: ContextMap[T], bot: Bot) => Promise<void>
96
+ ): void {
97
+ if (typeof filtersOrHandler === "function") {
98
+ this.handlers[type].push({
99
+ filters: [],
100
+ handler: filtersOrHandler,
101
+ });
102
+ } else if (Array.isArray(filtersOrHandler) && maybeHandler) {
103
+ this.handlers[type].push({
104
+ filters: filtersOrHandler,
105
+ handler: maybeHandler,
106
+ });
107
+ }
108
+ }
109
+
110
+ /**
111
+ * یک دستور (command) را ثبت می‌کند.
112
+ *
113
+ * این متد به شما امکان می‌دهد تا به دستوراتی با پیشوند مشخص (مثل `/start` یا `!help`) پاسخ دهید.
114
+ * اگر پیام کاربر با پیشوند مطابقت داشته باشد، تابع پردازش مربوطه فراخوانی می‌شود.
115
+ *
116
+ * @param prefix - پیشوند دستور، که می‌تواند یک رشته یا یک عبارت منظم (RegExp) باشد.
117
+ * @param handler - (اختیاری) در صورتی که فیلتر وجود داشته باشد، این تابع پردازش خواهد بود.
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * bot.command("/start", (ctx) => {
122
+ * ctx.reply("سلام! خوش اومدی.");
123
+ * });
124
+ *
125
+ * bot.command(/!help/, (ctx) => {
126
+ * ctx.reply("راهنمایی دریافت شد.");
127
+ * });
128
+ *
129
+ * bot.command("/ban", [filter.isAdmin], (ctx) => {
130
+ * // فقط ادمین می‌تونه این دستور رو اجرا کنه
131
+ * ctx.reply("کاربر مسدود شد.");
132
+ * });
133
+ * ```
134
+ */
135
+ command(
136
+ prefix: string | RegExp,
137
+ handler: (ctx: Update, bot: Bot) => Promise<void>
138
+ ): void;
139
+
140
+ /**
141
+ * یک دستور (command) را ثبت می‌کند.
142
+ *
143
+ * این متد به شما امکان می‌دهد تا به دستوراتی با پیشوند مشخص (مثل `/start` یا `!help`) پاسخ دهید.
144
+ * اگر پیام کاربر با پیشوند مطابقت داشته باشد، تابع پردازش مربوطه فراخوانی می‌شود.
145
+ *
146
+ * @param prefix - پیشوند دستور، که می‌تواند یک رشته یا یک عبارت منظم (RegExp) باشد.
147
+ * @param filters - می‌تواند چند فیلتر یا تابع پردازش باشد.
148
+ * @param handler - (اختیاری) در صورتی که فیلتر وجود داشته باشد، این تابع پردازش خواهد بود.
149
+ *
150
+ * @example
151
+ * ```ts
152
+ * bot.command("/start", (ctx) => {
153
+ * ctx.reply("سلام! خوش اومدی.");
154
+ * });
155
+ *
156
+ * bot.command(/!help/, (ctx) => {
157
+ * ctx.reply("راهنمایی دریافت شد.");
158
+ * });
159
+ *
160
+ * bot.command("/ban", [filter.isAdmin], (ctx) => {
161
+ * // فقط ادمین می‌تونه این دستور رو اجرا کنه
162
+ * ctx.reply("کاربر مسدود شد.");
163
+ * });
164
+ * ```
165
+ */
166
+ command(
167
+ prefix: string | RegExp,
168
+ filters: NestedFilter<ContextMap["update"]>,
169
+ handler: (ctx: Update, bot: Bot) => Promise<void>
170
+ ): void;
171
+
172
+ command(
173
+ prefix: string | RegExp,
174
+ filtersOrHandler:
175
+ | NestedFilter<ContextMap["update"]>
176
+ | ((ctx: Update, bot: Bot) => Promise<void>),
177
+ maybeHandler?: (ctx: Update, bot: Bot) => Promise<void>
178
+ ) {
179
+ if (typeof filtersOrHandler === "function") {
180
+ this.handlers.update.push({
181
+ filters: [],
182
+ handler: filtersOrHandler,
183
+ prefix,
184
+ });
185
+ } else if (Array.isArray(filtersOrHandler) && maybeHandler) {
186
+ this.handlers.update.push({
187
+ filters: filtersOrHandler,
188
+ handler: maybeHandler,
189
+ prefix,
190
+ });
191
+ } else {
192
+ throw new Error("Invalid arguments for command()");
193
+ }
194
+ }
195
+ }
196
+
197
+ export default Bot;
package/src/filters.ts ADDED
@@ -0,0 +1,92 @@
1
+ import { Update } from "./types/interfaces";
2
+
3
+ export default class Filters {
4
+ static findKey(message: any, key: string): any {
5
+ if (!message || typeof message !== "object") {
6
+ return undefined;
7
+ }
8
+
9
+ const messageKeys = Object.keys(message);
10
+ if (messageKeys.includes(key)) {
11
+ return message[key];
12
+ }
13
+
14
+ for (const messageKey of messageKeys) {
15
+ const value = message[messageKey];
16
+
17
+ if (Array.isArray(value)) {
18
+ for (const item of value) {
19
+ if (typeof item === "object") {
20
+ const found = Filters.findKey(item, key);
21
+ if (found !== undefined) return found;
22
+ }
23
+ }
24
+ }
25
+
26
+ if (typeof value === "object") {
27
+ const found = Filters.findKey(value, key);
28
+ if (found !== undefined) return found;
29
+ }
30
+ }
31
+
32
+ return undefined;
33
+ }
34
+
35
+ static isText(message: Update): boolean {
36
+ return !!Filters.findKey(message, "text");
37
+ }
38
+
39
+ static isLocation(message: Update): boolean {
40
+ return !!Filters.findKey(message, "location");
41
+ }
42
+
43
+ static isSticker(message: Update): boolean {
44
+ return !!Filters.findKey(message, "sticker");
45
+ }
46
+
47
+ static isForward(message: Update): boolean {
48
+ return !!Filters.findKey(message, "forwarded_from");
49
+ }
50
+
51
+ static isReply(message: Update): boolean {
52
+ return !!Filters.findKey(message, "reply_to_message_id");
53
+ }
54
+
55
+ static isContact(message: Update): boolean {
56
+ return !!Filters.findKey(message, "contact_message");
57
+ }
58
+
59
+ static isPoll(message: Update): boolean {
60
+ return !!Filters.findKey(message, "poll");
61
+ }
62
+
63
+ static isLiveLocation(message: Update): boolean {
64
+ return !!Filters.findKey(message, "live_location");
65
+ }
66
+
67
+ static isFile(message: Update): boolean {
68
+ return !!Filters.findKey(message, "file");
69
+ }
70
+
71
+ static isMention(message: Update): boolean {
72
+ return !!Filters.findKey(
73
+ message.new_message?.metadata?.meta_data_parts,
74
+ "link"
75
+ );
76
+ }
77
+
78
+ static isMarkdown(message: Update): boolean {
79
+ return !!Filters.findKey(message, "metadata");
80
+ }
81
+
82
+ static isDelete(message: Update): boolean {
83
+ return !!Filters.findKey(message, "removed_message_id");
84
+ }
85
+
86
+ static kypadID(button_id: string): (message: Update) => boolean {
87
+ return (message: Update) => {
88
+ const res = Filters.findKey(message, "button_id");
89
+ return res === button_id;
90
+ };
91
+ }
92
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ import Bot from "./bot";
2
+ import Utils from "./utils";
3
+ import Filters from "./filters";
4
+ import type * as Enums from "./types/enums";
5
+
6
+ export default Bot;
7
+ export { Bot, Filters, Utils };
8
+ export type { Enums };
@@ -0,0 +1,23 @@
1
+ import Bot from "../..";
2
+
3
+ async function builder(
4
+ this: Bot,
5
+ method: string,
6
+ input: object = {}
7
+ ): Promise<any> {
8
+ if (!this.token) {
9
+ throw new Error(
10
+ "[builder] Bot token is not set. Please provide a valid token."
11
+ );
12
+ }
13
+
14
+ const response: any = await this.network.request(method, input);
15
+
16
+ if (response?.status === "OK") {
17
+ return response.data;
18
+ }
19
+
20
+ throw new Error(`[builder] error in request ${method}:\n ${response} `);
21
+ }
22
+
23
+ export default builder;
@@ -0,0 +1,3 @@
1
+ import builder from "./builder";
2
+
3
+ export { builder };
@@ -0,0 +1,8 @@
1
+ import Bot from "../..";
2
+
3
+
4
+ async function getMe(this: Bot) {
5
+ return await this.builder("getMe", {});
6
+ }
7
+
8
+ export default getMe;
@@ -0,0 +1,3 @@
1
+ import getMe from './getMe';
2
+
3
+ export { getMe };
@@ -0,0 +1,7 @@
1
+ import Bot from '../..';
2
+
3
+ async function getChat(this: Bot, chat_id: string) {
4
+ return await this.builder('getChat', { chat_id });
5
+ }
6
+
7
+ export default getChat;
@@ -0,0 +1,3 @@
1
+ import getChat from './getChat';
2
+
3
+ export { getChat };
@@ -0,0 +1,47 @@
1
+ import Bot from "../..";
2
+ import Markdown from "../../utils/parser";
3
+ import { FileSource, SendType } from "../../types/methods";
4
+ import { InlineKeypad, Keypad } from "../../types/interfaces";
5
+ import { ChatKeypadTypeEnum, FileTypeEnum } from "../../types/enums";
6
+
7
+ async function _sendFile(
8
+ this: Bot,
9
+ chat_id: string,
10
+ file: FileSource,
11
+ text?: string,
12
+ type: FileTypeEnum = FileTypeEnum.File,
13
+ chat_keypad?: Keypad,
14
+ inline_keypad?: InlineKeypad,
15
+ disable_notification = false,
16
+ reply_to_message_id?: string,
17
+ chat_keypad_type?: ChatKeypadTypeEnum
18
+ ) {
19
+ const { upload_url } = await this.requestSendFile(type);
20
+ const {
21
+ data: { file_id },
22
+ } = await this.uploadFile(upload_url, file);
23
+
24
+ let data: SendType = {
25
+ chat_id,
26
+ file_id,
27
+ disable_notification,
28
+ };
29
+
30
+ if (text) data = { ...data, ...Markdown.toMetadata(text.trim()) };
31
+ if (chat_keypad) data.chat_keypad = chat_keypad;
32
+ if (inline_keypad) data.inline_keypad = inline_keypad;
33
+ if (chat_keypad_type) data.chat_keypad_type = chat_keypad_type;
34
+ if (reply_to_message_id) data.reply_to_message_id = reply_to_message_id;
35
+
36
+ if (chat_keypad && !chat_keypad_type) {
37
+ data.chat_keypad_type = ChatKeypadTypeEnum.New;
38
+ }
39
+
40
+ if (inline_keypad && chat_keypad_type) {
41
+ data.chat_keypad_type = ChatKeypadTypeEnum.None;
42
+ }
43
+
44
+ return await this.builder("sendFile", data);
45
+ }
46
+
47
+ export default _sendFile;
@@ -0,0 +1,7 @@
1
+ import Bot from '../..';
2
+
3
+ async function getFile(this: Bot, file_id: string) {
4
+ return await this.builder('getFile', { file_id });
5
+ }
6
+
7
+ export default getFile;
@@ -0,0 +1,6 @@
1
+ import getFile from './getFile';
2
+ import requestSendFile from './requestSendFile';
3
+ import _sendFile from './_sendFile';
4
+ import uploadFile from './uploadFile';
5
+
6
+ export { getFile, requestSendFile, uploadFile , _sendFile };
@@ -0,0 +1,8 @@
1
+ import Bot from '../..';
2
+ import { FileTypeEnum } from '../../types/enums';
3
+
4
+ async function requestSendFile(this: Bot, type: FileTypeEnum) {
5
+ return await this.builder('requestSendFile', { type });
6
+ }
7
+
8
+ export default requestSendFile;
@@ -0,0 +1,88 @@
1
+ import { fetch } from "bun";
2
+ import { basename } from "path";
3
+ import Bot from "../..";
4
+ import { FileSource } from "../../types/methods";
5
+
6
+ async function uploadFile(
7
+ this: Bot,
8
+ url: string,
9
+ source: FileSource,
10
+ filename?: string
11
+ ) {
12
+ let fileData: ArrayBuffer;
13
+ let detectedFilename: string;
14
+
15
+ if (typeof source === "string") {
16
+ if (source.startsWith("http://") || source.startsWith("https://")) {
17
+ // download file
18
+ console.log(`[ uploadFile ] Downloading from URL: ${source}`);
19
+ const res = await fetch(source);
20
+ if (!res.ok) {
21
+ throw new Error(
22
+ `Failed to download file: ${res.status} ${res.statusText}`
23
+ );
24
+ }
25
+
26
+ fileData = await res.arrayBuffer();
27
+ detectedFilename =
28
+ filename || getFilenameFromUrl(source) || "downloaded_file";
29
+ } else {
30
+ // file path
31
+ if (!Bun.file(source).size) {
32
+ throw new Error(`File not found: ${source}`);
33
+ }
34
+ fileData = await Bun.file(source).arrayBuffer();
35
+ detectedFilename = filename || basename(source);
36
+ }
37
+ } else {
38
+ // binery data
39
+ if (source instanceof Buffer) {
40
+ fileData = source.buffer.slice(
41
+ source.byteOffset,
42
+ source.byteOffset + source.byteLength
43
+ ) as ArrayBuffer;
44
+ } else if (source instanceof Uint8Array) {
45
+ fileData = source.buffer.slice(
46
+ source.byteOffset,
47
+ source.byteOffset + source.byteLength
48
+ ) as ArrayBuffer;
49
+ } else {
50
+ fileData = source;
51
+ }
52
+ detectedFilename = filename || "binary_file";
53
+ }
54
+
55
+ // FormData
56
+ const formData = new FormData();
57
+ formData.append("file", new Blob([fileData]), detectedFilename);
58
+
59
+ const res = await fetch(url, {
60
+ method: "POST",
61
+ body: formData,
62
+ });
63
+
64
+ if (!res.ok) {
65
+ const text = await res.text();
66
+ throw new Error(`HTTP ${res.status}: ${text}`);
67
+ }
68
+
69
+ const response = await res.json();
70
+
71
+ if (response.status !== "OK") {
72
+ throw new Error(`Upload failed: ${JSON.stringify(response)}`);
73
+ }
74
+
75
+ return response;
76
+ }
77
+
78
+ function getFilenameFromUrl(url: string): string | null {
79
+ try {
80
+ const path = new URL(url).pathname;
81
+ const filename = basename(path);
82
+ return filename !== "." ? filename : null;
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+
88
+ export default uploadFile;