feat: send balance reminder message to lark
This commit is contained in:
6 files changed
+133
-38
No files matched your search
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { UniApiBalanceResponse } from "../types/uniapi-balance-response"
|
||||
|
||||
export async function getBalance(env: Env): Promise<number> {
|
||||
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<UniApiBalanceResponse>()
|
||||
const balance = result?.data?.balance
|
||||
|
||||
if (balance === undefined) {
|
||||
console.error("No field balance.")
|
||||
throw Error("No field balance.")
|
||||
}
|
||||
|
||||
return balance
|
||||
}
|
||||
+14
-18
@@ -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<Response> {
|
||||
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<Env>
|
||||
@@ -0,0 +1,10 @@
|
||||
interface ResponseData {
|
||||
balance: number
|
||||
used: number
|
||||
cache_used: number
|
||||
}
|
||||
|
||||
export interface UniApiBalanceResponse {
|
||||
data: ResponseData
|
||||
success: boolean
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
export async function generateSignature(timestamp: number, signKey: string): Promise<string> {
|
||||
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)
|
||||
}
|
||||
+11
-20
@@ -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<unknown, IncomingRequestCfProperties>;
|
||||
const IncomingRequest = Request<unknown, IncomingRequestCfProperties>
|
||||
|
||||
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!"`)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user