import type { Api } from "@effect-ak/tg-bot-api" import type { TgClientConfig } from "./client" // --- result --- export type ClientErrorReason = | { _tag: "NotOkResponse"; errorCode?: number; details?: string } | { _tag: "UnexpectedResponse"; response: unknown } | { _tag: "ClientInternalError"; cause: unknown } | { _tag: "UnableToGetFile"; cause: unknown } | { _tag: "BotHandlerError"; cause: unknown } | { _tag: "NotJsonResponse"; response: unknown } export type ClientResult = | { readonly ok: true; readonly data: T } | { readonly ok: false; readonly error: ClientErrorReason } // --- guards --- export interface FileContent { file_content: Uint8Array file_name: string } const isFileContent = (input: unknown): input is FileContent => typeof input == "object" && input != null && "file_content" in input && input.file_content instanceof Uint8Array && "file_name" in input && typeof input.file_name == "string" interface TgBotApiResponseSchema { ok: boolean error_code?: number description?: string result?: unknown } const isTgBotApiResponse = ( input: unknown ): input is TgBotApiResponseSchema => typeof input == "object" && input != null && "ok" in input && typeof input.ok == "boolean" // --- message effects --- export const MESSAGE_EFFECTS = { "🔥": "5104841245755180586", "👍": "5107584321108051014", "👎": "5104858069142078462", "❤️": "5159385139981059251", "🎉": "5046509860389126442", "💩": "5046589136895476101" } as const export type MessageEffect = keyof typeof MESSAGE_EFFECTS export const messageEffectIdCodes = Object.keys( MESSAGE_EFFECTS ) as MessageEffect[] const isMessageEffect = (input: unknown): input is MessageEffect => { return typeof input === "string" && input in MESSAGE_EFFECTS } // --- execute --- const snakeToCamel = (str: string): string => { return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()) } type WithMessageEffect = T extends { message_effect_id?: string } ? Omit & { message_effect_id?: MessageEffect | (string & {}) } : T export type ExecuteMethod = ( method: M, input: WithMessageEffect[0]> ) => Promise>> export async function executeTgBotMethod( params: { config: Required method: M input: Parameters[0] } ): Promise>> { const { config, method } = params let { input } = params if ("message_effect_id" in input && isMessageEffect(input.message_effect_id)) { input = { ...input, message_effect_id: MESSAGE_EFFECTS[input.message_effect_id] } } let httpResponse: Response try { httpResponse = await fetch( `${config.base_url}/bot${config.bot_token}/${snakeToCamel(method)}`, { body: makePayload(input) ?? null, method: "POST" } ) } catch (cause) { return { ok: false, error: { _tag: "ClientInternalError", cause } } } let response: unknown try { response = await httpResponse.json() } catch { return { ok: false, error: { _tag: "NotJsonResponse", response: httpResponse } } } if (!isTgBotApiResponse(response)) { return { ok: false, error: { _tag: "UnexpectedResponse", response } } } if (!httpResponse.ok) { return { ok: false, error: { _tag: "NotOkResponse", ...(response.error_code ? { errorCode: response.error_code } : {}), ...(response.description ? { details: response.description } : {}) } } } return { ok: true, data: response.result as ReturnType } } export const makePayload = (body: object): FormData | undefined => { const entries = Object.entries(body) if (entries.length == 0) return undefined const result = new FormData() for (const [key, value] of entries) { if (!value) continue if (typeof value != "object") { result.append(key, `${value}`) } else if (isFileContent(value)) { result.append(key, new Blob([value.file_content]), value.file_name) } else { result.append(key, JSON.stringify(value)) } } return result }