feat: 支持根据用户组转发消息
This commit is contained in:
parent
c1a4890eec
commit
cabc23ae77
@ -7,9 +7,10 @@
|
|||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"settings": {
|
"settings": {
|
||||||
"files.autoSave": "off",
|
"files.autoSave": "afterDelay",
|
||||||
"editor.guides.bracketPairs": true,
|
"editor.guides.bracketPairs": true,
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true
|
||||||
},
|
},
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
name: Egg CI/CD
|
name: Egg CI/CD
|
||||||
on: [push]
|
on: [push]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-image:
|
build-image:
|
||||||
runs-on: mi-server
|
runs-on: mi-server
|
||||||
container: catthehacker/ubuntu:act-latest
|
container: catthehacker/ubuntu:act-latest
|
||||||
@ -22,7 +22,8 @@ jobs:
|
|||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
push: true
|
push: true
|
||||||
tags: git.yingbo.im:333/zhaoyingbo/egg_server:latest
|
tags: git.yingbo.im:333/zhaoyingbo/egg_server:${{ github.sha }}
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
needs: build-image
|
needs: build-image
|
||||||
runs-on: mi-server
|
runs-on: mi-server
|
||||||
@ -40,7 +41,7 @@ jobs:
|
|||||||
key: ${{ secrets.SERVER_KEY }}
|
key: ${{ secrets.SERVER_KEY }}
|
||||||
port: ${{ secrets.SERVER_PORT }}
|
port: ${{ secrets.SERVER_PORT }}
|
||||||
source: docker-compose.yml
|
source: docker-compose.yml
|
||||||
target: /home/yingbo/docker/egg_server
|
target: /home/deploy/docker/egg_server
|
||||||
# 登录服务器,执行docker-compose命令
|
# 登录服务器,执行docker-compose命令
|
||||||
- name: Login to the server and execute docker-compose command
|
- name: Login to the server and execute docker-compose command
|
||||||
uses: appleboy/ssh-action@master
|
uses: appleboy/ssh-action@master
|
||||||
@ -51,6 +52,6 @@ jobs:
|
|||||||
port: ${{ secrets.SERVER_PORT }}
|
port: ${{ secrets.SERVER_PORT }}
|
||||||
script: |
|
script: |
|
||||||
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} git.yingbo.im:333
|
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} git.yingbo.im:333
|
||||||
docker compose -f /home/yingbo/docker/egg_server/docker-compose.yml down
|
cd /home/deploy/docker/egg_server
|
||||||
docker compose -f /home/yingbo/docker/egg_server/docker-compose.yml pull
|
sed -i "s/sha/${{ github.sha }}/g" docker-compose.yml
|
||||||
docker compose -f /home/yingbo/docker/egg_server/docker-compose.yml up -d
|
docker-compose up -d --force-recreate --no-deps egg_server
|
||||||
|
9
db/index.ts
Normal file
9
db/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import messageGroup from "./messageGroup";
|
||||||
|
import tenantAccessToken from "./tenantAccessToken";
|
||||||
|
|
||||||
|
const db = {
|
||||||
|
messageGroup,
|
||||||
|
tenantAccessToken,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default db;
|
13
db/messageGroup/index.ts
Normal file
13
db/messageGroup/index.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { managePb404 } from "../../utils/pbTools";
|
||||||
|
import pbClient from "../pbClient";
|
||||||
|
|
||||||
|
const getOne = (groupId: string) =>
|
||||||
|
managePb404(
|
||||||
|
async () => await pbClient.collection("message_group").getOne(groupId)
|
||||||
|
);
|
||||||
|
|
||||||
|
const messageGroup = {
|
||||||
|
getOne,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default messageGroup;
|
14
db/messageGroup/typings.d.ts
vendored
Normal file
14
db/messageGroup/typings.d.ts
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
interface PBMessageGroup {
|
||||||
|
collectionId: string;
|
||||||
|
collectionName: string;
|
||||||
|
updated: string;
|
||||||
|
created: string;
|
||||||
|
desc: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email?: string[];
|
||||||
|
chat_id?: string[];
|
||||||
|
open_id?: string[];
|
||||||
|
union_id?: string[];
|
||||||
|
user_id?: string[];
|
||||||
|
}
|
5
db/pbClient.ts
Normal file
5
db/pbClient.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import PocketBase from 'pocketbase';
|
||||||
|
|
||||||
|
const pbClient = new PocketBase('https://eggpb.imoaix.cn')
|
||||||
|
|
||||||
|
export default pbClient;
|
28
db/tenantAccessToken/index.ts
Normal file
28
db/tenantAccessToken/index.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import pbClient from "../pbClient";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新租户的token
|
||||||
|
* @param {string} value 新的token
|
||||||
|
*/
|
||||||
|
const update = async (value: string) => {
|
||||||
|
await pbClient.collection("config").update("ugel8f0cpk0rut6", { value });
|
||||||
|
console.log("reset access token success", value);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取租户的token
|
||||||
|
* @returns {string} 租户的token
|
||||||
|
*/
|
||||||
|
const get = async () => {
|
||||||
|
const { value } = await pbClient
|
||||||
|
.collection("config")
|
||||||
|
.getOne("ugel8f0cpk0rut6");
|
||||||
|
return value as string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tenantAccessToken = {
|
||||||
|
update,
|
||||||
|
get,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default tenantAccessToken;
|
@ -1,9 +1,9 @@
|
|||||||
version: "3"
|
version: "3"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
server:
|
egg_server:
|
||||||
image: git.yingbo.im:333/zhaoyingbo/egg_server:latest
|
image: git.yingbo.im:333/zhaoyingbo/egg_server:sha
|
||||||
container_name: egg_server
|
container_name: egg_server
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3003:3000
|
||||||
|
9
index.ts
9
index.ts
@ -1,4 +1,8 @@
|
|||||||
import { manageBotReq } from "./routes/bot";
|
import { manageBotReq } from "./routes/bot";
|
||||||
|
import { manageMessageReq } from "./routes/message";
|
||||||
|
import { initSchedule } from "./schedule";
|
||||||
|
|
||||||
|
initSchedule()
|
||||||
|
|
||||||
Bun.serve({
|
Bun.serve({
|
||||||
async fetch(req) {
|
async fetch(req) {
|
||||||
@ -7,7 +11,10 @@ Bun.serve({
|
|||||||
if (url.pathname === "/") return new Response("hello, glade to see you!");
|
if (url.pathname === "/") return new Response("hello, glade to see you!");
|
||||||
// 机器人
|
// 机器人
|
||||||
if (url.pathname === '/bot') return await manageBotReq(req);
|
if (url.pathname === '/bot') return await manageBotReq(req);
|
||||||
return Response.json({a: 'b'});
|
// 消息发送
|
||||||
|
if (url.pathname === '/message') return await manageMessageReq(req);
|
||||||
|
// 其他
|
||||||
|
return new Response('OK')
|
||||||
},
|
},
|
||||||
port: 3000
|
port: 3000
|
||||||
});
|
});
|
@ -12,6 +12,8 @@
|
|||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/node-schedule": "^2.1.6",
|
||||||
|
"node-schedule": "^2.1.1",
|
||||||
"pocketbase": "^0.21.1"
|
"pocketbase": "^0.21.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
108
routes/bot/eventMsg.ts
Normal file
108
routes/bot/eventMsg.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { sendMsg } from "../../utils/sendMsg";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为事件消息
|
||||||
|
* @param {LarkMessageEvent} body
|
||||||
|
*/
|
||||||
|
const isEventMsg = (body: LarkMessageEvent) => {
|
||||||
|
return body?.header?.event_type === "im.message.receive_v1";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取事件文本类型
|
||||||
|
* @param {LarkMessageEvent} body
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const getMsgType = (body: LarkMessageEvent) => {
|
||||||
|
return body?.event?.message?.message_type
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取对话流Id
|
||||||
|
* @param {LarkMessageEvent} body
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const getChatId = (body: LarkMessageEvent) => {
|
||||||
|
return body?.event?.message?.chat_id
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文本内容并剔除艾特信息
|
||||||
|
* @param {LarkMessageEvent} body
|
||||||
|
* @returns {string} 文本内容
|
||||||
|
*/
|
||||||
|
const getMsgText = (body: LarkMessageEvent) => {
|
||||||
|
// TODO: 如果之后想支持单独提醒,这里需要做模板解析
|
||||||
|
try {
|
||||||
|
const { text } = JSON.parse(body?.event?.message?.content)
|
||||||
|
// 去掉@_user_1相关的内容,例如 '@_user_1 测试' -> '测试'
|
||||||
|
const textWithoutAt = text.replace(/@_user_\d+/g, '')
|
||||||
|
// 去除空格和换行
|
||||||
|
const textWithoutSpace = textWithoutAt.replace(/[\s\n]/g, '')
|
||||||
|
return textWithoutSpace
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过滤出非法消息,如果发表情包就直接发回去
|
||||||
|
* @param {LarkMessageEvent} body
|
||||||
|
* @returns {boolean} 是否为非法消息
|
||||||
|
*/
|
||||||
|
const filterIllegalMsg = (body: LarkMessageEvent) => {
|
||||||
|
const chatId = getChatId(body)
|
||||||
|
if (!chatId) return true
|
||||||
|
const msgType = getMsgType(body)
|
||||||
|
if (msgType === 'sticker') {
|
||||||
|
const content = body?.event?.message?.content
|
||||||
|
sendMsg('chat_id', chatId, 'sticker', content)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (msgType !== 'text') {
|
||||||
|
const textList = [
|
||||||
|
'仅支持普通文本内容[黑脸]',
|
||||||
|
'唔...我只能处理普通文本哦[泣不成声]',
|
||||||
|
'噢!这似乎是个非普通文本[看]',
|
||||||
|
'哇!这是什么东东?我只懂普通文本啦![可爱]',
|
||||||
|
'只能处理普通文本内容哦[捂脸]',
|
||||||
|
]
|
||||||
|
const content = JSON.stringify({ text: textList[Math.floor(Math.random() * textList.length)] })
|
||||||
|
sendMsg('chat_id', chatId, 'text', content)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过滤出info指令
|
||||||
|
* @param {LarkMessageEvent} body
|
||||||
|
* @returns {boolean} 是否为info指令
|
||||||
|
*/
|
||||||
|
const filterGetInfoCommand = (body: LarkMessageEvent) => {
|
||||||
|
const chatId = getChatId(body)
|
||||||
|
const text = getMsgText(body)
|
||||||
|
if (text !== 'info') return false
|
||||||
|
const content = JSON.stringify({ text: JSON.stringify(body)})
|
||||||
|
sendMsg('chat_id', chatId, 'text', content)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export const manageEventMsg = async (body: LarkMessageEvent) => {
|
||||||
|
// 过滤非Event消息
|
||||||
|
if (!isEventMsg(body)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 过滤非法消息
|
||||||
|
if (filterIllegalMsg(body)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// 过滤info指令
|
||||||
|
if (filterGetInfoCommand(body)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
import { manageEventMsg } from "./eventMsg"
|
||||||
|
|
||||||
export const manageBotReq = async (req: Request) => {
|
export const manageBotReq = async (req: Request) => {
|
||||||
const body = await req.json() as any
|
const body = await req.json() as any
|
||||||
// 验证机器人
|
// 验证机器人
|
||||||
@ -5,5 +7,7 @@ export const manageBotReq = async (req: Request) => {
|
|||||||
console.log("🚀 ~ manageBotReq ~ url_verification:")
|
console.log("🚀 ~ manageBotReq ~ url_verification:")
|
||||||
return Response.json({ challenge: body?.challenge })
|
return Response.json({ challenge: body?.challenge })
|
||||||
}
|
}
|
||||||
|
// 处理Event消息
|
||||||
|
if (await manageEventMsg(body)) return new Response("success")
|
||||||
return new Response("hello, glade to see you!")
|
return new Response("hello, glade to see you!")
|
||||||
}
|
}
|
82
routes/message/index.ts
Normal file
82
routes/message/index.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import db from "../../db";
|
||||||
|
import { sendMsg } from "../../utils/sendMsg";
|
||||||
|
|
||||||
|
interface MessageReqJson {
|
||||||
|
group_id: string;
|
||||||
|
msg_type: MsgType;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateMessageReq = (body: MessageReqJson) => {
|
||||||
|
if (!body.group_id) {
|
||||||
|
return new Response("group_id is required");
|
||||||
|
}
|
||||||
|
if (!body.msg_type) {
|
||||||
|
return new Response("msg_type is required");
|
||||||
|
}
|
||||||
|
if (!body.content) {
|
||||||
|
return new Response("content is required");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const manageMessageReq = async (req: Request) => {
|
||||||
|
const body = (await req.json()) as MessageReqJson;
|
||||||
|
// 校验参数
|
||||||
|
const validateRes = validateMessageReq(body);
|
||||||
|
if (validateRes) {
|
||||||
|
return validateRes;
|
||||||
|
}
|
||||||
|
// 获取所有接收者
|
||||||
|
const group = (await db.messageGroup.getOne(body.group_id)) as PBMessageGroup;
|
||||||
|
if (!group) {
|
||||||
|
return new Response("group not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { chat_id, open_id, union_id, user_id, email } = group;
|
||||||
|
// 遍历所有id发送消息,保存所有对应的messageId
|
||||||
|
const sendRes = {
|
||||||
|
chat_id: {} as Record<string, any>,
|
||||||
|
open_id: {} as Record<string, any>,
|
||||||
|
union_id: {} as Record<string, any>,
|
||||||
|
user_id: {} as Record<string, any>,
|
||||||
|
email: {} as Record<string, any>,
|
||||||
|
};
|
||||||
|
// 发送消息列表
|
||||||
|
const sendList = [] as Promise<any>[];
|
||||||
|
|
||||||
|
// 构造发送消息函数
|
||||||
|
const makeSendFunc = (receive_id_type: ReceiveIDType) => {
|
||||||
|
return (receive_id: string) => {
|
||||||
|
sendList.push(
|
||||||
|
sendMsg(receive_id_type, receive_id, body.msg_type, body.content).then(
|
||||||
|
(res) => {
|
||||||
|
sendRes[receive_id_type][receive_id] = res;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
|
if (chat_id) chat_id.map(makeSendFunc("chat_id"));
|
||||||
|
if (open_id) open_id.map(makeSendFunc("open_id"));
|
||||||
|
if (union_id) union_id.map(makeSendFunc("union_id"));
|
||||||
|
if (user_id) user_id.map(makeSendFunc("user_id"));
|
||||||
|
if (email) email.map(makeSendFunc("email"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(sendList);
|
||||||
|
return Response.json({
|
||||||
|
code: 200,
|
||||||
|
msg: "ok",
|
||||||
|
data: sendRes,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return Response.json({
|
||||||
|
code: 400,
|
||||||
|
msg: "send msg failed",
|
||||||
|
data: sendRes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
1
routes/message/readme.md
Normal file
1
routes/message/readme.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# 批量发送消息,给已经订阅的用户和群组发送消息
|
20
schedule/accessToken.ts
Normal file
20
schedule/accessToken.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import db from "../db"
|
||||||
|
|
||||||
|
export const resetAccessToken = async () => {
|
||||||
|
const URL = 'https://open.f.mioffice.cn/open-apis/auth/v3/tenant_access_token/internal'
|
||||||
|
const app_id = 'cli_a1eff35b43b89063'
|
||||||
|
const app_secret = 'IFSl8ig5DMwMnFjwPiljCfoEWlgRwDxW'
|
||||||
|
const res = await fetch(URL, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
app_id,
|
||||||
|
app_secret
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const { tenant_access_token } = await res.json() as any
|
||||||
|
await db.tenantAccessToken.update(tenant_access_token)
|
||||||
|
return tenant_access_token
|
||||||
|
}
|
9
schedule/index.ts
Normal file
9
schedule/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { resetAccessToken } from "./accessToken";
|
||||||
|
import schedule from 'node-schedule'
|
||||||
|
|
||||||
|
export const initSchedule = async () => {
|
||||||
|
// 定时任务,每15分钟刷新一次token
|
||||||
|
schedule.scheduleJob('*/15 * * * *', resetAccessToken);
|
||||||
|
// 立即执行一次
|
||||||
|
resetAccessToken()
|
||||||
|
}
|
445
typings.d.ts
vendored
Normal file
445
typings.d.ts
vendored
Normal file
@ -0,0 +1,445 @@
|
|||||||
|
/**
|
||||||
|
* 用户信息
|
||||||
|
*/
|
||||||
|
interface User {
|
||||||
|
/**
|
||||||
|
* id
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* 用户名
|
||||||
|
* @example zhaoyingbo
|
||||||
|
*/
|
||||||
|
userId: string;
|
||||||
|
/**
|
||||||
|
* open_id
|
||||||
|
*/
|
||||||
|
openId: string;
|
||||||
|
/**
|
||||||
|
* 提醒列表
|
||||||
|
*/
|
||||||
|
remindList: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提醒列表
|
||||||
|
*/
|
||||||
|
interface Remind {
|
||||||
|
/**
|
||||||
|
* id
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* 所有者信息,绑定用户表的id
|
||||||
|
*/
|
||||||
|
owner: string;
|
||||||
|
/**
|
||||||
|
* 消息Id
|
||||||
|
*/
|
||||||
|
messageId: string;
|
||||||
|
/**
|
||||||
|
* 接收者类型
|
||||||
|
*/
|
||||||
|
subscriberType: "open_id" | "user_id" | "union_id" | "email" | "chat_id";
|
||||||
|
/**
|
||||||
|
* 接收者Id
|
||||||
|
*/
|
||||||
|
subscriberId: string;
|
||||||
|
/**
|
||||||
|
* 是否需要回复,不需要回复的也不会重复提醒
|
||||||
|
*/
|
||||||
|
needReply: boolean;
|
||||||
|
/**
|
||||||
|
* 延迟时间
|
||||||
|
*/
|
||||||
|
delayTime: number;
|
||||||
|
/**
|
||||||
|
* 卡片信息,用于绘制初始卡片、确认卡片、取消卡片、延迟卡片
|
||||||
|
*/
|
||||||
|
cardInfo: {
|
||||||
|
/**
|
||||||
|
* 提醒标题,必须要有
|
||||||
|
*/
|
||||||
|
title: string;
|
||||||
|
/**
|
||||||
|
* 插图key
|
||||||
|
*/
|
||||||
|
imageKey?: string;
|
||||||
|
/**
|
||||||
|
* 提醒内容,为空不显示
|
||||||
|
*/
|
||||||
|
content?: string;
|
||||||
|
/**
|
||||||
|
* 确认文本,为空不显示,为需要回复卡片时,如果为空则默认为“完成”
|
||||||
|
*/
|
||||||
|
confirmText?: string;
|
||||||
|
/**
|
||||||
|
* 取消文本,为空不显示
|
||||||
|
*/
|
||||||
|
cancelText?: string;
|
||||||
|
/**
|
||||||
|
* 延迟文本,为空不显示
|
||||||
|
*/
|
||||||
|
delayText?: string;
|
||||||
|
} | null;
|
||||||
|
/**
|
||||||
|
* 卡片模板信息
|
||||||
|
*/
|
||||||
|
templateInfo: {
|
||||||
|
/**
|
||||||
|
* 卡片模板ID,会注入变量
|
||||||
|
* ${owner} 所有者
|
||||||
|
* ${remindTime} 提醒时间
|
||||||
|
*/
|
||||||
|
pendingTemplateId: string;
|
||||||
|
/**
|
||||||
|
* 交互之后的卡片模板ID,如果有这个就不会用下边三个但是都会注入变量
|
||||||
|
* ${owner} 所有者
|
||||||
|
* ${remindTime} 提醒时间
|
||||||
|
* ${result} 交互结果,会读卡片按钮绑定的变量text,如果没有则是绑定的result对应的 已确认、已取消、已延迟
|
||||||
|
* ${interactTime} 交互时间
|
||||||
|
*/
|
||||||
|
interactedTemplateId: string;
|
||||||
|
/**
|
||||||
|
* 确认之后的卡片模板ID
|
||||||
|
*/
|
||||||
|
confirmedTemplateId: string;
|
||||||
|
/**
|
||||||
|
* 取消之后的卡片模板ID
|
||||||
|
*/
|
||||||
|
cancelededTemplateId: string;
|
||||||
|
/**
|
||||||
|
* 延迟之后的卡片模板ID
|
||||||
|
*/
|
||||||
|
delayedTemplateId: string;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提醒时间
|
||||||
|
*/
|
||||||
|
remindTimes: RemindTime[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用
|
||||||
|
*/
|
||||||
|
enabled: boolean;
|
||||||
|
/**
|
||||||
|
* 下次提醒的时间,格式为yyyy-MM-dd HH:mm
|
||||||
|
*/
|
||||||
|
nextRemindTime: string;
|
||||||
|
/**
|
||||||
|
* 下次提醒时间的中文
|
||||||
|
*/
|
||||||
|
nextRemindTimeCHS: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提醒时间
|
||||||
|
* 为了支持多个时间点提醒,将时间存成数组
|
||||||
|
*/
|
||||||
|
interface RemindTime {
|
||||||
|
/**
|
||||||
|
* 重复类型
|
||||||
|
* single: 一次性
|
||||||
|
* daily: 每天
|
||||||
|
* weekly: 每周
|
||||||
|
* monthly: 每月
|
||||||
|
* yearly: 每年
|
||||||
|
* workday: 工作日
|
||||||
|
* holiday: 节假日
|
||||||
|
*/
|
||||||
|
frequency:
|
||||||
|
| "single"
|
||||||
|
| "daily"
|
||||||
|
| "weekly"
|
||||||
|
| "monthly"
|
||||||
|
| "yearly"
|
||||||
|
| "workday"
|
||||||
|
| "holiday";
|
||||||
|
/**
|
||||||
|
* 提醒时间,格式为HH:mm, single类型时仅作展示用,类型为yyyy-MM-dd HH:mm
|
||||||
|
*/
|
||||||
|
time: string;
|
||||||
|
/**
|
||||||
|
* 星期几[1-7],当frequency为weekly时有效
|
||||||
|
*/
|
||||||
|
daysOfWeek: number[];
|
||||||
|
/**
|
||||||
|
* 每月的几号[1-31],当frequency为monthly时有效
|
||||||
|
*/
|
||||||
|
daysOfMonth: number[];
|
||||||
|
/**
|
||||||
|
* 每年的哪天提醒,当frequency为 yearly 时有效,格式为MM-dd
|
||||||
|
*/
|
||||||
|
dayOfYear: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提醒记录
|
||||||
|
* 记录提醒时间,回答结果等
|
||||||
|
*/
|
||||||
|
interface RemindRecord {
|
||||||
|
/**
|
||||||
|
* 记录Id
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* 关联的提醒Id
|
||||||
|
*/
|
||||||
|
remindId: string;
|
||||||
|
/**
|
||||||
|
* 发送的卡片Id
|
||||||
|
*/
|
||||||
|
messageId: string;
|
||||||
|
/**
|
||||||
|
* 提醒状态
|
||||||
|
* pending: 待确认
|
||||||
|
* delay: 已延迟
|
||||||
|
* confirmed: 已确认
|
||||||
|
* canceled: 已取消
|
||||||
|
*/
|
||||||
|
status: "pending" | "delayed" | "confirmed" | "canceled";
|
||||||
|
/**
|
||||||
|
* 本次提醒时间,格式为yyyy-MM-dd HH:mm
|
||||||
|
*/
|
||||||
|
remindTime: string;
|
||||||
|
/**
|
||||||
|
* 用户交互的时间,格式为yyyy-MM-dd HH:mm
|
||||||
|
*/
|
||||||
|
interactTime: string;
|
||||||
|
/**
|
||||||
|
* 用户回答的结果,类似每天 07:00
|
||||||
|
*/
|
||||||
|
result: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息事件头
|
||||||
|
*/
|
||||||
|
interface Header {
|
||||||
|
/**
|
||||||
|
* 事件ID
|
||||||
|
* @example 0f8ab23b60993cf8dd15c8cde4d7b0f5
|
||||||
|
*/
|
||||||
|
event_id: string;
|
||||||
|
/**
|
||||||
|
* token
|
||||||
|
* @example tV9djUKSjzVnekV7xTg2Od06NFTcsBnj
|
||||||
|
*/
|
||||||
|
token: string;
|
||||||
|
/**
|
||||||
|
* 创建时间戳
|
||||||
|
* @example 1693565712117
|
||||||
|
*/
|
||||||
|
create_time: string;
|
||||||
|
/**
|
||||||
|
* 事件类型
|
||||||
|
* @example im.message.receive_v1
|
||||||
|
*/
|
||||||
|
event_type: string;
|
||||||
|
/**
|
||||||
|
* tenant_key
|
||||||
|
* @example 2ee61fe50f4f1657
|
||||||
|
*/
|
||||||
|
tenant_key: string;
|
||||||
|
/**
|
||||||
|
* app_id
|
||||||
|
* @example cli_a1eff35b43b89063
|
||||||
|
*/
|
||||||
|
app_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户ID信息
|
||||||
|
*/
|
||||||
|
interface UserIdInfo {
|
||||||
|
/**
|
||||||
|
* 用户标记
|
||||||
|
* @example ou_032f507d08f9a7f28b042fcd086daef5
|
||||||
|
*/
|
||||||
|
open_id: string;
|
||||||
|
/**
|
||||||
|
* 用户标记
|
||||||
|
* @example on_7111660fddd8302ce47bf1999147c011
|
||||||
|
*/
|
||||||
|
union_id: string;
|
||||||
|
/**
|
||||||
|
* 用户名
|
||||||
|
* @example zhaoyingbo
|
||||||
|
*/
|
||||||
|
user_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 被AT的人的信息
|
||||||
|
*/
|
||||||
|
interface Mention {
|
||||||
|
/**
|
||||||
|
* 被艾特的人的ID信息
|
||||||
|
*/
|
||||||
|
id: UserIdInfo;
|
||||||
|
/**
|
||||||
|
* 对应到文本内的内容
|
||||||
|
* @example "@_user_1"
|
||||||
|
*/
|
||||||
|
key: string;
|
||||||
|
/**
|
||||||
|
* 用户名
|
||||||
|
* @example 小煎蛋
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* 应用ID
|
||||||
|
* @example 2ee61fe50f4f1657
|
||||||
|
*/
|
||||||
|
tenant_key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息内容信息
|
||||||
|
*/
|
||||||
|
interface Message {
|
||||||
|
/**
|
||||||
|
* 对话流ID
|
||||||
|
* @example oc_433b1cb7a9dbb7ebe70a4e1a59cb8bb1
|
||||||
|
*/
|
||||||
|
chat_id: string;
|
||||||
|
/**
|
||||||
|
* 消息类型
|
||||||
|
* @example group | p2p
|
||||||
|
*/
|
||||||
|
chat_type: string;
|
||||||
|
/**
|
||||||
|
* JSON字符串文本内容
|
||||||
|
* @example "{\"text\":\"@_user_1 测试\"}"
|
||||||
|
*/
|
||||||
|
content: string;
|
||||||
|
/**
|
||||||
|
* 消息发送时间戳
|
||||||
|
* @example 1693565711996
|
||||||
|
*/
|
||||||
|
create_time: string;
|
||||||
|
/**
|
||||||
|
* 被艾特的人信息
|
||||||
|
*/
|
||||||
|
mentions?: Mention[];
|
||||||
|
/**
|
||||||
|
* 当前消息的ID
|
||||||
|
* @example om_038fc0eceed6224a1abc1cdaa4266405
|
||||||
|
*/
|
||||||
|
message_id: string;
|
||||||
|
/**
|
||||||
|
* 消息类型
|
||||||
|
* @example text、post、image、file、audio、media、sticker、interactive、share_chat、share_user
|
||||||
|
*/
|
||||||
|
message_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息发送者信息
|
||||||
|
*/
|
||||||
|
interface Sender {
|
||||||
|
/**
|
||||||
|
* id 相关信息
|
||||||
|
*/
|
||||||
|
sender_id: UserIdInfo;
|
||||||
|
/**
|
||||||
|
* 发送者类型
|
||||||
|
* @example user
|
||||||
|
*/
|
||||||
|
sender_type: string;
|
||||||
|
/**
|
||||||
|
* 应用ID
|
||||||
|
* @example 2ee61fe50f4f1657
|
||||||
|
*/
|
||||||
|
tenant_key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 事件详情
|
||||||
|
*/
|
||||||
|
interface Event {
|
||||||
|
message: Message;
|
||||||
|
sender: Sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 事件订阅信息
|
||||||
|
*/
|
||||||
|
interface LarkMessageEvent {
|
||||||
|
/**
|
||||||
|
* 协议版本
|
||||||
|
* @example 2.0
|
||||||
|
*/
|
||||||
|
schema: string;
|
||||||
|
/**
|
||||||
|
* 事件头
|
||||||
|
*/
|
||||||
|
header: Header;
|
||||||
|
/**
|
||||||
|
* 事件详情
|
||||||
|
*/
|
||||||
|
event: Event;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户Action信息
|
||||||
|
*/
|
||||||
|
interface LarkUserAction {
|
||||||
|
/**
|
||||||
|
* open_id
|
||||||
|
*/
|
||||||
|
open_id: string;
|
||||||
|
/**
|
||||||
|
* 用户名
|
||||||
|
* @example zhaoyingbo
|
||||||
|
*/
|
||||||
|
user_id: string;
|
||||||
|
/**
|
||||||
|
* 当前消息的ID
|
||||||
|
* @example om_038fc0eceed6224a1abc1cdaa4266405
|
||||||
|
*/
|
||||||
|
open_message_id: string;
|
||||||
|
/**
|
||||||
|
* 对话流ID
|
||||||
|
* @example oc_433b1cb7a9dbb7ebe70a4e1a59cb8bb1
|
||||||
|
*/
|
||||||
|
open_chat_id: string;
|
||||||
|
/**
|
||||||
|
* 应用ID
|
||||||
|
* @example 2ee61fe50f4f1657
|
||||||
|
*/
|
||||||
|
tenant_key: string;
|
||||||
|
/**
|
||||||
|
* token
|
||||||
|
* @example tV9djUKSjzVnekV7xTg2Od06NFTcsBnj
|
||||||
|
*/
|
||||||
|
token: string;
|
||||||
|
/**
|
||||||
|
* 事件结果
|
||||||
|
*/
|
||||||
|
action: {
|
||||||
|
/**
|
||||||
|
* 传的参数
|
||||||
|
*/
|
||||||
|
value: any;
|
||||||
|
/**
|
||||||
|
* 标签名
|
||||||
|
* @example picker_datetime
|
||||||
|
*/
|
||||||
|
tag: string;
|
||||||
|
/**
|
||||||
|
* 选择的事件
|
||||||
|
* @example 2023-09-03 10:35 +0800
|
||||||
|
*/
|
||||||
|
option: string;
|
||||||
|
/**
|
||||||
|
* 时区
|
||||||
|
*/
|
||||||
|
timezone: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReceiveIDType = "open_id" | "user_id" | "union_id" | "email" | "chat_id";
|
||||||
|
|
||||||
|
type MsgType = "text" | "post" | "image" | "file" | "audio" | "media" | "sticker" | "interactive" | "share_chat" | "share_user";
|
11
utils/pbTools.ts
Normal file
11
utils/pbTools.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export const managePb404 = async (dbFunc: Function) => {
|
||||||
|
try {
|
||||||
|
return await dbFunc()
|
||||||
|
} catch (err: any) {
|
||||||
|
console.log("🚀 ~ manage404 ~ err:", err)
|
||||||
|
// 没有这个提醒就返回空
|
||||||
|
if (err?.message === "The requested resource wasn't found.") {
|
||||||
|
return null
|
||||||
|
} else throw err;
|
||||||
|
}
|
||||||
|
}
|
47
utils/sendMsg.ts
Normal file
47
utils/sendMsg.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import db from "../db";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送卡片
|
||||||
|
* @param {ReceiveIDType} receive_id_type 消息接收者id类型 open_id/user_id/union_id/email/chat_id
|
||||||
|
* @param {string} receive_id 消息接收者的ID,ID类型应与查询参数receive_id_type 对应
|
||||||
|
* @param {MsgType} msg_type 消息类型 包括:text、post、image、file、audio、media、sticker、interactive、share_chat、share_user
|
||||||
|
* @param {string} content 消息内容,JSON结构序列化后的字符串。不同msg_type对应不同内容
|
||||||
|
* @returns {string} 消息id
|
||||||
|
*/
|
||||||
|
export const sendMsg = async (
|
||||||
|
receive_id_type: ReceiveIDType,
|
||||||
|
receive_id: string,
|
||||||
|
msg_type: MsgType,
|
||||||
|
content: string
|
||||||
|
) => {
|
||||||
|
const URL = `https://open.f.mioffice.cn/open-apis/im/v1/messages?receive_id_type=${receive_id_type}`;
|
||||||
|
const tenant_access_token = await db.tenantAccessToken.get();
|
||||||
|
const header = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${tenant_access_token}`,
|
||||||
|
};
|
||||||
|
const body = { receive_id, msg_type, content };
|
||||||
|
const res = await fetch(URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: header,
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
const data = (await res.json()) as any;
|
||||||
|
if (data.code !== 0) {
|
||||||
|
console.log("sendMsg error", data);
|
||||||
|
return {
|
||||||
|
code: data.code,
|
||||||
|
msg: data.msg,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
console.log("sendMsg success", data);
|
||||||
|
return {
|
||||||
|
code: 0,
|
||||||
|
msg: "success",
|
||||||
|
data: {
|
||||||
|
message_id: data.data.message_id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// sendMsg('user_id', 'liuke9', 'text', JSON.stringify({text: '这是测试消息,不要回复'}))
|
Loading…
x
Reference in New Issue
Block a user