Skip to content

Commit

Permalink
feat(chat): message upload
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Apr 4, 2021
1 parent cf4be06 commit eb1ed85
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 75 deletions.
1 change: 1 addition & 0 deletions packages/adapter-onebot/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from './types'

export const adaptUser = (user: OneBot.AccountInfo): Koishi.UserInfo => ({
userId: user.userId.toString(),
avatar: `http://q.qlogo.cn/headimg_dl?dst_uin=${user.userId}&spec=640`,
username: user.nickname,
})

Expand Down
4 changes: 2 additions & 2 deletions packages/koishi-core/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import { contain, observe, Logger, defineProperty, Random, template, remove } fr
import { Argv } from './parser'
import { Middleware, NextFunction } from './context'
import { App } from './app'
import { Bot, ChannelInfo, MessageBase, Platform } from './adapter'
import { Bot, ChannelInfo, GroupInfo, MessageBase, Platform } from './adapter'

const logger = new Logger('session')

type UnionToIntersection<U> = (U extends any ? (key: U) => void : never) extends (key: infer I) => void ? I : never
type Flatten<T, K extends keyof T = keyof T> = UnionToIntersection<T[K]>
type InnerKeys<T, K extends keyof T = keyof T> = keyof Flatten<T> & keyof Flatten<T, K>

export interface Session<U, G, P, X, Y> extends MessageBase, Partial<ChannelInfo> {}
export interface Session<U, G, P, X, Y> extends MessageBase, Partial<ChannelInfo>, Partial<GroupInfo> {}

export namespace Session {
type Genres = 'friend' | 'channel' | 'group' | 'group-member' | 'group-role' | 'group-file' | 'group-emoji'
Expand Down
7 changes: 4 additions & 3 deletions packages/plugin-chat/client/chat.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<template>
<k-chat-panel class="sandbox" :messages="messages">
<template #default="{ username, content }">
<k-chat-panel class="sandbox" :messages="messages" pinned>
<template #default="{ channelName, username, content }">
<p>
{{ username }}: <k-message :content="content"/>
[{{ channelName || '私聊' }}] {{ username }}:
<k-message :content="content"/>
</p>
</template>
</k-chat-panel>
Expand Down
159 changes: 89 additions & 70 deletions packages/plugin-chat/src/debug.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Bot, Context, Session } from 'koishi-core'
import { Logger, segment, Time, interpolate, pick } from 'koishi-utils'
import { Logger, template, Time, pick, segment } from 'koishi-utils'

export interface DebugConfig {
formatSend?: string
formatReceive?: string
includeUsers?: string[]
includeChannels?: string[]
refreshUserName?: number
refreshGroupName?: number
refreshChannelName?: number
}

Expand All @@ -28,8 +27,9 @@ const cqTypes = {
card: '卡片消息',
}

interface Params {
interface Message {
content?: string
abstract?: string
username?: string
nickname?: string
platform?: string
Expand All @@ -41,11 +41,6 @@ interface Params {
groupName?: string
}

function getDeps(template: string) {
const cap = template.match(/\{\{[\s\S]+?\}\}/g) || []
return cap.map(seg => seg.slice(2, -2).trim())
}

async function getUserName(bot: Bot, groupId: string, userId: string) {
try {
const { username } = await bot.getGroupMember(groupId, userId)
Expand All @@ -55,6 +50,15 @@ async function getUserName(bot: Bot, groupId: string, userId: string) {
}
}

async function getGroupName(bot: Bot, groupId: string) {
try {
const { groupName } = await bot.getGroup(groupId)
return groupName
} catch {
return groupId
}
}

async function getChannelName(bot: Bot, channelId: string) {
try {
const { channelName } = await bot.getChannel(channelId)
Expand All @@ -64,105 +68,120 @@ async function getChannelName(bot: Bot, channelId: string) {
}
}

template.set('chat', {
send: '[{{ channelName || "私聊" }}] {{ abstract }}',
receive: '[{{ channelName || "私聊" }}] {{ username }}: {{ abstract }}',
})

export default function apply(ctx: Context, config: DebugConfig = {}) {
const {
formatSend = '[{{ channelName }}] {{ content }}',
formatReceive = '[{{ channelName }}] {{ username }}: {{ content }}',
refreshUserName = Time.hour,
refreshGroupName = Time.hour,
refreshChannelName = Time.hour,
includeUsers = [],
includeChannels = [],
includeUsers,
includeChannels,
} = config

const sendDeps = getDeps(formatSend)
const receiveDeps = getDeps(formatReceive)

const logger = new Logger('message')
Logger.levels.message = 3

const tasks: Record<string, (session: Session.Message) => Promise<any>> = {}
const channelMap: Record<string, [string | Promise<string>, number]> = {}
const userMap: Record<string, [string | Promise<string>, number]> = {}

function on<K extends keyof Params>(key: K, callback: (session: Session.Message) => Promise<Params[K]>) {
tasks[key] = callback
}
const channelMap: Record<string, [Promise<string>, number]> = {}
const groupMap: Record<string, [Promise<string>, number]> = {}
const userMap: Record<string, [Promise<string>, number]> = {}

ctx.on('connect', () => {
const timestamp = Date.now()
ctx.bots.forEach(bot => userMap[bot.sid] = [bot.username, timestamp])
ctx.bots.forEach(bot => userMap[bot.sid] = [Promise.resolve(bot.username), timestamp])
})

on('content', async (session) => {
async function prepareChannelName(session: Session, params: Message, timestamp: number) {
const { cid, groupId, channelName } = session
if (channelName) {
channelMap[cid] = [Promise.resolve(channelName), timestamp]
return
}
if (!groupId) return
if (!channelMap[cid] || timestamp - channelMap[cid][1] >= refreshChannelName) {
channelMap[cid] = [getChannelName(session.bot, session.channelId), timestamp]
}
params.channelName = await channelMap[cid][0]
}

async function prepareGroupName(session: Session, params: Message, timestamp: number) {
const { cid, gid, groupId, groupName } = session
if (groupName) {
groupMap[gid] = [Promise.resolve(groupName), timestamp]
return
}
if (!groupId || cid === gid) return
if (!groupMap[gid] || timestamp - groupMap[gid][1] >= refreshGroupName) {
groupMap[gid] = [getGroupName(session.bot, groupId), timestamp]
}
params.groupName = await groupMap[gid][0]
}

async function prepareAbstract(session: Session, params: Message, timestamp: number) {
const codes = segment.parse(session.content.split('\n', 1)[0])
let output = ''
params.abstract = ''
for (const code of codes) {
if (textSegmentTypes.includes(code.type)) {
output += segment.unescape(code.data.content)
params.abstract += segment.unescape(code.data.content)
} else if (code.type === 'at') {
if (code.data.type === 'all') {
output += '@全体成员'
params.abstract += '@全体成员'
} else if (session.subtype === 'group') {
const id = `${session.platform}:${code.data.id}`
const timestamp = Date.now()
if (!userMap[id] || timestamp - userMap[id][1] >= refreshUserName) {
userMap[id] = [getUserName(session.bot, session.groupId, code.data.id), timestamp]
}
output += '@' + await userMap[id][0]
params.abstract += '@' + await userMap[id][0]
} else {
output += '@' + session.bot.username
params.abstract += '@' + session.bot.username
}
} else if (code.type === 'share' || code.type === 'location') {
output += `[分享:${code.data.title}]`
params.abstract += `[分享:${code.data.title}]`
} else if (code.type === 'contact') {
output += `[推荐${code.data.type === 'qq' ? '好友' : '群'}:${code.data.id}]`
params.abstract += `[推荐${code.data.type === 'qq' ? '好友' : '群'}:${code.data.id}]`
} else {
output += `[${cqTypes[code.type] || '未知'}]`
params.abstract += `[${cqTypes[code.type] || '未知'}]`
}
}
return output
})
}

on('channelName', async (session) => {
const timestamp = Date.now()
function handleMessage(session: Session) {
const params: Message = pick(session, ['content', 'platform', 'channelId', 'channelName', 'groupId', 'groupName', 'userId', 'selfId'])
Object.assign(params, pick(session.author, ['username', 'nickname']))
if (session.type === 'message') {
userMap[session.uid] = [Promise.resolve(session.author.username), Date.now()]
}
const { cid, channelName } = session
if (channelName) return (channelMap[cid] = [channelName, timestamp])[0]
if (session.subtype === 'private') return '私聊'
if (!channelMap[cid] || timestamp - channelMap[cid][1] >= refreshChannelName) {
channelMap[cid] = [getChannelName(session.bot, session.channelId), timestamp]
const timestamp = Date.now()
if (channelName) {
session.channelName = channelName
channelMap[cid] = [Promise.resolve(channelName), timestamp]
}
return await channelMap[cid][0]
})

async function handleMessage(deps: string[], template: string, session: Session.Message) {
const params: Params = pick(session, ['platform', 'channelId', 'groupId', 'userId', 'selfId'])
Object.assign(params, pick(session.author, ['username', 'nickname']))
await Promise.all(deps.map(async (key) => {
const callback = tasks[key]
params[key] ||= await callback(session)
}))
logger.debug(interpolate(template, params))
dispatchMessage(session, params, timestamp)
}

ctx.intersect((session) => {
async function dispatchMessage(session: Session, params: Message, timestamp: number) {
await Promise.all([prepareChannelName, prepareGroupName, prepareAbstract].map(cb => cb(session, params, timestamp)))

console.log(params)

// webui
ctx?.webui?.adapter.broadcast('chat', params)

// logger
if (session.subtype === 'private') {
return includeUsers.length ? includeUsers.includes(session.userId) : true
if (includeUsers && !includeUsers.includes(session.userId)) return
} else {
return includeChannels.length ? includeChannels.includes(session.channelId) : true
if (includeChannels && !includeChannels.includes(session.channelId)) return
const { assignee } = await session.observeChannel(['assignee'])
if (assignee !== session.selfId) return
}
}).plugin((ctx) => {
ctx.on('message', async (session) => {
if (session.subtype === 'group') {
const { assignee } = await session.observeChannel(['assignee'])
if (assignee !== session.selfId) return
}

userMap[session.uid] = [session.author.username, Date.now()]
handleMessage(receiveDeps, formatReceive, session)
})
logger.debug(template('chat.' + (session.type === 'message' ? 'receive' : 'send'), params))
}

ctx.before('send', (session) => {
handleMessage(sendDeps, formatSend, session)
})
})
ctx.on('message', handleMessage)
ctx.before('send', handleMessage)
}

0 comments on commit eb1ed85

Please sign in to comment.