diff --git a/src/api/lark.ts b/src/api/lark.ts new file mode 100644 index 0000000..b6c517c --- /dev/null +++ b/src/api/lark.ts @@ -0,0 +1,41 @@ +import { generateSignature } from "../utils/signature-util" + +export async function sendMessage(balance: number, env: Env) { + const larkBotId = env.LARK_BOT_ID + const larkSignKey = env.LARK_SIGN_KEY + + if (!larkBotId) { + throw new Error("LARK_BOT_ID not set.") + } + + const url = `https://open.larksuite.com/open-apis/bot/v2/hook/${larkBotId}` + + const timestamp = Math.floor(Date.now() / 1000) + console.log(`timestamp = ${timestamp}`) + const signature = await generateSignature(timestamp, larkSignKey) + + const data = { + msg_type: "text", + content: { + text: `Your UniAPI balance: US$ ${balance}`, + }, + timestamp: timestamp, + sign: signature, + } + + console.log(data) + + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json; charset=UTF-8" }, + body: JSON.stringify(data) + }) + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Request failed: ${response.status} ${response.statusText}, ${text}`); + } + + const result = await response.json(); + console.log("Lark message send response:", result); +} diff --git a/src/api/uniapi.ts b/src/api/uniapi.ts new file mode 100644 index 0000000..f074b87 --- /dev/null +++ b/src/api/uniapi.ts @@ -0,0 +1,24 @@ +import { UniApiBalanceResponse } from "../types/uniapi-balance-response" + +export async function getBalance(env: Env): Promise { + const response = await fetch("https://api.uniapi.io/v1/billing/usage?unit=usd", { + headers: { + Authorization: `Bearer ${env.UNIAPI_TOKEN}`, + }, + }) + + if (!response.ok) { + console.error(`Request failed, response status: ${response.status}`) + throw Error(`Request failed, response status: ${response.status}`) + } + + const result = await response.json() + const balance = result?.data?.balance + + if (balance === undefined) { + console.error("No field balance.") + throw Error("No field balance.") + } + + return balance +} diff --git a/src/index.ts b/src/index.ts index 216259c..e6b7870 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,22 +1,18 @@ -/** - * 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 - * - * Bind resources to your worker in `wrangler.jsonc`. After adding bindings, a type definition for the - * `Env` object can be regenerated with `npm run cf-typegen`. - * - * Learn more at https://developers.cloudflare.com/workers/ - */ +import { getBalance } from "./api/uniapi" +import { sendMessage } from "./api/lark" + +async function scheduleController(env: Env) { + // fetch balance from `UniAPI` + const balance = await getBalance(env) + console.log(`balance = ${balance}`) + + // send message + await sendMessage(balance, env) + console.log("Message has sent.") +} export default { - async fetch(request, env, ctx): Promise { - const uniApiToken = env.UNIAPI_TOKEN; - const larkBotId = env.LARK_BOT_ID; - const larkSignKey = env.LARK_SIGN_KEY; - - return new Response("Hello World!") + async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext) { + await scheduleController(env) }, } satisfies ExportedHandler diff --git a/src/types/uniapi-balance-response.ts b/src/types/uniapi-balance-response.ts new file mode 100644 index 0000000..dca1337 --- /dev/null +++ b/src/types/uniapi-balance-response.ts @@ -0,0 +1,10 @@ +interface ResponseData { + balance: number + used: number + cache_used: number +} + +export interface UniApiBalanceResponse { + data: ResponseData + success: boolean +} diff --git a/src/utils/signature-util.ts b/src/utils/signature-util.ts new file mode 100644 index 0000000..1d306ce --- /dev/null +++ b/src/utils/signature-util.ts @@ -0,0 +1,33 @@ +export async function generateSignature(timestamp: number, signKey: string): Promise { + const stringToSign = `${timestamp}\n${signKey}` + + const encoder = new TextEncoder() + + const keyData = encoder.encode(stringToSign) + const messageData = new Uint8Array(0) + + const cryptoKey = await crypto.subtle.importKey( + "raw", + keyData, + { + name: "HMAC", + hash: "SHA-256", + }, + false, + ["sign"], + ) + + const signatureBuffer = await crypto.subtle.sign("HMAC", cryptoKey, messageData) + + return bufferToBase64(signatureBuffer) +} + +function bufferToBase64(buffer: ArrayBuffer): string { + let binary = "" + const bytes = new Uint8Array(buffer) + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]) + } + + return btoa(binary) +} diff --git a/test/index.spec.ts b/test/index.spec.ts index 5197296..b1ea2da 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -1,24 +1,15 @@ -import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test'; -import { describe, it, expect } from 'vitest'; -import worker from '../src/index'; +import { env, createExecutionContext, waitOnExecutionContext, SELF } from "cloudflare:test" +import { describe, it, expect } from "vitest" +import worker from "../src/index" +import { generateSignature } from "../src/utils/signature-util" // For now, you'll need to do something like this to get a correctly-typed // `Request` to pass to `worker.fetch()`. -const IncomingRequest = Request; +const IncomingRequest = Request -describe('Hello World worker', () => { - it('responds with Hello World! (unit style)', async () => { - const request = new IncomingRequest('http://example.com'); - // Create an empty context to pass to `worker.fetch()`. - const ctx = createExecutionContext(); - const response = await worker.fetch(request, env, ctx); - // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions - await waitOnExecutionContext(ctx); - expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`); - }); - - it('responds with Hello World! (integration style)', async () => { - const response = await SELF.fetch('https://example.com'); - expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`); - }); -}); +describe("Hello World worker", () => { + it("responds with Hello World! (integration style)", async () => { + const response = await SELF.fetch("https://example.com") + expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`) + }) +})