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 @@
|
|||||||
/**
|
import { getBalance } from "./api/uniapi"
|
||||||
* Welcome to Cloudflare Workers! This is your first worker.
|
import { sendMessage } from "./api/lark"
|
||||||
*
|
|
||||||
* - Run `npm run dev` in your terminal to start a development server
|
async function scheduleController(env: Env) {
|
||||||
* - Open a browser tab at http://localhost:8787/ to see your worker in action
|
// fetch balance from `UniAPI`
|
||||||
* - Run `npm run deploy` to publish your worker
|
const balance = await getBalance(env)
|
||||||
*
|
console.log(`balance = ${balance}`)
|
||||||
* 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`.
|
// send message
|
||||||
*
|
await sendMessage(balance, env)
|
||||||
* Learn more at https://developers.cloudflare.com/workers/
|
console.log("Message has sent.")
|
||||||
*/
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async fetch(request, env, ctx): Promise<Response> {
|
async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext) {
|
||||||
const uniApiToken = env.UNIAPI_TOKEN;
|
await scheduleController(env)
|
||||||
const larkBotId = env.LARK_BOT_ID;
|
|
||||||
const larkSignKey = env.LARK_SIGN_KEY;
|
|
||||||
|
|
||||||
return new Response("Hello World!")
|
|
||||||
},
|
},
|
||||||
} satisfies ExportedHandler<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 { env, createExecutionContext, waitOnExecutionContext, SELF } from "cloudflare:test"
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from "vitest"
|
||||||
import worker from '../src/index';
|
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
|
// For now, you'll need to do something like this to get a correctly-typed
|
||||||
// `Request` to pass to `worker.fetch()`.
|
// `Request` to pass to `worker.fetch()`.
|
||||||
const IncomingRequest = Request<unknown, IncomingRequestCfProperties>;
|
const IncomingRequest = Request<unknown, IncomingRequestCfProperties>
|
||||||
|
|
||||||
describe('Hello World worker', () => {
|
describe("Hello World worker", () => {
|
||||||
it('responds with Hello World! (unit style)', async () => {
|
it("responds with Hello World! (integration style)", async () => {
|
||||||
const request = new IncomingRequest('http://example.com');
|
const response = await SELF.fetch("https://example.com")
|
||||||
// Create an empty context to pass to `worker.fetch()`.
|
expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`)
|
||||||
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!"`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in new issue
Block a user