From fe8272e0c5c4b9db4bfb1684f7c0417e1da5617c Mon Sep 17 00:00:00 2001 From: Paul Pan Date: Sat, 2 Dec 2023 22:06:24 +0800 Subject: [PATCH] basic functions --- .editorconfig | 13 ---- .idea/.gitignore | 5 ++ .idea/codeStyles/Project.xml | 65 ++++++++++++++++++ .idea/codeStyles/codeStyleConfig.xml | 5 ++ .idea/forward-air.iml | 13 ++++ .idea/inspectionProfiles/Project_Default.xml | 12 ++++ .idea/modules.xml | 8 +++ .idea/prettier.xml | 7 ++ .idea/vcs.xml | 6 ++ .prettierrc | 6 -- src/.prettierrc | 8 +++ src/func/hook.ts | 42 ++++++++++++ src/func/push.ts | 24 +++++++ src/index.ts | 51 +++++++-------- src/lib/bot.ts | 69 ++++++++++++++++++++ src/lib/status.ts | 6 ++ src/lib/uuid.ts | 9 +++ src/lib/wrapper.ts | 20 ++++++ wrangler.toml | 12 ++-- 19 files changed, 329 insertions(+), 52 deletions(-) delete mode 100644 .editorconfig create mode 100644 .idea/.gitignore create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/forward-air.iml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/prettier.xml create mode 100644 .idea/vcs.xml delete mode 100644 .prettierrc create mode 100644 src/.prettierrc create mode 100644 src/func/hook.ts create mode 100644 src/func/push.ts create mode 100644 src/lib/bot.ts create mode 100644 src/lib/status.ts create mode 100644 src/lib/uuid.ts create mode 100644 src/lib/wrapper.ts diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 64ab260..0000000 --- a/.editorconfig +++ /dev/null @@ -1,13 +0,0 @@ -# http://editorconfig.org -root = true - -[*] -indent_style = tab -tab_width = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.yml] -indent_style = space diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..892b3e3 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/forward-air.iml b/.idea/forward-air.iml new file mode 100644 index 0000000..8c13ba2 --- /dev/null +++ b/.idea/forward-air.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..0a6464d --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..a24199e --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/prettier.xml b/.idea/prettier.xml new file mode 100644 index 0000000..0c83ac4 --- /dev/null +++ b/.idea/prettier.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 5c7b5d3..0000000 --- a/.prettierrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "printWidth": 140, - "singleQuote": true, - "semi": true, - "useTabs": true -} diff --git a/src/.prettierrc b/src/.prettierrc new file mode 100644 index 0000000..5170861 --- /dev/null +++ b/src/.prettierrc @@ -0,0 +1,8 @@ +{ + "endOfLine": "lf", + "printWidth": 140, + "semi": true, + "singleQuote": true, + "tabWidth": 4, + "trailingComma": "es5" +} diff --git a/src/func/hook.ts b/src/func/hook.ts new file mode 100644 index 0000000..074c2d1 --- /dev/null +++ b/src/func/hook.ts @@ -0,0 +1,42 @@ +import { Bot, Message } from '../lib/bot'; +import { Env } from '../index'; +import { IsValidUUID } from '../lib/uuid'; +import { Status } from '../lib/status'; +import { Wrap } from '../lib/wrapper'; + +export async function FuncHook(request: Request, env: Env, ctx: ExecutionContext): Promise { + if (request.method !== 'POST') return Wrap(Status.BadRequest, 'Only POST method is allowed'); + + const token = request.headers.get('X-Telegram-Bot-Api-Secret-Token'); + if (token !== env.TG_HOOK_TOKEN) return Wrap(Status.BadRequest, 'Invalid Token'); + + const message = ((await request.json()) as Message).message; + if (message.from.is_bot || message.chat.type !== 'private') return Wrap(Status.OK, 'Only private from user is allowed'); + + const chat_id = message.chat.id; + const text = message.text; + + console.debug(`[Hook] ${chat_id} ${text}`); + + if (text.startsWith('BIND:')) { + const uuid = text.split(':')[1]; + if (!IsValidUUID(uuid)) return Wrap(Status.BadRequest, 'Invalid UUID'); + await env.KV.put(uuid, chat_id.toString()); + await new Bot(env.TG_BOT_TOKEN).send(chat_id, '[BOT] Bind Success'); + return Wrap(Status.OK, 'Bind successfully'); + } + + if (text.startsWith('UNBIND:')) { + const uuid = text.split(':')[1]; + if (!IsValidUUID(uuid)) return Wrap(Status.BadRequest, 'Invalid UUID'); + await env.KV.delete(uuid); + await new Bot(env.TG_BOT_TOKEN).send(chat_id, '[BOT] Unbind Success'); + return Wrap(Status.OK, 'Unbind successfully'); + } + + // TODO: Handle "SEND:" command + + // TODO: Handle Cited Message (Text or Voice) with command + + return Wrap(Status.OK, 'Nothing Executed'); +} diff --git a/src/func/push.ts b/src/func/push.ts new file mode 100644 index 0000000..de68233 --- /dev/null +++ b/src/func/push.ts @@ -0,0 +1,24 @@ +import { Bot } from '../lib/bot'; +import { Env } from '../index'; +import { IsValidUUID } from '../lib/uuid'; +import { Status } from '../lib/status'; +import { Wrap } from '../lib/wrapper'; + +export async function FuncPush(request: Request, env: Env, ctx: ExecutionContext): Promise { + if (request.method !== 'POST') return Wrap(Status.BadRequest, 'Only POST method is allowed'); + + const token = request.headers.get('X-Auth-Token'); + if (!token) return Wrap(Status.BadRequest, 'Missing X-Auth-Token header'); + if (!IsValidUUID(token)) return Wrap(Status.BadRequest, 'Invalid Token'); + + const chat_id = await env.KV.get(token); + if (!chat_id) return Wrap(Status.BadRequest, 'Invalid Token'); + + const bot = new Bot(env.TG_BOT_TOKEN); + const message = await request.text(); + await bot.send(+chat_id, message); + + console.debug(`[Push] ${chat_id} ${message}`); + + return Wrap(Status.OK, null); +} diff --git a/src/index.ts b/src/index.ts index 8087b85..9bf8007 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,32 +1,29 @@ -/** - * Welcome to Cloudflare Workers! This is your first worker. - * - * - Run `npm run dev` in your terminal to start a development server - * - Open a browser tab at http://localhost:8787/ to see your worker in action - * - Run `npm run deploy` to publish your worker - * - * Learn more at https://developers.cloudflare.com/workers/ - */ +import { NotFound } from './lib/wrapper'; +import { FuncPush } from './func/push'; +import { FuncHook } from './func/hook'; export interface Env { - // Example binding to KV. Learn more at https://developers.cloudflare.com/workers/runtime-apis/kv/ - // MY_KV_NAMESPACE: KVNamespace; - // - // Example binding to Durable Object. Learn more at https://developers.cloudflare.com/workers/runtime-apis/durable-objects/ - // MY_DURABLE_OBJECT: DurableObjectNamespace; - // - // Example binding to R2. Learn more at https://developers.cloudflare.com/workers/runtime-apis/r2/ - // MY_BUCKET: R2Bucket; - // - // Example binding to a Service. Learn more at https://developers.cloudflare.com/workers/runtime-apis/service-bindings/ - // MY_SERVICE: Fetcher; - // - // Example binding to a Queue. Learn more at https://developers.cloudflare.com/queues/javascript-apis/ - // MY_QUEUE: Queue; + KV: KVNamespace; + BUCKET: R2Bucket; + TG_BOT_TOKEN: string; + TG_HOOK_TOKEN: string; } -export default { - async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { - return new Response('Hello World!'); - }, +export type Handler = (request: Request, env: Env, ctx: ExecutionContext) => Promise; + +const routes: { [page: string]: Handler } = { + '/push': FuncPush, + '/hook': FuncHook, +}; + +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + const { pathname } = new URL(request.url); + for (const route in routes) { + if (pathname === route) { + return routes[route](request, env, ctx); + } + } + return NotFound(); + }, }; diff --git a/src/lib/bot.ts b/src/lib/bot.ts new file mode 100644 index 0000000..857ab80 --- /dev/null +++ b/src/lib/bot.ts @@ -0,0 +1,69 @@ +// import { console } from '@cloudflare/workers-types'; + +export interface Message { + update_id: number; + message: { + message_id: number; + from: { + id: number; + is_bot: boolean; + username: string; + }; + chat: { + id: number; + username: string; + type: string; + }; + date: number; + text: string; + }; +} + +export class Bot { + readonly token: string = ''; + + constructor(token: string) { + this.token = token; + } + + async fetch(): Promise { + const url = `https://api.telegram.org/bot${this.token}/getUpdates`; + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + console.debug(`Fetch message response: ${response.status} ${response.body}`); + + const body = (await response.json()) as { ok: boolean; result: Message[] }; + + if (response.ok && body.ok) return body.result; + else return Promise.reject(`Fetch message failed`); + } + + async send(chat_id: number, message: string): Promise { + console.log(`Message sent to ${chat_id}: ${message}`); + + const url = `https://api.telegram.org/bot${this.token}/sendMessage`; + const body = { + chat_id: chat_id, + text: message, + }; + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + console.debug(`Send message response: ${response.status} ${response.body}`); + + if (response.ok) return Promise.resolve(); + else return Promise.reject(`Send message failed`); + } +} diff --git a/src/lib/status.ts b/src/lib/status.ts new file mode 100644 index 0000000..b566f66 --- /dev/null +++ b/src/lib/status.ts @@ -0,0 +1,6 @@ +export const enum Status { + OK = 200, + BadRequest = 400, + NotFound = 404, + InternalServerError = 500, +} diff --git a/src/lib/uuid.ts b/src/lib/uuid.ts new file mode 100644 index 0000000..a0608b0 --- /dev/null +++ b/src/lib/uuid.ts @@ -0,0 +1,9 @@ +export function GenUUID(): string { + return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => + (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16) + ); +} + +export function IsValidUUID(uuid: string): boolean { + return uuid.match(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i) !== null; +} diff --git a/src/lib/wrapper.ts b/src/lib/wrapper.ts new file mode 100644 index 0000000..6a722c4 --- /dev/null +++ b/src/lib/wrapper.ts @@ -0,0 +1,20 @@ +import { Status } from './status'; + +export async function Wrap(status: Status, body: any): Promise { + return new Response( + JSON.stringify({ + code: status, + body: status == Status.OK ? body : null, + message: status == Status.OK ? null : body, + }), + { + headers: { + 'Content-Type': 'application/json', + }, + } + ); +} + +export async function NotFound(): Promise { + return Wrap(Status.NotFound, 'Not Found'); +} diff --git a/wrangler.toml b/wrangler.toml index 11ff0e1..57101ac 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -10,15 +10,15 @@ compatibility_date = "2023-11-21" # Bind a KV Namespace. Use KV as persistent storage for small key-value pairs. # Docs: https://developers.cloudflare.com/workers/runtime-apis/kv -# [[kv_namespaces]] -# binding = "MY_KV_NAMESPACE" -# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +[[kv_namespaces]] +binding = "KV" +id = "b1d40d195c9b4bc8ab53d94e15564faa" # Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files. # Docs: https://developers.cloudflare.com/r2/api/workers/workers-api-usage/ -# [[r2_buckets]] -# binding = "MY_BUCKET" -# bucket_name = "my-bucket" +[[r2_buckets]] +binding = "BUCKET" +bucket_name = "forward-air" # Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer. # Docs: https://developers.cloudflare.com/queues/get-started