basic functions

This commit is contained in:
Paul Pan 2023-12-02 22:06:24 +08:00
parent 2c4443bb26
commit fe8272e0c5
19 changed files with 329 additions and 52 deletions

View File

@ -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

5
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

View File

@ -0,0 +1,65 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
<option name="HTML_QUOTE_STYLE" value="Single" />
<option name="HTML_ENFORCE_QUOTES" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="USE_DOUBLE_QUOTES" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="140" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="140" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="140" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="140" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

13
.idea/forward-air.iml Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
<excludeFolder url="file://$MODULE_DIR$/.wrangler" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,12 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="IncorrectHttpHeaderInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="customHeaders">
<set>
<option value="X-Auth-Token" />
</set>
</option>
</inspection_tool>
</profile>
</component>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/forward-air.iml" filepath="$PROJECT_DIR$/.idea/forward-air.iml" />
</modules>
</component>
</project>

7
.idea/prettier.xml Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myConfigurationMode" value="AUTOMATIC" />
<option name="myRunOnSave" value="true" />
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -1,6 +0,0 @@
{
"printWidth": 140,
"singleQuote": true,
"semi": true,
"useTabs": true
}

8
src/.prettierrc Normal file
View File

@ -0,0 +1,8 @@
{
"endOfLine": "lf",
"printWidth": 140,
"semi": true,
"singleQuote": true,
"tabWidth": 4,
"trailingComma": "es5"
}

42
src/func/hook.ts Normal file
View File

@ -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<Response> {
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');
}

24
src/func/push.ts Normal file
View File

@ -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<Response> {
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);
}

View File

@ -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 type Handler = (request: Request, env: Env, ctx: ExecutionContext) => Promise<Response>;
const routes: { [page: string]: Handler } = {
'/push': FuncPush,
'/hook': FuncHook,
};
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
return new Response('Hello World!');
const { pathname } = new URL(request.url);
for (const route in routes) {
if (pathname === route) {
return routes[route](request, env, ctx);
}
}
return NotFound();
},
};

69
src/lib/bot.ts Normal file
View File

@ -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<Message[]> {
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<void> {
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`);
}
}

6
src/lib/status.ts Normal file
View File

@ -0,0 +1,6 @@
export const enum Status {
OK = 200,
BadRequest = 400,
NotFound = 404,
InternalServerError = 500,
}

9
src/lib/uuid.ts Normal file
View File

@ -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;
}

20
src/lib/wrapper.ts Normal file
View File

@ -0,0 +1,20 @@
import { Status } from './status';
export async function Wrap(status: Status, body: any): Promise<Response> {
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<Response> {
return Wrap(Status.NotFound, 'Not Found');
}

View File

@ -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