feat: 支持根据用户组转发消息
This commit is contained in:
parent
c1a4890eec
commit
cabc23ae77
@ -7,9 +7,10 @@
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {
|
||||
"files.autoSave": "off",
|
||||
"files.autoSave": "afterDelay",
|
||||
"editor.guides.bracketPairs": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
|
@ -22,7 +22,8 @@ jobs:
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
push: true
|
||||
tags: git.yingbo.im:333/zhaoyingbo/egg_server:latest
|
||||
tags: git.yingbo.im:333/zhaoyingbo/egg_server:${{ github.sha }}
|
||||
|
||||
deploy:
|
||||
needs: build-image
|
||||
runs-on: mi-server
|
||||
@ -40,7 +41,7 @@ jobs:
|
||||
key: ${{ secrets.SERVER_KEY }}
|
||||
port: ${{ secrets.SERVER_PORT }}
|
||||
source: docker-compose.yml
|
||||
target: /home/yingbo/docker/egg_server
|
||||
target: /home/deploy/docker/egg_server
|
||||
# 登录服务器,执行docker-compose命令
|
||||
- name: Login to the server and execute docker-compose command
|
||||
uses: appleboy/ssh-action@master
|
||||
@ -51,6 +52,6 @@ jobs:
|
||||
port: ${{ secrets.SERVER_PORT }}
|
||||
script: |
|
||||
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
|
||||
docker compose -f /home/yingbo/docker/egg_server/docker-compose.yml pull
|
||||
docker compose -f /home/yingbo/docker/egg_server/docker-compose.yml up -d
|
||||
cd /home/deploy/docker/egg_server
|
||||
sed -i "s/sha/${{ github.sha }}/g" docker-compose.yml
|
||||
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"
|
||||
|
||||
services:
|
||||
server:
|
||||
image: git.yingbo.im:333/zhaoyingbo/egg_server:latest
|
||||
egg_server:
|
||||
image: git.yingbo.im:333/zhaoyingbo/egg_server:sha
|
||||
container_name: egg_server
|
||||
restart: always
|
||||
ports:
|
||||
- 3000:3000
|
||||
- 3003:3000
|
||||
|
9
index.ts
9
index.ts
@ -1,4 +1,8 @@
|
||||
import { manageBotReq } from "./routes/bot";
|
||||
import { manageMessageReq } from "./routes/message";
|
||||
import { initSchedule } from "./schedule";
|
||||
|
||||
initSchedule()
|
||||
|
||||
Bun.serve({
|
||||
async fetch(req) {
|
||||
@ -7,7 +11,10 @@ Bun.serve({
|
||||
if (url.pathname === "/") return new Response("hello, glade to see you!");
|
||||
// 机器人
|
||||
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
|
||||
});
|
@ -12,6 +12,8 @@
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node-schedule": "^2.1.6",
|
||||
"node-schedule": "^2.1.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) => {
|
||||
const body = await req.json() as any
|
||||
// 验证机器人
|
||||
@ -5,5 +7,7 @@ export const manageBotReq = async (req: Request) => {
|
||||
console.log("🚀 ~ manageBotReq ~ url_verification:")
|
||||
return Response.json({ challenge: body?.challenge })
|
||||
}
|
||||
// 处理Event消息
|
||||
if (await manageEventMsg(body)) return new Response("success")
|
||||
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