Compare commits

...

17 Commits

Author SHA1 Message Date
linyuchen
2eb0ad589a chore: ver 3.7.0 2024-02-23 19:57:20 +08:00
linyuchen
829aba18f8 feat: 管理员变动事件
feat: 加群事件
feat: 加群请求处理api
feat: 退群api
fix: 回复消息id改为string
2024-02-23 19:56:20 +08:00
linyuchen
67dfd7c22f Merge branch 'main' into dev 2024-02-23 14:06:36 +08:00
linyuchen
27745087ad Merge pull request #69 from MisaLiu/fix_app_version 2024-02-23 11:50:37 +08:00
linyuchen
4ba333b6f5 Merge pull request #68 from MisaLiu/fix_echo 2024-02-23 11:50:28 +08:00
Misa Liu
f4fe26fbe1 fix: Fix app_version in get_version_info 2024-02-23 10:33:15 +08:00
Misa Liu
30e488aeaf fix: Fix var type of echo 2024-02-23 10:22:42 +08:00
linyuchen
1f0dad786c feat: group admin change notice 2024-02-23 04:08:20 +08:00
linyuchen
8dfc71ab6d fix: message id int32 2024-02-22 23:05:07 +08:00
linyuchen
12d1f87ad5 fix: message id int32 2024-02-22 23:02:23 +08:00
linyuchen
b27dadbbca temp save 2024-02-22 22:55:52 +08:00
linyuchen
688624500f docs: tg 2024-02-22 17:35:14 +08:00
linyuchen
eefb919f0f docs: update readme 2024-02-21 22:21:32 +08:00
linyuchen
5044d24ee1 feat: go-cqhttp api get_stranger_info
feat: api send_like
fix: some image of message use base64 instead of http url
2024-02-21 22:19:02 +08:00
linyuchen
7664e746b4 feat: some go-cqhttp feature 2024-02-21 17:17:15 +08:00
linyuchen
ebea755731 perf: log long string 2024-02-21 16:43:10 +08:00
linyuchen
e4508ea5c7 feat: go-cqhttp api send_private_forward_msg & send_group_forward_msg 2024-02-21 16:36:40 +08:00
34 changed files with 641 additions and 192 deletions

View File

@@ -1,7 +1,9 @@
# LLOneBot API
# LLOneBot API
LiteLoaderQQNT的OneBot11协议插件
TG群<https://t.me/+nLZEnpne-pQ1OWFl>
*注意:本文档对应的是 LiteLoader 1.0.0及以上版本如果你使用的是旧版本请切换到本项目v1分支查看文档*
*V3之后不再需要LLAPI*
@@ -31,11 +33,16 @@ LiteLoaderQQNT的OneBot11协议插件
- [x] 获取群列表
- [x] 获取群成员列表
- [x] 撤回消息
- [x] 处理加群请求
- [x] 退群
- [x] 上报好友消息
- [x] 上报群消息
- [x] 上报好友、群消息撤回
- [x] 上报加群请求
- [x] 上报群员人数变动
消息格式支持:
- [x] cq码
- [x] 文字
- [x] 表情
- [x] 图片
@@ -59,11 +66,19 @@ LiteLoaderQQNT的OneBot11协议插件
- [x] get_group_member_info
- [x] get_friend_list
- [x] get_msg
- [x] send_like
- [x] set_group_add_request
- [x] set_group_leave
- [x] get_version_info
- [x] get_status
- [x] can_send_image
- [x] can_send_record
支持的go-cqhtp api:
- [x] send_private_forward_msg
- [x] send_group_forward_msg
- [x] get_stranger_info
## 示例
![](doc/image/example.jpg)
@@ -91,13 +106,6 @@ LiteLoaderQQNT的OneBot11协议插件
</details>
<br/>
<details>
<summary>不支持cq码</summary>
<br/>
cq码已经过时了没有支持的打算(主要是我不用这玩意儿,加上我懒)
</details>
<br/>
<details>
<summary>QQ变得很卡</summary>
<br/>
@@ -110,7 +118,7 @@ LiteLoaderQQNT的OneBot11协议插件
- [x] 重构摆脱LLAPI目前调用LLAPI只能在renderer进程调用需重构成在main进程调用
- [x] 支持正、反向websocket感谢@disymayufei的PR
- [x] 转发消息记录
- [ ] 好友点赞api
- [x] 好友点赞api
## onebot11文档
<https://11.onebot.dev/>

View File

@@ -4,7 +4,7 @@
"name": "LLOneBot",
"slug": "LLOneBot",
"description": "LiteLoaderQQNT的OneBotApi",
"version": "3.4.0",
"version": "3.7.0",
"thumbnail": "./icon.png",
"authors": [
{

View File

@@ -2,6 +2,8 @@ import fs from "fs";
import {Config, OB11Config} from "./types";
import {mergeNewProperties} from "./utils";
export const HOOK_LOG = false;
export class ConfigUtil {
private readonly configPath: string;
private config: Config | null = null;

View File

@@ -1,15 +1,15 @@
import {NTQQApi} from '../ntqqapi/ntcall';
import {Friend, Group, GroupMember, RawMessage, SelfInfo} from "../ntqqapi/types";
import {Friend, Group, GroupMember, GroupNotify, RawMessage, SelfInfo} from "../ntqqapi/types";
export let groups: Group[] = []
export let friends: Friend[] = []
export let msgHistory: Record<string, RawMessage> = {} // msgId: RawMessage
let globalMsgId = Date.now()
let globalMsgId = Math.floor(Date.now() / 1000);
export function addHistoryMsg(msg: RawMessage): boolean{
export function addHistoryMsg(msg: RawMessage): boolean {
let existMsg = msgHistory[msg.msgId]
if (existMsg){
if (existMsg) {
Object.assign(existMsg, msg)
msg.msgShortId = existMsg.msgShortId;
return false
@@ -19,7 +19,7 @@ export function addHistoryMsg(msg: RawMessage): boolean{
return true
}
export function getHistoryMsgByShortId(shortId: number | string){
export function getHistoryMsgByShortId(shortId: number | string) {
// log("getHistoryMsgByShortId", shortId, Object.values(msgHistory).map(m=>m.msgShortId))
return Object.values(msgHistory).find(msg => msg.msgShortId.toString() == shortId.toString())
}
@@ -43,20 +43,19 @@ export async function getGroup(qq: string): Promise<Group | undefined> {
return group
}
export async function getGroupMember(groupQQ: string, memberQQ: string=null, memberUid: string=null) {
export async function getGroupMember(groupQQ: string, memberQQ: string, memberUid: string = null) {
const group = await getGroup(groupQQ)
if (group) {
let filterFunc: (member: GroupMember) => boolean
if (memberQQ){
if (memberQQ) {
filterFunc = member => member.uin === memberQQ
}
else if (memberUid){
} else if (memberUid) {
filterFunc = member => member.uid === memberUid
}
let member = group.members?.find(filterFunc)
if (!member){
if (!member) {
const _members = await NTQQApi.getGroupMembers(groupQQ)
if (_members.length){
if (_members.length) {
group.members = _members
}
member = group.members?.find(filterFunc)
@@ -77,7 +76,7 @@ export function getHistoryMsgBySeq(seq: string) {
}
export let uidMaps:Record<string, string> = {} // 一串加密的字符串(uid) -> qq号
export let uidMaps: Record<string, string> = {} // 一串加密的字符串(uid) -> qq号
export function getUidByUin(uin: string) {
for (const key in uidMaps) {
@@ -87,4 +86,6 @@ export function getUidByUin(uin: string) {
}
}
export const version = "v3.4.0"
export const version = "3.7.0"
export let groupNotifies: Map<string, GroupNotify> = new Map();

View File

@@ -91,6 +91,7 @@ export abstract class HttpServerBase {
if (method == "get"){
payload = req.query
}
log("收到http请求", url, payload);
try{
res.send(await handler(res, payload))
}catch (e) {

View File

@@ -13,6 +13,23 @@ export function getConfigUtil() {
return new ConfigUtil(configFilePath)
}
function truncateString(obj: any, maxLength = 500) {
if (obj !== null && typeof obj === 'object') {
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'string') {
// 如果是字符串且超过指定长度,则截断
if (obj[key].length > maxLength) {
obj[key] = obj[key].substring(0, maxLength) + '...';
}
} else if (typeof obj[key] === 'object') {
// 如果是对象或数组,则递归调用
truncateString(obj[key], maxLength);
}
});
}
return obj;
}
export function log(...msg: any[]) {
if (!getConfigUtil().getConfig().log) {
return
@@ -28,7 +45,8 @@ export function log(...msg: any[]) {
for (let msgItem of msg) {
// 判断是否是对象
if (typeof msgItem === "object") {
logMsg += JSON.stringify(msgItem) + " ";
let obj = JSON.parse(JSON.stringify(msgItem));
logMsg += JSON.stringify(truncateString(obj)) + " ";
continue;
}
logMsg += msgItem + " ";
@@ -175,4 +193,8 @@ export async function encodeSilk(filePath: string) {
log("convert silk failed", error.stack);
return {};
}
}
export function isNull(value: any) {
return value === undefined || value === null;
}

View File

@@ -6,16 +6,19 @@ import {Config} from "../common/types";
import {CHANNEL_GET_CONFIG, CHANNEL_LOG, CHANNEL_SET_CONFIG,} from "../common/channels";
import {ob11WebsocketServer} from "../onebot11/server/ws/WebsocketServer";
import {CONFIG_DIR, getConfigUtil, log} from "../common/utils";
import {addHistoryMsg, getGroupMember, msgHistory, selfInfo} from "../common/data";
import {addHistoryMsg, getGroup, getGroupMember, groupNotifies, msgHistory, selfInfo} from "../common/data";
import {hookNTQQApiCall, hookNTQQApiReceive, ReceiveCmd, registerReceiveHook} from "../ntqqapi/hook";
import {OB11Constructor} from "../onebot11/constructor";
import {NTQQApi} from "../ntqqapi/ntcall";
import {ChatType, RawMessage} from "../ntqqapi/types";
import {ChatType, GroupMember, GroupNotifies, GroupNotifyTypes, RawMessage} from "../ntqqapi/types";
import {ob11HTTPServer} from "../onebot11/server/http";
import {OB11FriendRecallNoticeEvent} from "../onebot11/event/notice/OB11FriendRecallNoticeEvent";
import {OB11GroupRecallNoticeEvent} from "../onebot11/event/notice/OB11GroupRecallNoticeEvent";
import {postEvent} from "../onebot11/server/postevent";
import {ob11ReverseWebsockets} from "../onebot11/server/ws/ReverseWebsocket";
import {OB11GroupAdminNoticeEvent} from "../onebot11/event/notice/OB11GroupAdminNoticeEvent";
import {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecreaseEvent";
import {OB11GroupRequestEvent} from "../onebot11/event/request/OB11GroupRequest";
let running = false;
@@ -94,7 +97,8 @@ function onLoad() {
if (debug) {
msg.raw = message;
}
if (msg.user_id.toString() == selfInfo.uin && !reportSelfMessage) {
const isSelfMsg = msg.user_id.toString() == selfInfo.uin
if (isSelfMsg && !reportSelfMessage) {
return
}
postEvent(msg);
@@ -103,8 +107,7 @@ function onLoad() {
}
}
async function start() {
async function startReceiveHook() {
registerReceiveHook<{ msgList: Array<RawMessage> }>(ReceiveCmd.NEW_MSG, (payload) => {
try {
postReceiveMsg(payload.msgList);
@@ -157,6 +160,81 @@ function onLoad() {
log("report self message error: ", e.toString());
}
})
registerReceiveHook<{
"doubt": boolean,
"oldestUnreadSeq": string,
"unreadCount": number
}>(ReceiveCmd.UNREAD_GROUP_NOTIFY, async (payload) => {
if (payload.unreadCount) {
log("开始获取群通知详情")
let notify: GroupNotifies;
try {
notify = await NTQQApi.getGroupNotifies();
}catch (e) {
// log("获取群通知详情失败", e);
return
}
const notifies = notify.notifies.slice(0, payload.unreadCount)
log("获取群通知详情完成", notifies, payload);
try {
for (const notify of notifies) {
if (parseInt(notify.seq) / 1000 < startTime){
continue;
}
const member1 = await getGroupMember(notify.group.groupCode, null, notify.user1.uid);
let member2: GroupMember;
if (notify.user2.uid){
member2 = await getGroupMember(notify.group.groupCode, null, notify.user2.uid);
}
if ([GroupNotifyTypes.ADMIN_SET, GroupNotifyTypes.ADMIN_UNSET].includes(notify.type)) {
log("有管理员变动通知");
let groupAdminNoticeEvent = new OB11GroupAdminNoticeEvent()
groupAdminNoticeEvent.group_id = parseInt(notify.group.groupCode);
log("开始获取变动的管理员")
if(member1){
log("变动管理员获取成功")
groupAdminNoticeEvent.user_id = parseInt(member1.uin);
groupAdminNoticeEvent.sub_type = notify.type == GroupNotifyTypes.ADMIN_UNSET ? "unset" : "set";
postEvent(groupAdminNoticeEvent, true);
}
else{
log("获取群通知的成员信息失败", notify, getGroup(notify.group.groupCode));
}
}
else if (notify.type == GroupNotifyTypes.MEMBER_EXIT){
log("有成员退出通知");
let groupDecreaseEvent = new OB11GroupDecreaseEvent(parseInt(notify.group.groupCode), parseInt(member1.uin))
// postEvent(groupDecreaseEvent, true);
}
else if ([GroupNotifyTypes.JOIN_REQUEST].includes(notify.type)){
log("有加群请求");
groupNotifies[notify.seq] = notify;
let groupRequestEvent = new OB11GroupRequestEvent();
groupRequestEvent.group_id = parseInt(notify.group.groupCode);
let requestQQ = ""
try {
requestQQ = (await NTQQApi.getUserInfo(notify.user1.uid)).uin;
}catch (e) {
log("获取加群人QQ号失败", e)
}
groupRequestEvent.user_id = parseInt(requestQQ) || 0;
groupRequestEvent.sub_type = "add"
groupRequestEvent.comment = notify.postscript;
groupRequestEvent.flag = notify.seq;
postEvent(groupRequestEvent);
}
}
}catch (e) {
log("解析群通知失败", e.stack);
}
}
})
}
let startTime = 0;
async function start() {
startTime = Date.now();
startReceiveHook().then();
NTQQApi.getGroups(true).then()
const config = getConfigUtil().getConfig()
if (config.ob11.enableHttp) {
@@ -166,16 +244,17 @@ function onLoad() {
log("http server start failed", e);
}
}
if (config.ob11.enableWs){
if (config.ob11.enableWs) {
ob11WebsocketServer.start(config.ob11.wsPort);
}
if (config.ob11.enableWsReverse){
if (config.ob11.enableWsReverse) {
ob11ReverseWebsockets.start();
}
log("LLOneBot start")
}
let getSelfNickCount = 0;
const init = async () => {
try {
const _ = await NTQQApi.getSelfInfo();
@@ -192,7 +271,10 @@ function onLoad() {
if (userInfo) {
selfInfo.nick = userInfo.nick;
} else {
return setTimeout(init, 1000);
getSelfNickCount++;
if (getSelfNickCount < 10){
return setTimeout(init, 1000);
}
}
} catch (e) {
log("get self nickname failed", e.toString());

View File

@@ -7,6 +7,7 @@ import {OB11GroupDecreaseEvent} from "../onebot11/event/notice/OB11GroupDecrease
import {OB11GroupIncreaseEvent} from "../onebot11/event/notice/OB11GroupIncreaseEvent";
import {v4 as uuidv4} from "uuid"
import {postEvent} from "../onebot11/server/postevent";
import {HOOK_LOG} from "../common/config";
export let hookApiCallbacks: Record<string, (apiReturn: any) => void> = {}
@@ -17,7 +18,10 @@ export enum ReceiveCmd {
USER_INFO = "nodeIKernelProfileListener/onProfileSimpleChanged",
GROUPS = "nodeIKernelGroupListener/onGroupListUpdate",
GROUPS_UNIX = "onGroupListUpdate",
FRIENDS = "onBuddyListChange"
FRIENDS = "onBuddyListChange",
MEDIA_DOWNLOAD_COMPLETE = "nodeIKernelMsgListener/onRichMediaDownloadComplete",
UNREAD_GROUP_NOTIFY = "nodeIKernelGroupListener/onGroupNotifiesUnreadCountUpdated",
GROUP_NOTIFY = "nodeIKernelGroupListener/onGroupSingleScreenNotifies"
}
interface NTQQApiReturnData<PayloadType = unknown> extends Array<any> {
@@ -43,7 +47,7 @@ let receiveHooks: Array<{
export function hookNTQQApiReceive(window: BrowserWindow) {
const originalSend = window.webContents.send;
const patchSend = (channel: string, ...args: NTQQApiReturnData) => {
// log(`received ntqq api message: ${channel}`, JSON.stringify(args))
HOOK_LOG && log(`received ntqq api message: ${channel}`, JSON.stringify(args))
if (args?.[1] instanceof Array) {
for (let receiveData of args?.[1]) {
const ntQQApiMethodName = receiveData.cmdName;
@@ -87,15 +91,15 @@ export function hookNTQQApiCall(window: BrowserWindow) {
const proxyIpcMsg = new Proxy(ipc_message_proxy, {
apply(target, thisArg, args) {
log("call NTQQ api", thisArg, args);
HOOK_LOG && log("call NTQQ api", thisArg, args);
return target.apply(thisArg, args);
},
});
// if (webContents._events["-ipc-message"]?.[0]) {
// webContents._events["-ipc-message"][0] = proxyIpcMsg;
// } else {
// webContents._events["-ipc-message"] = proxyIpcMsg;
// }
if (webContents._events["-ipc-message"]?.[0]) {
webContents._events["-ipc-message"][0] = proxyIpcMsg;
} else {
webContents._events["-ipc-message"] = proxyIpcMsg;
}
}
export function registerReceiveHook<PayloadType>(method: ReceiveCmd, hookFunc: (payload: PayloadType) => void): string {
@@ -250,3 +254,4 @@ registerReceiveHook<{ msgRecord: RawMessage }>(ReceiveCmd.SELF_SEND_MSG, ({msgRe
}
}
})

View File

@@ -1,9 +1,19 @@
import {ipcMain} from "electron";
import {hookApiCallbacks, ReceiveCmd, registerReceiveHook, removeReceiveHook} from "./hook";
import {log} from "../common/utils";
import {ChatType, Friend, Group, GroupMember, RawMessage, SelfInfo, SendMessageElement, User} from "./types";
import {
ChatType,
Friend,
Group,
GroupMember,
GroupNotifies, GroupNotify, GroupRequestOperateTypes,
RawMessage,
SelfInfo,
SendMessageElement,
User
} from "./types";
import * as fs from "fs";
import {addHistoryMsg, msgHistory, selfInfo, uidMaps} from "../common/data";
import {addHistoryMsg, groupNotifies, msgHistory, selfInfo} from "../common/data";
import {v4 as uuidv4} from "uuid"
interface IPCReceiveEvent {
@@ -41,7 +51,10 @@ export enum NTQQApiMethod {
RECALL_MSG = "nodeIKernelMsgService/recallMsg",
SEND_MSG = "nodeIKernelMsgService/sendMsg",
DOWNLOAD_MEDIA = "nodeIKernelMsgService/downloadRichMedia",
MULTI_FORWARD_MSG = "nodeIKernelMsgService/multiForwardMsgWithComment" // 合并转发
MULTI_FORWARD_MSG = "nodeIKernelMsgService/multiForwardMsgWithComment", // 合并转发
GET_GROUP_NOTICE = "nodeIKernelGroupService/getSingleScreenNotifies",
HANDLE_GROUP_REQUEST = "nodeIKernelGroupService/operateSysNotify",
QUIT_GROUP = "nodeIKernelGroupService/quitGroup",
}
enum NTQQApiChannel {
@@ -56,24 +69,22 @@ export interface Peer {
guildId?: ""
}
enum CallBackType {
UUID,
METHOD
}
interface NTQQApiParams {
methodName: NTQQApiMethod,
methodName: NTQQApiMethod | string,
className?: NTQQApiClass,
channel?: NTQQApiChannel,
classNameIsRegister?: boolean
args?: unknown[],
cbCmd?: ReceiveCmd | null
cbCmd?: ReceiveCmd | null,
cmdCB?: (payload: any) => boolean;
timeoutSecond?: number,
}
function callNTQQApi<ReturnType>(params: NTQQApiParams) {
let {
className, methodName, channel, args,
cbCmd, timeoutSecond: timeout
cbCmd, timeoutSecond: timeout,
classNameIsRegister, cmdCB
} = params;
className = className ?? NTQQApiClass.NT_API;
channel = channel ?? NTQQApiChannel.IPC_UP_2;
@@ -85,6 +96,11 @@ function callNTQQApi<ReturnType>(params: NTQQApiParams) {
// log("callNTQQApiPromise", channel, className, methodName, args, uuid)
const _timeout = timeout * 1000
let success = false
let eventName = className + "-" + channel[channel.length - 1];
if (classNameIsRegister) {
eventName += "-register";
}
const apiArgs = [methodName, ...args]
if (!cbCmd) {
// QQ后端会返回结果并且可以插根据uuid识别
hookApiCallbacks[uuid] = (r: ReturnType) => {
@@ -95,12 +111,20 @@ function callNTQQApi<ReturnType>(params: NTQQApiParams) {
// 这里的callback比较特殊QQ后端先返回是否调用成功再返回一条结果数据
hookApiCallbacks[uuid] = (result: GeneralCallResult) => {
log(`${methodName} callback`, result)
if (result.result == 0) {
if (result?.result == 0 || result === undefined) {
const hookId = registerReceiveHook<ReturnType>(cbCmd, (payload) => {
log(methodName, "second callback", cbCmd, payload);
removeReceiveHook(hookId);
success = true
resolve(payload);
if (cmdCB) {
if (cmdCB(payload)) {
removeReceiveHook(hookId);
success = true
resolve(payload);
}
} else {
removeReceiveHook(hookId);
success = true
resolve(payload);
}
})
} else {
success = true
@@ -111,12 +135,11 @@ function callNTQQApi<ReturnType>(params: NTQQApiParams) {
setTimeout(() => {
// log("ntqq api timeout", success, channel, className, methodName)
if (!success) {
log(`ntqq api timeout ${channel}, ${className}, ${methodName}`)
reject(`ntqq api timeout ${channel}, ${className}, ${methodName}`)
log(`ntqq api timeout ${channel}, ${eventName}, ${methodName}`, apiArgs);
reject(`ntqq api timeout ${channel}, ${eventName}, ${methodName}, ${apiArgs}`)
}
}, _timeout)
const eventName = className + "-" + channel[channel.length - 1];
const apiArgs = [methodName, ...args]
ipcMain.emit(
channel,
{},
@@ -138,7 +161,7 @@ interface GeneralCallResult {
export class NTQQApi {
// static likeFriend = defineNTQQApi<void>(NTQQApiChannel.IPC_UP_2, NTQQApiClass.NT_API, NTQQApiMethod.LIKE_FRIEND)
static likeFriend(uid: string, count = 1) {
return callNTQQApi({
return callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.LIKE_FRIEND,
args: [{
doLikeUserInfo: {
@@ -225,11 +248,12 @@ export class NTQQApi {
let values = result.result.infos.values()
let members = Array.from(values) as GroupMember[]
for(const member of members){
uidMaps[member.uid] = member.uin;
for (const member of members) {
// uidMaps[member.uid] = member.uin;
}
log(uidMaps);
// log(uidMaps);
// log("members info", values);
log(`get group ${groupQQ} members success`)
return members
} catch (e) {
log(`get group ${groupQQ} members failed`, e)
@@ -328,7 +352,16 @@ export class NTQQApi {
},
undefined,
]
await callNTQQApi({methodName: NTQQApiMethod.DOWNLOAD_MEDIA, args: apiParams})
// log("需要下载media", sourcePath);
await callNTQQApi({
methodName: NTQQApiMethod.DOWNLOAD_MEDIA,
args: apiParams,
cbCmd: ReceiveCmd.MEDIA_DOWNLOAD_COMPLETE,
cmdCB: (payload: { notifyInfo: { filePath: string } }) => {
// log("media 下载完成判断", payload.notifyInfo.filePath, sourcePath);
return payload.notifyInfo.filePath == sourcePath;
}
})
return sourcePath
}
@@ -341,8 +374,8 @@ export class NTQQApi {
})
}
static sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = false) {
const sendTimeout = 10 * 1000
static sendMsg(peer: Peer, msgElements: SendMessageElement[], waitComplete = false, timeout = 10000) {
const sendTimeout = timeout
return new Promise<RawMessage>((resolve, reject) => {
const peerUid = peer.peerUid;
@@ -379,6 +412,7 @@ export class NTQQApi {
return reject("发送超时")
}
if (msgHistory[rawMessage.msgId]?.sendStatus == 2) {
log(`${peerUid}发送消息成功`)
success = true;
resolve(rawMessage);
} else {
@@ -389,6 +423,7 @@ export class NTQQApi {
checkSendComplete();
} else {
success = true;
log(`${peerUid}发送消息成功`)
resolve(rawMessage);
}
}
@@ -458,4 +493,56 @@ export class NTQQApi {
})
})
}
static async getGroupNotifies() {
// 获取管理员变更
// 加群通知,退出通知,需要管理员权限
await callNTQQApi<GeneralCallResult>({
methodName: ReceiveCmd.GROUP_NOTIFY,
classNameIsRegister: true,
})
return await callNTQQApi<GroupNotifies>({
methodName: NTQQApiMethod.GET_GROUP_NOTICE,
cbCmd: ReceiveCmd.GROUP_NOTIFY,
args: [
{"doubt": false, "startSeq": "", "number": 14},
null
]
});
}
static async handleGroupRequest(seq: string, operateType: GroupRequestOperateTypes, reason?: string) {
const notify: GroupNotify = groupNotifies[seq];
if (!notify){
throw `${seq}对应的加群通知不存在`
}
return await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.HANDLE_GROUP_REQUEST,
args: [
{
"doubt": false,
"operateMsg": {
"operateType": operateType, // 2 拒绝
"targetMsg": {
"seq": seq, // 通知序列号
"type": notify.type,
"groupCode": notify.group.groupCode,
"postscript": reason
}
}
},
null
]
});
}
static async quitGroup(groupQQ: string){
await callNTQQApi<GeneralCallResult>({
methodName: NTQQApiMethod.QUIT_GROUP,
args:[
{"groupCode": groupQQ},
null
]
})
}
}

View File

@@ -241,3 +241,42 @@ export interface RawMessage {
faceElement: FaceElement;
}[];
}
export enum GroupNotifyTypes {
INVITED_JOIN = 4, // 有人接受了邀请入群
JOIN_REQUEST = 7,
ADMIN_SET = 8,
ADMIN_UNSET = 12,
MEMBER_EXIT = 11, // 主动退出?
}
export interface GroupNotifies {
doubt: boolean,
nextStartSeq: string,
notifies: GroupNotify[],
}
export interface GroupNotify {
seq: string, // 转成数字再除以1000应该就是时间戳
type: GroupNotifyTypes,
status: 0, // 未知
group: { groupCode: string, groupName: string },
user1: { uid: string, nickName: string }, // 被设置管理员的人
user2: { uid: string, nickName: string }, // 操作者
actionUser: { uid: string, nickName: string }, //未知
actionTime: string,
invitationExt: {
srcType: number, // 0?未知
groupCode: string, waitStatus: number
},
postscript: string, // 加群用户填写的验证信息
repeatSeqs: [],
warningTips: string
}
export enum GroupRequestOperateTypes{
approve = 1,
reject = 2
}

View File

@@ -1,6 +1,6 @@
import {ActionName, BaseCheckResult} from "./types"
import {OB11Response, OB11WebsocketResponse} from "./utils"
import {OB11Return, OB11WebsocketReturn} from "../types";
import {OB11Response} from "./utils"
import {OB11Return} from "../types";
class BaseAction<PayloadType, ReturnDataType> {
actionName: ActionName
@@ -23,16 +23,16 @@ class BaseAction<PayloadType, ReturnDataType> {
}
}
public async websocketHandle(payload: PayloadType, echo: string): Promise<OB11WebsocketReturn<ReturnDataType | null>> {
public async websocketHandle(payload: PayloadType, echo: any): Promise<OB11Return<ReturnDataType | null>> {
const result = await this.check(payload)
if (!result.valid) {
return OB11WebsocketResponse.error(result.message, 1400)
return OB11Response.error(result.message, 1400)
}
try {
const resData = await this._handle(payload)
return OB11WebsocketResponse.ok(resData, echo);
return OB11Response.ok(resData, echo);
} catch (e) {
return OB11WebsocketResponse.error(e.toString(), 1200)
return OB11Response.error(e.toString(), 1200, echo)
}
}

View File

@@ -0,0 +1,9 @@
import BaseAction from "./BaseAction";
import {ActionName} from "./types";
export default class GetGuildList extends BaseAction<null, null>{
actionName = ActionName.GetGuildList
protected async _handle(payload: null): Promise<null> {
return null;
}
}

View File

@@ -0,0 +1,31 @@
import BaseAction from "./BaseAction";
import {getFriend} from "../../common/data";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {ActionName} from "./types";
import { log } from "../../common/utils";
interface Payload {
user_id: number,
times: number
}
export default class SendLike extends BaseAction<Payload, null> {
actionName = ActionName.SendLike
protected async _handle(payload: Payload): Promise<null> {
const qq = payload.user_id.toString();
const friend = await getFriend(qq)
if (!friend) {
throw (`点赞失败,${qq}不是好友`)
}
try {
let result = await NTQQApi.likeFriend(friend.uid, parseInt(payload.times.toString()) || 1);
if (result.result !== 0){
throw result.errMsg
}
} catch (e) {
throw `点赞失败 ${e}`
}
return null
}
}

View File

@@ -9,6 +9,7 @@ import {ActionName, BaseCheckResult} from "./types";
import * as fs from "fs";
import {log} from "../../common/utils";
import {v4 as uuidv4} from "uuid"
import {parseCQCode} from "../cqcode";
function checkSendMessage(sendMsgList: OB11MessageData[]) {
function checkUri(uri: string): boolean {
@@ -49,7 +50,7 @@ export interface ReturnDataType {
message_id: number
}
class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
export class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
actionName = ActionName.SendMsg
protected async check(payload: OB11PostSendMsg): Promise<BaseCheckResult> {
@@ -115,14 +116,15 @@ class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
}
}
private convertMessage2List(message: OB11MessageMixType) {
protected convertMessage2List(message: OB11MessageMixType) {
if (typeof message === "string") {
message = [{
type: OB11MessageDataType.text,
data: {
text: message
}
}] as OB11MessageData[]
// message = [{
// type: OB11MessageDataType.text,
// data: {
// text: message
// }
// }] as OB11MessageData[]
message = parseCQCode(message.toString())
} else if (!Array.isArray(message)) {
message = [message]
}
@@ -143,24 +145,27 @@ class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
peerUid: selfInfo.uid
}
let nodeIds: string[] = []
for (const messageNode of messageNodes) {
for (const messageNode of messageNodes){
// 一个node表示一个人的消息
let nodeId = messageNode.data.id;
// 有nodeId表示一个子转发消息卡片
if (nodeId) {
nodeIds.push(nodeId)
let nodeMsg = getHistoryMsgByShortId(nodeId);
if (nodeMsg){
nodeIds.push(nodeMsg.msgId);
}
} else {
// 自定义的消息
// 提取消息段发给自己生成消息id
const {
sendElements,
deleteAfterSentFiles
} = await this.createSendElements(this.convertMessage2List(messageNode.data.content), group)
try {
const {
sendElements,
deleteAfterSentFiles
} = await this.createSendElements(this.convertMessage2List(messageNode.data.content), group);
log("开始生成转发节点", sendElements);
const nodeMsg = await this.send(selfPeer, sendElements, deleteAfterSentFiles, true);
nodeIds.push(nodeMsg.msgId)
log("转发节点生成成功", nodeMsg.msgId);
} catch (e) {
log("生效转发消息节点失败", e)
}
@@ -240,7 +245,8 @@ class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
}
}
}
} break;
}
break;
}
}
@@ -255,7 +261,7 @@ class SendMsg extends BaseAction<OB11PostSendMsg, ReturnDataType> {
if (!sendElements.length) {
throw ("消息体无法解析")
}
const returnMsg = await NTQQApi.sendMsg(peer, sendElements, waitComplete)
const returnMsg = await NTQQApi.sendMsg(peer, sendElements, waitComplete, 20000);
addHistoryMsg(returnMsg)
deleteAfterSentFiles.map(f => fs.unlink(f, () => {
}))

View File

@@ -0,0 +1,30 @@
import BaseAction from "./BaseAction";
import {groupNotifies} from "../../common/data";
import {GroupNotify, GroupRequestOperateTypes} from "../../ntqqapi/types";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {ActionName} from "./types";
interface Payload{
flag: string,
// sub_type: "add" | "invite",
// type: "add" | "invite"
approve: boolean,
reason: string
}
export default class SetGroupAddRequest extends BaseAction<Payload, null>{
actionName = ActionName.SetGroupAddRequest
protected async _handle(payload: Payload): Promise<null> {
const seq = payload.flag.toString();
const notify: GroupNotify = groupNotifies[seq]
try{
await NTQQApi.handleGroupRequest(seq,
payload.approve ? GroupRequestOperateTypes.approve: GroupRequestOperateTypes.reject,
payload.reason
)
}catch (e) {
throw e
}
return null
}
}

View File

@@ -0,0 +1,22 @@
import BaseAction from "./BaseAction";
import {NTQQApi} from "../../ntqqapi/ntcall";
import {log} from "../../common/utils";
import {ActionName} from "./types";
interface Payload{
group_id: number,
is_dismiss: boolean
}
export default class SetGroupLeave extends BaseAction<Payload, any>{
actionName = ActionName.SetGroupLeave
protected async _handle(payload: Payload): Promise<any> {
try{
await NTQQApi.quitGroup(payload.group_id.toString())
}
catch (e) {
log("退群失败", e)
throw e
}
}
}

View File

@@ -0,0 +1,24 @@
import BaseAction from "../BaseAction";
import {OB11GroupMember, OB11User} from "../../types";
import {friends, getFriend, getGroupMember, groups} from "../../../common/data";
import {OB11Constructor} from "../../constructor";
import {ActionName} from "../types";
export default class GoCQHTTPGetStrangerInfo extends BaseAction<{user_id: number}, OB11User>{
actionName = ActionName.GoCQHTTP_GetStrangerInfo
protected async _handle(payload: { user_id: number }): Promise<OB11User> {
const user_id = payload.user_id.toString()
const friend = await getFriend(user_id)
if (friend){
return OB11Constructor.friend(friend);
}
for(const group of groups){
const member = await getGroupMember(group.groupCode, user_id)
if (member){
return OB11Constructor.groupMember(group.groupCode, member) as OB11User
}
}
throw ("查无此人")
}
}

View File

@@ -0,0 +1,15 @@
import SendMsg, {ReturnDataType} from "../SendMsg";
import {OB11MessageMixType, OB11PostSendMsg} from "../../types";
import {ActionName, BaseCheckResult} from "../types";
export class GoCQHTTPSendGroupForwardMsg extends SendMsg{
actionName = ActionName.GoCQHTTP_SendGroupForwardMsg;
protected async check(payload: OB11PostSendMsg){
payload.message = this.convertMessage2List(payload.messages);
return super.check(payload);
}
}
export class GoCQHTTPSendPrivateForwardMsg extends GoCQHTTPSendGroupForwardMsg{
actionName = ActionName.GoCQHTTP_SendPrivateForwardMsg;
}

View File

@@ -14,18 +14,34 @@ import GetVersionInfo from "./GetVersionInfo";
import CanSendRecord from "./CanSendRecord";
import CanSendImage from "./CanSendImage";
import GetStatus from "./GetStatus";
import {GoCQHTTPSendGroupForwardMsg, GoCQHTTPSendPrivateForwardMsg} from "./go-cqhttp/SendForwardMsg";
import GoCQHTTPGetStrangerInfo from "./go-cqhttp/GetStrangerInfo";
import SendLike from "./SendLike";
import SetGroupAddRequest from "./SetGroupAddRequest";
import SetGroupLeave from "./SetGroupLeave";
import GetGuildList from "./GetGuildList";
export const actionHandlers = [
new SendLike(),
new GetMsg(),
new GetLoginInfo(),
new GetFriendList(),
new GetGroupList(), new GetGroupInfo(), new GetGroupMemberList(), new GetGroupMemberInfo(),
new SendGroupMsg(), new SendPrivateMsg(), new SendMsg(),
new DeleteMsg(),
new SetGroupAddRequest(),
new SetGroupLeave(),
new GetVersionInfo(),
new CanSendRecord(),
new CanSendImage(),
new GetStatus()
new GetStatus(),
//以下为go-cqhttp api
new GoCQHTTPSendGroupForwardMsg(),
new GoCQHTTPSendPrivateForwardMsg(),
new GoCQHTTPGetStrangerInfo(),
new GetGuildList()
]
function initActionMap() {

View File

@@ -1,3 +1,5 @@
import GetGuildList from "./GetGuildList";
export type BaseCheckResult = ValidCheckResult | InvalidCheckResult
export interface ValidCheckResult {
@@ -13,6 +15,7 @@ export interface InvalidCheckResult {
export enum ActionName {
TestForwardMsg = "test_forward_msg",
SendLike = "send_like",
GetLoginInfo = "get_login_info",
GetFriendList = "get_friend_list",
GetGroupInfo = "get_group_info",
@@ -24,8 +27,15 @@ export enum ActionName {
SendGroupMsg = "send_group_msg",
SendPrivateMsg = "send_private_msg",
DeleteMsg = "delete_msg",
SetGroupAddRequest = "set_group_add_request",
SetGroupLeave = "set_group_leave",
GetVersionInfo = "get_version_info",
GetStatus = "get_status",
CanSendRecord = "can_send_record",
CanSendImage = "can_send_image",
// 以下为go-cqhttp api
GoCQHTTP_SendGroupForwardMsg = "send_group_forward_msg",
GoCQHTTP_SendPrivateForwardMsg = "send_private_forward_msg",
GoCQHTTP_GetStrangerInfo = "get_stranger_info",
GetGuildList = "get_guild_list",
}

View File

@@ -1,4 +1,5 @@
import {OB11Return, OB11WebsocketReturn} from '../types';
import {OB11Return} from '../types';
import {isNull} from '../../common/utils';
export class OB11Response {
static res<T>(data: T, status: string, retcode: number, message: string = ""): OB11Return<T> {
@@ -6,31 +7,25 @@ export class OB11Response {
status: status,
retcode: retcode,
data: data,
message: message
message: message,
wording: message,
echo: null
}
}
static ok<T>(data: T) {
return OB11Response.res<T>(data, "ok", 0)
}
static error(err: string, retcode: number) {
return OB11Response.res(null, "failed", retcode, err)
}
}
export class OB11WebsocketResponse {
static res<T>(data: T, status: string, retcode: number, echo: string, message: string = ""): OB11WebsocketReturn<T> {
return {
status: status,
retcode: retcode,
data: data,
echo: echo,
message: message
static ok<T>(data: T, echo: any = null) {
let res = OB11Response.res<T>(data, "ok", 0)
if (!isNull(echo)) {
res.echo = echo;
}
return res;
}
static ok<T>(data: T, echo: string = "") {
return OB11WebsocketResponse.res<T>(data, "ok", 0, echo)
}
static error(err: string, retcode: number, echo: string = "") {
return OB11WebsocketResponse.res(null, "failed", retcode, echo, err)
static error(err: string, retcode: number, echo: any = null) {
let res = OB11Response.res(null, "failed", retcode, err)
if (!isNull(echo)) {
res.echo = echo;
}
return res;
}
}

View File

@@ -7,10 +7,21 @@ import {
OB11MessageDataType,
OB11User
} from "./types";
import {AtType, ChatType, Group, GroupMember, IMAGE_HTTP_HOST, RawMessage, SelfInfo, User} from '../ntqqapi/types';
import {
AtType,
ChatType,
Friend,
Group,
GroupMember,
IMAGE_HTTP_HOST,
RawMessage,
SelfInfo,
User
} from '../ntqqapi/types';
import {getFriend, getGroupMember, getHistoryMsgBySeq, selfInfo} from '../common/data';
import {file2base64, getConfigUtil, log} from "../common/utils";
import {NTQQApi} from "../ntqqapi/ntcall";
import {EventType} from "./event/OB11BaseEvent";
export class OB11Constructor {
@@ -34,10 +45,10 @@ export class OB11Constructor {
font: 14,
sub_type: "friend",
message: [],
post_type: "message",
post_type: selfInfo.uin == msg.senderUin ? EventType.MESSAGE_SENT : EventType.MESSAGE,
}
if (msg.chatType == ChatType.group) {
resMsg.sub_type = "normal"
resMsg.sub_type = "normal" // 这里go-cqhttp是group而onebot11标准是normal, 蛋疼
resMsg.group_id = parseInt(msg.peerUin)
const member = await getGroupMember(msg.peerUin, msg.senderUin);
if (member) {
@@ -80,7 +91,14 @@ export class OB11Constructor {
}
} else if (element.textElement) {
message_data["type"] = "text"
resMsg.raw_message += message_data["data"]["text"] = element.textElement.content
let text= element.textElement.content
if (!text.trim()){
continue;
}
message_data["data"]["text"] = text
if (text){
resMsg.raw_message += text
}
} else if (element.picElement) {
message_data["type"] = "image"
message_data["data"]["file_id"] = element.picElement.fileUuid
@@ -96,7 +114,7 @@ export class OB11Constructor {
message_data["type"] = "reply"
const replyMsg = getHistoryMsgBySeq(element.replyElement.replayMsgSeq)
if (replyMsg) {
message_data["data"]["id"] = replyMsg.msgShortId
message_data["data"]["id"] = replyMsg.msgShortId.toString()
} else {
continue
}
@@ -123,7 +141,7 @@ export class OB11Constructor {
if (!enableLocalFile2Url) {
message_data.data.file = "file://" + filePath
} else { // 不使用本地路径
if (message_data.data.http_file) {
if (message_data.data.http_file && !message_data.data.http_file.startsWith(IMAGE_HTTP_HOST + "/download")) {
message_data.data.file = message_data.data.http_file
} else {
let {err, data} = await file2base64(filePath);
@@ -139,6 +157,7 @@ export class OB11Constructor {
resMsg.message.push(message_data);
}
}
resMsg.raw_message = resMsg.raw_message.trim();
return resMsg;
}
@@ -175,7 +194,8 @@ export class OB11Constructor {
group_id: parseInt(group_id),
user_id: parseInt(member.uin),
nickname: member.nick,
card: member.cardName
card: member.cardName,
role: OB11Constructor.groupMemberRole(member.role),
}
}

49
src/onebot11/cqcode.ts Normal file
View File

@@ -0,0 +1,49 @@
import {OB11MessageData} from "./types";
const pattern = /\[CQ:(\w+)((,\w+=[^,\]]*)*)\]/
function unescape(source: string) {
return String(source)
.replace(/&#91;/g, '[')
.replace(/&#93;/g, ']')
.replace(/&#44;/g, ',')
.replace(/&amp;/g, '&')
}
function from(source: string) {
const capture = pattern.exec(source)
if (!capture) return null
const [, type, attrs] = capture
const data: Record<string, any> = {}
attrs && attrs.slice(1).split(',').forEach((str) => {
const index = str.indexOf('=')
data[str.slice(0, index)] = unescape(str.slice(index + 1))
})
return {type, data, capture}
}
function h(type: string, data: any) {
return {
type,
data,
}
}
export function parseCQCode(source: string): OB11MessageData[] {
const elements: any[] = []
let result: ReturnType<typeof from>
while ((result = from(source))) {
const {type, data, capture} = result
if (capture.index) {
elements.push(h('text', {text: unescape(source.slice(0, capture.index))}))
}
elements.push(h(type, data))
source = source.slice(capture.index + capture[0].length)
}
if (source) elements.push(h('text', {text: unescape(source)}))
return elements
}
// const result = parseCQCode("[CQ:at,qq=114514]早上好啊[CQ:image,file=http://baidu.com/1.jpg,type=show,id=40004]")
// const result = parseCQCode("好好好")
// console.log(JSON.stringify(result))

View File

@@ -4,7 +4,8 @@ export enum EventType {
META = "meta_event",
REQUEST = "request",
NOTICE = "notice",
MESSAGE = "message"
MESSAGE = "message",
MESSAGE_SENT = "message_sent",
}

View File

@@ -1,6 +1,7 @@
import {OB11BaseNoticeEvent} from "./OB11BaseNoticeEvent";
import {OB11GroupNoticeEvent} from "./OB11GroupNoticeEvent";
export class OB11GroupAdminNoticeEvent extends OB11BaseNoticeEvent {
export class OB11GroupAdminNoticeEvent extends OB11GroupNoticeEvent {
notice_type = "group_admin"
sub_type: string // "set" | "unset"
sub_type: "set" | "unset" // "set" | "unset"
}

View File

@@ -2,7 +2,7 @@ import {OB11GroupNoticeEvent} from "./OB11GroupNoticeEvent";
export class OB11GroupDecreaseEvent extends OB11GroupNoticeEvent {
notice_type = "group_decrease";
sub_type = "leave"; // TODO: 实现其他几种子类型的识别 ("leave" | "kick" | "kick_me")
sub_type: "leave" | "kick" | "kick_me" = "leave"; // TODO: 实现其他几种子类型的识别 ("leave" | "kick" | "kick_me")
operate_id: number;
constructor(groupId: number, userId: number) {

View File

@@ -0,0 +1,9 @@
import {OB11GroupNoticeEvent} from "../notice/OB11GroupNoticeEvent";
export class OB11GroupRequestEvent extends OB11GroupNoticeEvent{
request_type: "group" = "group";
sub_type: "add" | "invite" = "add";
comment: string;
flag: string;
}

View File

@@ -29,10 +29,10 @@ export function postWsEvent(event: PostEventType) {
}
}
export function postEvent(msg: PostEventType) {
export function postEvent(msg: PostEventType, reportSelf=false) {
const config = getConfigUtil().getConfig();
// 判断msg是否是event
if (!config.reportSelfMessage) {
if (!config.reportSelfMessage && !reportSelf) {
if ((msg as OB11Message).user_id.toString() == selfInfo.uin) {
return
}

View File

@@ -4,7 +4,7 @@ import * as WebSocket from "ws";
import {selfInfo} from "../../../common/data";
import {LifeCycleSubType, OB11LifeCycleEvent} from "../../event/meta/OB11LifeCycleEvent";
import {ActionName} from "../../action/types";
import {OB11WebsocketResponse} from "../../action/utils";
import {OB11Response} from "../../action/utils";
import BaseAction from "../../action/BaseAction";
import {actionMap} from "../../action";
import {registerWsEventSender, unregisterWsEventSender} from "../postevent";
@@ -33,24 +33,24 @@ export class ReverseWebsocket {
}
public async onmessage(msg: string) {
let receiveData: { action: ActionName, params: any, echo?: string } = {action: null, params: {}}
let echo = ""
log("收到反向Websocket消息", msg.toString())
let receiveData: { action: ActionName, params: any, echo?: any } = {action: null, params: {}}
let echo = null
try {
receiveData = JSON.parse(msg.toString())
echo = receiveData.echo
log("收到反向Websocket消息", receiveData)
} catch (e) {
return wsReply(this.websocket, OB11WebsocketResponse.error("json解析失败请检查数据格式", 1400, echo))
return wsReply(this.websocket, OB11Response.error("json解析失败请检查数据格式", 1400, echo))
}
const action: BaseAction<any, any> = actionMap.get(receiveData.action);
if (!action) {
return wsReply(this.websocket, OB11WebsocketResponse.error("不支持的api " + receiveData.action, 1404, echo))
return wsReply(this.websocket, OB11Response.error("不支持的api " + receiveData.action, 1404, echo))
}
try {
let handleResult = await action.websocketHandle(receiveData.params, echo);
wsReply(this.websocket, handleResult)
} catch (e) {
wsReply(this.websocket, OB11WebsocketResponse.error(`api处理出错:${e}`, 1200, echo))
wsReply(this.websocket, OB11Response.error(`api处理出错:${e}`, 1200, echo))
}
}
@@ -89,18 +89,18 @@ export class ReverseWebsocket {
log("Trying to connect to the websocket server: " + this.url);
this.websocket.on("open", ()=> {
this.websocket.on("open", () => {
log("Connected to the websocket server: " + this.url);
this.onopen();
});
this.websocket.on("message", async (data)=>{
this.websocket.on("message", async (data) => {
await this.onmessage(data.toString());
});
this.websocket.on("error", log);
this.websocket.on("close", ()=> {
this.websocket.on("close", () => {
log("The websocket connection: " + this.url + " closed, trying reconnecting...");
this.onclose();
});

View File

@@ -1,7 +1,7 @@
import {WebSocket} from "ws";
import {getConfigUtil, log} from "../../../common/utils";
import {actionMap} from "../../action";
import {OB11WebsocketResponse} from "../../action/utils";
import {OB11Response} from "../../action/utils";
import {postWsEvent, registerWsEventSender, unregisterWsEventSender} from "../postevent";
import {ActionName} from "../../action/types";
import BaseAction from "../../action/BaseAction";
@@ -15,33 +15,33 @@ let heartbeatRunning = false;
class OB11WebsocketServer extends WebsocketServerBase {
authorizeFailed(wsClient: WebSocket) {
wsClient.send(JSON.stringify(OB11WebsocketResponse.res(null, "failed", 1403, "token验证失败")))
wsClient.send(JSON.stringify(OB11Response.res(null, "failed", 1403, "token验证失败")))
}
async handleAction(wsClient: WebSocket, actionName: string, params: any, echo?: string) {
async handleAction(wsClient: WebSocket, actionName: string, params: any, echo?: any) {
const action: BaseAction<any, any> = actionMap.get(actionName);
if (!action) {
return wsReply(wsClient, OB11WebsocketResponse.error("不支持的api " + actionName, 1404, echo))
return wsReply(wsClient, OB11Response.error("不支持的api " + actionName, 1404, echo))
}
try {
let handleResult = await action.websocketHandle(params, echo);
wsReply(wsClient, handleResult)
} catch (e) {
wsReply(wsClient, OB11WebsocketResponse.error(`api处理出错:${e}`, 1200, echo))
wsReply(wsClient, OB11Response.error(`api处理出错:${e}`, 1200, echo))
}
}
onConnect(wsClient: WebSocket, url: string, req: IncomingMessage) {
if (url == "/api" || url == "/api/" || url == "/") {
wsClient.on("message", async (msg) => {
let receiveData: { action: ActionName, params: any, echo?: string } = {action: null, params: {}}
let echo = ""
log("收到正向Websocket消息", msg.toString())
let receiveData: { action: ActionName, params: any, echo?: any } = {action: null, params: {}}
let echo = null
try {
receiveData = JSON.parse(msg.toString())
echo = receiveData.echo
log("收到正向Websocket消息", receiveData);
} catch (e) {
return wsReply(wsClient, OB11WebsocketResponse.error("json解析失败请检查数据格式", 1400, echo))
return wsReply(wsClient, OB11Response.error("json解析失败请检查数据格式", 1400, echo))
}
this.handleAction(wsClient, receiveData.action, receiveData.params, receiveData.echo).then()
})

View File

@@ -1,17 +1,17 @@
import * as websocket from "ws";
import {OB11WebsocketResponse} from "../../action/utils";
import {OB11Response} from "../../action/utils";
import {PostEventType} from "../postevent";
import {log} from "../../../common/utils";
import {isNull, log} from "../../../common/utils";
export function wsReply(wsClient: websocket.WebSocket, data: OB11WebsocketResponse | PostEventType) {
export function wsReply(wsClient: websocket.WebSocket, data: OB11Response | PostEventType) {
try {
let packet = Object.assign({
}, data);
if (!packet["echo"]){
if (isNull(packet["echo"])){
delete packet["echo"];
}
wsClient.send(JSON.stringify(packet))
log("ws 消息上报", wsClient.url, data)
log("ws 消息上报", wsClient.url || "", data)
} catch (e) {
log("websocket 回复失败", e)
}

View File

@@ -1,4 +1,5 @@
import {AtType, RawMessage} from "../ntqqapi/types";
import {EventType} from "./event/OB11BaseEvent";
export interface OB11User {
user_id: number;
@@ -67,7 +68,7 @@ export interface OB11Message {
message: OB11MessageData[],
raw_message: string,
font: number,
post_type?: "message",
post_type?: EventType,
raw?: RawMessage
}
@@ -76,10 +77,8 @@ export interface OB11Return<DataType> {
retcode: number
data: DataType
message: string,
}
export interface OB11WebsocketReturn<DataType> extends OB11Return<DataType>{
echo: string
echo?: any, // ws调用api才有此字段
wording?: string, // go-cqhttp字段错误信息
}
export enum OB11MessageDataType {
@@ -160,6 +159,7 @@ export interface OB11PostSendMsg {
user_id: string,
group_id?: string,
message: OB11MessageMixType;
messages?: OB11MessageMixType; // 兼容 go-cqhttp
}
export interface OB11Version {

View File

@@ -60,40 +60,4 @@ export async function uri2local(fileName: string, uri: string){
res.success = true
res.path = filePath
return res
}
function checkSendMessage(sendMsgList: OB11MessageData[]) {
function checkUri(uri: string): boolean {
const pattern = /^(file:\/\/|http:\/\/|https:\/\/|base64:\/\/)/;
return pattern.test(uri);
}
for (let msg of sendMsgList) {
if (msg["type"] && msg["data"]) {
let type = msg["type"];
let data = msg["data"];
if (type === "text" && !data["text"]) {
return 400;
} else if (["image", "voice", "record"].includes(type)) {
if (!data["file"]) {
return 400;
} else {
if (checkUri(data["file"])) {
return 200;
} else {
return 400;
}
}
} else if (type === "at" && !data["qq"]) {
return 400;
} else if (type === "reply" && !data["id"]) {
return 400;
}
} else {
return 400
}
}
return 200;
}
}

View File

@@ -109,7 +109,7 @@ async function onSettingWindowCreated(view: Element) {
<setting-item data-direction="row" class="hostItem vertical-list-item">
<div>
<div>上报文件不采用本地路径</div>
<div class="tips">开启后,上报图片为http连接语音为base64编码</div>
<div class="tips">开启后,上报文件(图片语音等)为http链接或base64编码</div>
</div>
<setting-switch id="switchFileUrl" ${config.enableLocalFile2Url ? "is-active" : ""}></setting-switch>
</setting-item>