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