Merge pull request #462 from LLOneBot/dev

release: 4.0.3
This commit is contained in:
idranme
2024-10-11 00:52:33 +08:00
committed by GitHub
16 changed files with 153 additions and 190 deletions

View File

@@ -4,7 +4,7 @@
"name": "LLOneBot", "name": "LLOneBot",
"slug": "LLOneBot", "slug": "LLOneBot",
"description": "实现 OneBot 11 和 Satori 协议,用于 QQ 机器人开发", "description": "实现 OneBot 11 和 Satori 协议,用于 QQ 机器人开发",
"version": "4.0.2", "version": "4.0.3",
"icon": "./icon.webp", "icon": "./icon.webp",
"authors": [ "authors": [
{ {

View File

@@ -18,7 +18,7 @@ import {
CHANNEL_UPDATE, CHANNEL_UPDATE,
CHANNEL_SET_CONFIG_CONFIRMED CHANNEL_SET_CONFIG_CONFIRMED
} from '../common/channels' } from '../common/channels'
import { hookNTQQApiCall, hookNTQQApiReceive } from '../ntqqapi/hook' import { startHook } from '../ntqqapi/hook'
import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade' import { checkNewVersion, upgradeLLOneBot } from '../common/utils/upgrade'
import { getConfigUtil } from '../common/config' import { getConfigUtil } from '../common/config'
import { checkFfmpeg } from '../common/utils/video' import { checkFfmpeg } from '../common/utils/video'
@@ -217,23 +217,11 @@ function onLoad() {
// 创建窗口时触发 // 创建窗口时触发
function onBrowserWindowCreated(window: BrowserWindow) { function onBrowserWindowCreated(window: BrowserWindow) {
if (![2, 4, 6].includes(window.id)) {
return
}
if (window.id === 2) {
mainWindow = window
}
//log('window create', window.webContents.getURL().toString())
try {
hookNTQQApiCall(window, window.id !== 2)
hookNTQQApiReceive(window, window.id !== 2)
} catch (e) {
log('LLOneBot hook error: ', String(e))
}
} }
try { try {
onLoad() onLoad()
startHook()
} catch (e) { } catch (e) {
console.log(e) console.log(e)
} }

View File

@@ -16,7 +16,7 @@ import {
SendTextElement, SendTextElement,
SendVideoElement, SendVideoElement,
} from './types' } from './types'
import { stat, writeFile, copyFile, unlink } from 'node:fs/promises' import { stat, writeFile, copyFile, unlink, access } from 'node:fs/promises'
import { calculateFileMD5 } from '../common/utils/file' import { calculateFileMD5 } from '../common/utils/file'
import { defaultVideoThumb, getVideoInfo } from '../common/utils/video' import { defaultVideoThumb, getVideoInfo } from '../common/utils/video'
import { encodeSilk } from '../common/utils/audio' import { encodeSilk } from '../common/utils/audio'
@@ -115,25 +115,17 @@ export namespace SendElement {
} }
export async function video(ctx: Context, filePath: string, fileName = '', diyThumbPath = ''): Promise<SendVideoElement> { export async function video(ctx: Context, filePath: string, fileName = '', diyThumbPath = ''): Promise<SendVideoElement> {
try { await access(filePath)
await stat(filePath)
} catch (e) {
throw `文件${filePath}异常,不存在`
}
ctx.logger.info('复制视频到QQ目录', filePath)
const { fileName: _fileName, path, fileSize, md5 } = await ctx.ntFileApi.uploadFile(filePath, ElementType.Video) const { fileName: _fileName, path, fileSize, md5 } = await ctx.ntFileApi.uploadFile(filePath, ElementType.Video)
ctx.logger.info('复制视频到QQ目录完成', path)
if (fileSize === 0) { if (fileSize === 0) {
throw '文件异常大小为0' throw new Error('文件异常,大小为 0')
} }
const maxMB = 100; const maxMB = 100
if (fileSize > 1024 * 1024 * maxMB) { if (fileSize > 1024 * 1024 * maxMB) {
throw `视频过大,最大支持${maxMB}MB当前文件大小${fileSize}B` throw new Error(`视频过大,最大支持${maxMB}MB当前文件大小${fileSize}B`)
} }
let thumbDir = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`) const thumbDir = pathLib.dirname(path.replaceAll('\\', '/').replace(`/Ori/`, `/Thumb/`))
thumbDir = pathLib.dirname(thumbDir)
// log("thumb 目录", thumb)
let videoInfo = { let videoInfo = {
width: 1920, width: 1920,
height: 1080, height: 1080,
@@ -194,7 +186,6 @@ export namespace SendElement {
const _thumbPath = await createThumb const _thumbPath = await createThumb
ctx.logger.info('生成视频缩略图', _thumbPath) ctx.logger.info('生成视频缩略图', _thumbPath)
const thumbSize = (await stat(_thumbPath)).size const thumbSize = (await stat(_thumbPath)).size
// log("生成缩略图", _thumbPath)
thumbPath.set(0, _thumbPath) thumbPath.set(0, _thumbPath)
const thumbMd5 = await calculateFileMD5(_thumbPath) const thumbMd5 = await calculateFileMD5(_thumbPath)
const element: SendVideoElement = { const element: SendVideoElement = {

View File

@@ -1,8 +1,7 @@
import type { BrowserWindow } from 'electron' import { NTMethod } from './ntcall'
import { NTClass, NTMethod } from './ntcall'
import { log } from '@/common/utils' import { log } from '@/common/utils'
import { randomUUID } from 'node:crypto' import { randomUUID } from 'node:crypto'
import { Dict } from 'cosmokit' import { ipcMain } from 'electron'
export const hookApiCallbacks: Record<string, (res: any) => void> = {} export const hookApiCallbacks: Record<string, (res: any) => void> = {}
@@ -28,19 +27,6 @@ export enum ReceiveCmdS {
MEDIA_UPLOAD_COMPLETE = 'nodeIKernelMsgListener/onRichMediaUploadComplete', MEDIA_UPLOAD_COMPLETE = 'nodeIKernelMsgListener/onRichMediaUploadComplete',
} }
type NTReturnData = [
{
type: 'request'
eventName: NTClass
callbackId?: string
},
{
cmdName: ReceiveCmdS
cmdType: 'event'
payload: unknown
}[]
]
const logHook = false const logHook = false
const receiveHooks: Array<{ const receiveHooks: Array<{
@@ -54,92 +40,64 @@ const callHooks: Array<{
hookFunc: (callParams: unknown[]) => void | Promise<void> hookFunc: (callParams: unknown[]) => void | Promise<void>
}> = [] }> = []
export function hookNTQQApiReceive(window: BrowserWindow, onlyLog: boolean) { export function startHook() {
window.webContents.send = new Proxy(window.webContents.send, { const senderExclude = Symbol()
apply(target, thisArg, args: [channel: string, ...args: NTReturnData]) {
try { ipcMain.emit = new Proxy(ipcMain.emit, {
if (logHook && !args[1]?.eventName?.startsWith('ns-LoggerApi')) { apply(target, thisArg, args: [eventName: string, ...args: any]) {
log('received ntqq api message', args) if (args[2]?.eventName.startsWith('ns-LoggerApi')) {
} return target.apply(thisArg, args)
} catch { } }
if (!onlyLog) { if (logHook) {
if (args[2] instanceof Array) { log('request', args)
for (const receiveData of args[2]) { }
const ntMethodName = receiveData.cmdName
for (const hook of receiveHooks) { const event = args[1]
if (hook.method.includes(ntMethodName)) { if (event.sender && !event.sender[senderExclude]) {
Promise.resolve(hook.hookFunc(receiveData.payload)) event.sender[senderExclude] = true
event.sender.send = new Proxy(event.sender.send, {
apply(target, thisArg, args: any[]) {
if (args[1].eventName?.startsWith('ns-LoggerApi')) {
return target.apply(thisArg, args)
}
if (logHook) {
log('received', args)
}
const callbackId = args[1].callbackId
if (callbackId) {
if (hookApiCallbacks[callbackId]) {
Promise.resolve(hookApiCallbacks[callbackId](args[2]))
delete hookApiCallbacks[callbackId]
}
} else if (args[2]) {
for (const receiveData of args[2]) {
for (const hook of receiveHooks) {
if (hook.method.includes(receiveData.cmdName)) {
Promise.resolve(hook.hookFunc(receiveData.payload))
}
}
} }
} }
return target.apply(thisArg, args)
} }
} })
if (args[1]?.callbackId) { }
const callbackId = args[1].callbackId
if (hookApiCallbacks[callbackId]) { if (args[3]?.length) {
Promise.resolve(hookApiCallbacks[callbackId](args[2])) const method = args[3][0]
delete hookApiCallbacks[callbackId] const callParams = args[3].slice(1)
for (const hook of callHooks) {
if (hook.method.includes(method)) {
Promise.resolve(hook.hookFunc(callParams))
} }
} }
} }
return target.apply(thisArg, args) return target.apply(thisArg, args)
}, }
}) })
} }
export function hookNTQQApiCall(window: BrowserWindow, onlyLog: boolean) {
const webContents = window.webContents as Dict
const ipc_message_proxy = webContents._events['-ipc-message']?.[0] || webContents._events['-ipc-message']
const proxyIpcMsg = new Proxy(ipc_message_proxy, {
apply(target, thisArg, args) {
const isLogger = args[3]?.[0]?.eventName?.startsWith('ns-LoggerApi')
if (!isLogger) {
try {
logHook && log('call NTQQ api', args)
} catch (e) { }
if (!onlyLog) {
try {
const _args: unknown[] = args[3][1]
const cmdName = _args[0] as NTMethod
const callParams = _args.slice(1)
callHooks.forEach((hook) => {
if (hook.method.includes(cmdName)) {
Promise.resolve(hook.hookFunc(callParams))
}
})
} catch { }
}
}
return target.apply(thisArg, args)
},
})
if (webContents._events['-ipc-message']?.[0]) {
webContents._events['-ipc-message'][0] = proxyIpcMsg
} else {
webContents._events['-ipc-message'] = proxyIpcMsg
}
/*const ipc_invoke_proxy = webContents._events['-ipc-invoke']?.[0] || webContents._events['-ipc-invoke']
const proxyIpcInvoke = new Proxy(ipc_invoke_proxy, {
apply(target, thisArg, args) {
//HOOK_LOG && log('call NTQQ invoke api', thisArg, args)
args[0]['_replyChannel']['sendReply'] = new Proxy(args[0]['_replyChannel']['sendReply'], {
apply(sendtarget, sendthisArg, sendargs) {
sendtarget.apply(sendthisArg, sendargs)
},
})
const ret = target.apply(thisArg, args)
//HOOK_LOG && log('call NTQQ invoke api return', ret)
return ret
},
})
if (webContents._events['-ipc-invoke']?.[0]) {
webContents._events['-ipc-invoke'][0] = proxyIpcInvoke
} else {
webContents._events['-ipc-invoke'] = proxyIpcInvoke
}*/
}
export function registerReceiveHook<PayloadType>( export function registerReceiveHook<PayloadType>(
method: string | string[], method: string | string[],
hookFunc: (payload: PayloadType) => void, hookFunc: (payload: PayloadType) => void,

View File

@@ -123,8 +123,8 @@ export function invoke<
const apiArgs = [method, ...args] const apiArgs = [method, ...args]
const callbackId = randomUUID() const callbackId = randomUUID()
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
log(`ntqq api timeout ${channel}, ${eventName}, ${method}`, apiArgs) log(`ntqq api timeout ${channel}, ${eventName}, ${method}`, args)
reject(`ntqq api timeout ${channel}, ${eventName}, ${method}, ${apiArgs}`) reject(`ntqq api timeout ${channel}, ${eventName}, ${method}, ${JSON.stringify(args)}`)
}, timeout) }, timeout)
if (!options.cbCmd) { if (!options.cbCmd) {

View File

@@ -95,4 +95,6 @@ export interface NodeIKernelMsgService {
getMultiMsg(...args: unknown[]): Promise<GeneralCallResult & { msgList: RawMessage[] }> getMultiMsg(...args: unknown[]): Promise<GeneralCallResult & { msgList: RawMessage[] }>
getTempChatInfo(chatType: number, uid: string): Promise<TmpChatInfoApi> getTempChatInfo(chatType: number, uid: string): Promise<TmpChatInfoApi>
sendSsoCmdReqByContend(ssoCmd: string, content: string): Promise<GeneralCallResult & { rsp: string }>
} }

View File

@@ -1,5 +1,3 @@
import { QQLevel, Sex } from './user'
export enum GroupListUpdateType { export enum GroupListUpdateType {
REFRESHALL, REFRESHALL,
GETALL, GETALL,
@@ -35,36 +33,54 @@ export interface Group {
memberUin: string memberUin: string
memberUid: string memberUid: string
} }
members: GroupMember[] // 原始数据是没有这个的,为了方便自己加了这个字段
createTime: string createTime: string
} }
export enum GroupMemberRole { export enum GroupMemberRole {
normal = 2, Normal = 2,
admin = 3, Admin = 3,
owner = 4, Owner = 4,
} }
export interface GroupMember { export interface GroupMember {
memberSpecialTitle?: string uid: string
avatarPath: string
cardName: string
cardType: number
isDelete: boolean
nick: string
qid: string qid: string
uin: string
nick: string
remark: string remark: string
role: GroupMemberRole // 群主:4, 管理员:3群员:2 cardType: number
shutUpTime: number // 禁言时间,单位是什么暂时不清楚 cardName: string
uid: string // 加密的字符串 role: GroupMemberRole
uin: string // QQ号 avatarPath: string
shutUpTime: number
isDelete: boolean
isSpecialConcerned: boolean
isSpecialShield: boolean
isRobot: boolean isRobot: boolean
sex?: Sex groupHonor: Uint8Array
qqLevel?: QQLevel memberRealLevel: number
isChangeRole: boolean memberLevel: number
globalGroupLevel: number
globalGroupPoint: number
memberTitleId: number
memberSpecialTitle: string
specialTitleExpireTime: string
userShowFlag: number
userShowFlagNew: number
richFlag: number
mssVipType: number
bigClubLevel: number
bigClubFlag: number
autoRemark: string
creditLevel: number
joinTime: number joinTime: number
lastSpeakTime: number lastSpeakTime: number
memberLevel: number memberFlag: number
memberFlagExt: number
memberMobileFlag: number
memberFlagExt2: number
isSpecialShielded: boolean
cardNameId: number
} }
export interface PublishGroupBulletinReq { export interface PublishGroupBulletinReq {

View File

@@ -67,6 +67,7 @@ export interface User {
recommendImgFlag?: number recommendImgFlag?: number
disableEmojiShortCuts?: number disableEmojiShortCuts?: number
pendantId?: string pendantId?: string
age?: number
} }
export interface SelfInfo extends User { export interface SelfInfo extends User {

View File

@@ -26,7 +26,7 @@ export class GetStrangerInfo extends BaseAction<Payload, OB11User> {
...extendData, ...extendData,
user_id: parseInt(extendData.info.uin) || 0, user_id: parseInt(extendData.info.uin) || 0,
nickname: extendData.info.nick, nickname: extendData.info.nick,
sex: OB11UserSex.unknown, sex: OB11UserSex.Unknown,
age: (extendData.info.birthday_year == 0) ? 0 : new Date().getFullYear() - extendData.info.birthday_year, age: (extendData.info.birthday_year == 0) ? 0 : new Date().getFullYear() - extendData.info.birthday_year,
qid: extendData.info.qid, qid: extendData.info.qid,
level: extendData.info.qqLevel && calcQQLevel(extendData.info.qqLevel) || 0, level: extendData.info.qqLevel && calcQQLevel(extendData.info.qqLevel) || 0,
@@ -46,7 +46,7 @@ export class GetStrangerInfo extends BaseAction<Payload, OB11User> {
...extendData, ...extendData,
user_id: parseInt(extendData.detail.uin) || 0, user_id: parseInt(extendData.detail.uin) || 0,
nickname: extendData.detail.simpleInfo.coreInfo.nick, nickname: extendData.detail.simpleInfo.coreInfo.nick,
sex: OB11UserSex.unknown, sex: OB11UserSex.Unknown,
age: 0, age: 0,
level: extendData.detail.commonExt.qqLevel && calcQQLevel(extendData.detail.commonExt.qqLevel) || 0, level: extendData.detail.commonExt.qqLevel && calcQQLevel(extendData.detail.commonExt.qqLevel) || 0,
login_days: 0, login_days: 0,

View File

@@ -2,7 +2,7 @@ import { BaseAction, Schema } from '../BaseAction'
import { OB11GroupMember } from '../../types' import { OB11GroupMember } from '../../types'
import { OB11Entities } from '../../entities' import { OB11Entities } from '../../entities'
import { ActionName } from '../types' import { ActionName } from '../types'
import { isNullable } from 'cosmokit' import { calcQQLevel } from '@/common/utils/misc'
interface Payload { interface Payload {
group_id: number | string group_id: number | string
@@ -22,14 +22,14 @@ class GetGroupMemberInfo extends BaseAction<Payload, OB11GroupMember> {
if (!uid) throw new Error('无法获取用户信息') if (!uid) throw new Error('无法获取用户信息')
const member = await this.ctx.ntGroupApi.getGroupMember(groupCode, uid) const member = await this.ctx.ntGroupApi.getGroupMember(groupCode, uid)
if (member) { if (member) {
if (isNullable(member.sex)) {
const info = await this.ctx.ntUserApi.getUserDetailInfo(member.uid)
Object.assign(member, info)
}
const ret = OB11Entities.groupMember(groupCode, member) const ret = OB11Entities.groupMember(groupCode, member)
const date = Math.round(Date.now() / 1000) const date = Math.round(Date.now() / 1000)
ret.last_sent_time ??= date ret.last_sent_time ??= date
ret.join_time ??= date ret.join_time ??= date
const info = await this.ctx.ntUserApi.getUserDetailInfo(member.uid)
ret.sex = OB11Entities.sex(info.sex!)
ret.qq_level = (info.qqLevel && calcQQLevel(info.qqLevel)) || 0
ret.age = info.age ?? 0
return ret return ret
} }
throw new Error(`群成员${payload.user_id}不存在`) throw new Error(`群成员${payload.user_id}不存在`)

View File

@@ -25,7 +25,7 @@ export default class SetGroupAdmin extends BaseAction<Payload, null> {
await this.ctx.ntGroupApi.setMemberRole( await this.ctx.ntGroupApi.setMemberRole(
groupCode, groupCode,
uid, uid,
payload.enable ? GroupMemberRole.admin : GroupMemberRole.normal payload.enable ? GroupMemberRole.Admin : GroupMemberRole.Normal
) )
return null return null
} }

View File

@@ -647,21 +647,21 @@ export namespace OB11Entities {
return raw.map(friendV2) return raw.map(friendV2)
} }
export function groupMemberRole(role: number): OB11GroupMemberRole | undefined { export function groupMemberRole(role: number): OB11GroupMemberRole {
return { return {
4: OB11GroupMemberRole.owner, 4: OB11GroupMemberRole.Owner,
3: OB11GroupMemberRole.admin, 3: OB11GroupMemberRole.Admin,
2: OB11GroupMemberRole.member, 2: OB11GroupMemberRole.Member,
}[role] }[role] ?? OB11GroupMemberRole.Member
} }
export function sex(sex: Sex): OB11UserSex { export function sex(sex: Sex): OB11UserSex {
const sexMap = { const sexMap = {
[Sex.male]: OB11UserSex.male, [Sex.male]: OB11UserSex.Male,
[Sex.female]: OB11UserSex.female, [Sex.female]: OB11UserSex.Female,
[Sex.unknown]: OB11UserSex.unknown, [Sex.unknown]: OB11UserSex.Unknown,
} }
return sexMap[sex] || OB11UserSex.unknown return sexMap[sex] || OB11UserSex.Unknown
} }
export function groupMember(group_id: string, member: GroupMember): OB11GroupMember { export function groupMember(group_id: string, member: GroupMember): OB11GroupMember {
@@ -670,20 +670,20 @@ export namespace OB11Entities {
user_id: parseInt(member.uin), user_id: parseInt(member.uin),
nickname: member.nick, nickname: member.nick,
card: member.cardName, card: member.cardName,
sex: sex(member.sex!), sex: OB11UserSex.Unknown,
age: 0, age: 0,
area: '', area: '',
level: String(member.memberLevel ?? 0), level: String(member.memberLevel ?? 0),
qq_level: (member.qqLevel && calcQQLevel(member.qqLevel)) || 0, qq_level: 0,
join_time: member.joinTime, join_time: member.joinTime,
last_sent_time: member.lastSpeakTime, last_sent_time: member.lastSpeakTime,
title_expire_time: 0, title_expire_time: +member.specialTitleExpireTime,
unfriendly: false, unfriendly: false,
card_changeable: true, card_changeable: true,
is_robot: member.isRobot, is_robot: member.isRobot,
shut_up_timestamp: member.shutUpTime, shut_up_timestamp: member.shutUpTime,
role: groupMemberRole(member.role), role: groupMemberRole(member.role),
title: member.memberSpecialTitle || '', title: member.memberSpecialTitle,
} }
} }

View File

@@ -57,7 +57,7 @@ export async function createSendElements(
.RemainAtAllCountForUin .RemainAtAllCountForUin
ctx.logger.info(`${groupCode}剩余at全体次数`, remainAtAllCount) ctx.logger.info(`${groupCode}剩余at全体次数`, remainAtAllCount)
const self = await ctx.ntGroupApi.getGroupMember(groupCode, selfInfo.uid) const self = await ctx.ntGroupApi.getGroupMember(groupCode, selfInfo.uid)
isAdmin = self?.role === GroupMemberRole.admin || self?.role === GroupMemberRole.owner isAdmin = self?.role === GroupMemberRole.Admin || self?.role === GroupMemberRole.Owner
} catch (e) { } catch (e) {
} }
} }
@@ -67,8 +67,15 @@ export async function createSendElements(
} }
else if (peer.chatType === ChatType.Group) { else if (peer.chatType === ChatType.Group) {
const uid = await ctx.ntUserApi.getUidByUin(atQQ) ?? '' const uid = await ctx.ntUserApi.getUidByUin(atQQ) ?? ''
const atNmae = sendMsg.data?.name let display = ''
const display = atNmae ? `@${atNmae}` : '' if (sendMsg.data.name) {
display = `@${sendMsg.data.name}`
} else {
try {
const member = await ctx.ntGroupApi.getGroupMember(peer.peerUid, uid)
display = `@${member.cardName || member.nick}`
} catch { }
}
sendElements.push(SendElement.at(atQQ, uid, AtType.One, display)) sendElements.push(SendElement.at(atQQ, uid, AtType.One, display))
} }
} }

View File

@@ -16,36 +16,36 @@ export interface OB11User {
} }
export enum OB11UserSex { export enum OB11UserSex {
male = 'male', Male = 'male',
female = 'female', Female = 'female',
unknown = 'unknown', Unknown = 'unknown',
} }
export enum OB11GroupMemberRole { export enum OB11GroupMemberRole {
owner = 'owner', Owner = 'owner',
admin = 'admin', Admin = 'admin',
member = 'member', Member = 'member',
} }
export interface OB11GroupMember { export interface OB11GroupMember {
group_id: number group_id: number
user_id: number user_id: number
nickname: string nickname: string
card?: string card: string
sex?: OB11UserSex sex: OB11UserSex
age?: number age: number
join_time?: number join_time: number
last_sent_time?: number last_sent_time: number
level?: string level: string
qq_level?: number qq_level?: number
role?: OB11GroupMemberRole role: OB11GroupMemberRole
title?: string title: string
area?: string area: string
unfriendly?: boolean unfriendly: boolean
title_expire_time?: number title_expire_time: number
card_changeable?: boolean card_changeable: boolean
// 以下为gocq字段 // 以下为gocq字段
shut_up_timestamp?: number shut_up_timestamp: number
// 以下为扩展字段 // 以下为扩展字段
is_robot?: boolean is_robot?: boolean
qage?: number qage?: number

View File

@@ -56,7 +56,7 @@ export class SatoriServer {
const { listen, port } = this.config const { listen, port } = this.config
this.httpServer = this.express.listen(port, listen, () => { this.httpServer = this.express.listen(port, listen, () => {
this.ctx.logger.info(`HTTP server started ${listen}:${port}`) this.ctx.logger.info(`server started ${listen}:${port}`)
}) })
this.wsServer = new WebSocketServer({ this.wsServer = new WebSocketServer({
server: this.httpServer server: this.httpServer
@@ -65,7 +65,7 @@ export class SatoriServer {
this.wsServer.on('connection', (socket, req) => { this.wsServer.on('connection', (socket, req) => {
const url = req.url?.split('?').shift() const url = req.url?.split('?').shift()
if (!['/v1/events', '/v1/events/'].includes(url!)) { if (!['/v1/events', '/v1/events/'].includes(url!)) {
return socket.close() return socket.close(1008, 'invalid address')
} }
socket.addEventListener('message', async (event) => { socket.addEventListener('message', async (event) => {

View File

@@ -1 +1 @@
export const version = '4.0.2' export const version = '4.0.3'