Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add adapter for infoflow (如流) #328

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
- [x] WeCom (企业微信)
- [x] Wechat Official (微信公众号)
- [x] Zulip

- [x] Infoflow(如流)
## Examples

### Basic usage
Expand Down
38 changes: 38 additions & 0 deletions adapters/infoflow/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "koishi-plugin-infoflow-adapter",
"description": "如流适配器",
"version": "0.1.0",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
"contributors": [
"firstmeet <[email protected]>"
],
"homepage": "https://github.com/firstmeet1108/infoflow-adapter",
"files": [
"lib",
"dist"
],
"license": "MIT",
"keywords": [
"chatbot",
"koishi",
"plugin",
"infoflow-adapter",
"koishi-plugin",
"koishi-plugin-infoflow-adapter",
"koishi-plugin-infoflow",
"如流",
"如流适配器",
"如流适配器插件",
"百度"
],
"peerDependencies": {
"koishi": "^4.18.1"
},
"dependencies": {
"crypto-js": "^4.2.0"
},
"devDependencies": {
"@types/crypto-js": "^4"
}
}
10 changes: 10 additions & 0 deletions adapters/infoflow/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# koishi-plugin-infoflow-adapter

[![npm](https://img.shields.io/npm/v/koishi-plugin-infoflow-adapter?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-infoflow-adapter)

# 如流适配器

## 基础设置说明

- robotId 需要接收到推送消息后从消息体中提取
- 其余信息从如流Bot设置中获取
61 changes: 61 additions & 0 deletions adapters/infoflow/src/bot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Bot, Context, HTTP, Schema } from '@satorijs/core'
import { Internal } from './type'
import { HttpServer } from './http'
import { getParam } from './utils'
import { InfoflowMessageEncoder } from './message'
export class InfoflowBot<C extends Context = Context> extends Bot<C, InfoflowBot.Config> {
static inject = ['server', 'http']
static MessageEncoder = InfoflowMessageEncoder
http: HTTP
internal: Internal

constructor(ctx: C, config: InfoflowBot.Config) {
super(ctx, config, 'infoflow')
const { access_token } = getParam(config.targetUrl) as { access_token: string }

this.http = ctx.http.extend({
endpoint: 'http://apiin.im.baidu.com/api/',
})
this.internal = new Internal(this, {
query: {
access_token,
},
})

ctx.plugin(HttpServer, this)
}

initialize() {
Promise.resolve().then(e => {
this.user.name = this.config.name
this.user.avatar = '' // 如流暂未支持获取机器人信息
this.online()
})
}
}

export namespace InfoflowBot {
export interface Config {
name: string
robotId: string
targetUrl: string
selfUrl?: string
port?: number
token?: string
EncodingAESKey?: string
path?: string
}

export const Config: Schema<Config> = Schema.object({
targetUrl: Schema.string().description('目标 URL'),
name: Schema.string().description('机器人名称'),
robotId: Schema.string().description('机器人 ID'),
selfUrl: Schema.string().default('localhost').description('机器人 URL'),
port: Schema.number().default(80).description('端口号'),
token: Schema.string().description('安全令牌'),
EncodingAESKey: Schema.string().description('消息加解密密钥'),
path: Schema.string().default('/infoflow').description('路径'),
})
}

export default InfoflowBot
49 changes: 49 additions & 0 deletions adapters/infoflow/src/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Adapter, Context } from '@satorijs/core'
import InfoflowBot from './bot'
import {} from '@cordisjs/plugin-server'
import { AESCipher, getParam, getSession, getSignature } from './utils'
import { rAt, requestUrlParam } from './type'

export class HttpServer<C extends Context = Context> extends Adapter<C, InfoflowBot<C>> {
static inject = ['server']
cipher: AESCipher
constructor(ctx: C, bot: InfoflowBot<C>) {
super(ctx)
}

fork(ctx: C, bot: InfoflowBot<C>) {
super.fork(ctx, bot)
const { EncodingAESKey } = bot.config
this.cipher = new AESCipher(EncodingAESKey)
return bot.initialize()
}

async connect(bot: InfoflowBot) {
const { path } = bot.config
bot.ctx.server.post(path, (ctx) => {
const reqBody = ctx.request.body
// 验证
if (reqBody.echostr) {
if (getSignature(reqBody.rn, reqBody.timestamp, bot.config.token) === reqBody.signature) ctx.body = reqBody.echostr
else throw new Error('签名错误')
return
}

const param = getParam(ctx.request.url) as requestUrlParam
if (!param.signature || !param.timestamp || !param.rn || getSignature(param.rn, param.timestamp, bot.config.token) !== param.signature) {
ctx.body = 'fail'
ctx.status = 403
return
}
const res = this.cipher.decrypt(reqBody)
const { message } = res
const { robotid } = message.body.find((item) => {
if (item.type === 'AT') { return '' + item?.robotid === bot.config.robotId }
}) as rAt
if (!robotid) return
const theBot = this.bots.find((item) => item.config.robotId === '' + robotid)
const session = getSession(theBot, message)
theBot.dispatch(session)
})
}
}
5 changes: 5 additions & 0 deletions adapters/infoflow/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { InfoflowBot } from './bot'

export * from './bot'

export default InfoflowBot
89 changes: 89 additions & 0 deletions adapters/infoflow/src/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Context, Element, MessageEncoder } from '@satorijs/core'
import InfoflowBot from './bot'
import { SendMessage, Text } from './type'
import { getBase64 } from './utils'

export class InfoflowMessageEncoder<C extends Context = Context> extends MessageEncoder<C, InfoflowBot<C>> {
private header: SendMessage['message']['header']
private body: SendMessage['message']['body']
async prepare() {
this.header = {
toid: [+this.channelId],
}
this.body = []
}

async visit(element: Element): Promise<void> {
const { type, attrs, children } = element
if (type === 'text') {
this.body.push({
type: 'TEXT',
content: attrs.content,
})
} else if (type === 'at') {
if (attrs.type === 'all') {
this.body.push({
type: 'AT',
atall: true,
})
} else {
this.body.push({
type: 'AT',
atuserids: attrs.id ? [attrs.id] : [],
})
}
} else if (type === 'a') {
this.body.push({
type: 'LINK',
href: attrs.href,
label: attrs.children ? attrs.children.toString() : attrs.href,
})
} else if (type === 'p') {
if (!(this.body[this.body.length - 1].type === 'TEXT' && (this.body[this.body.length - 1] as Text).content.endsWith('\n'))) {
this.body.push({
type: 'TEXT',
content: '\n',
})
}
await this.render(children)
if (!(this.body[this.body.length - 1].type === 'TEXT' && (this.body[this.body.length - 1] as Text).content.endsWith('\n'))) {
this.body.push({
type: 'TEXT',
content: '\n',
})
}
} else if (type === 'br') {
this.body.push({
type: 'TEXT',
content: '\n',
})
} else if (type === 'quote') {
await this.flush()
} else if (type === 'img' || type === 'image') {
const res = await this.bot.ctx.http.get(attrs.src, { responseType: 'arraybuffer' })
const encodeImage = getBase64(res)
this.body.push({
type: 'IMAGE',
content: encodeImage,
})
} else if (type === 'figure' || type === 'message') {
await this.render(children)
} else {
await this.render(children)
}
}

async flush() {
await this.post()
this.body = []
}

async post() {
this.bot.internal.sendGroupMessage({
message: {
header: this.header,
body: this.body,
},
})
}
}
18 changes: 18 additions & 0 deletions adapters/infoflow/src/type/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Internal } from './internal'
import { SendMessage } from './index'

Internal.define({
'/msg/groupmsgsend': {
POST: 'sendGroupMessage',
},
})

declare module './internal' {
interface Internal {
/**
* 发送群消息
* @see https://qy.baidu.com/doc/index.html#/inner_serverapi/robot?id=%e5%8f%91%e9%80%81%e6%b6%88%e6%81%af-1
*/
sendGroupMessage(query?: SendMessage)
}
}
98 changes: 98 additions & 0 deletions adapters/infoflow/src/type/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
export * from './internal'
export * from './api'

export interface requestUrlParam{
signature?: string
rn?: string
timestamp?: string
echostr?: string
}

export interface MessageRequest {
eventtype: string
agentid: number
groupid: number
corpid: string
time: number
fromid: number
opencode: string
message: ReceiveMessage
}

export type Text = {
type: 'TEXT'
content: string
}

export type Link = {
type: 'LINK'
href: string
label?: string
}

export type rAt = {
type: 'AT'
name: string
robotid?: number
userid?: string
}

export type sAt = {
type: 'AT'
atuserids?: string[]
robotid?: number
atall?: boolean
}

export type rImage = {
type: 'IMAGE'
downloadurl: string
}

export type sImage = {
type: 'IMAGE'
content: string
}

export type Md = {
type: 'MD'
content: string
}

export type Command = {
type: 'command'
commandname: string
}

export type r = Text | Link | rImage | Md | Command | rAt

export type s = Text | Link | sImage | Md | Command | sAt

export interface ReceiveMessage{
header: {
fromuserid: string
toid: number
totype: string
msgtype: string
clientmsgid: number
messageid: number
msgseqid: string
at: object
compatible: string
offlinenotify: string
extra: string
servertime: number
clientime: number
updatetime: number
}
body: r[]
}

export interface SendMessage {
message: {
header: {
toid: number | number[]
}
body: s[]
}
}
Loading