import { Bot , Session , camelize , segment , renameProperty , snakeCase , Adapter , Quester } from 'koishi' ; import { Method } from 'axios' ; import * as KHL from './types' ; import { adaptGroup , adaptAuthor , adaptUser , AdapterConfig } from './utils' ; import FormData from 'form-data' ; import { createReadStream } from 'fs' ; import internal from 'stream' ; export interface KaiheilaMessageInfo { channelName ? : string ; mention ? : string [ ] ; mentionRoles ? : string [ ] ; mentionAll ? : boolean ; mentionHere ? : boolean ; author ? : Bot . Author ; } const attachmentTypes = [ 'image' , 'video' , 'audio' , 'file' ] ; type SendHandle = [ string , KHL . MessageParams , Session ] ; export interface BotConfig extends Bot . BaseConfig { token ? : string ; verifyToken ? : string ; attachMode ? : 'separate' | 'card' | 'mixed' ; } export class KaiheilaBot extends Bot < BotConfig > { _sn : number ; _ping : NodeJS . Timeout ; _heartbeat : NodeJS . Timeout ; http : Quester ; static schema = AdapterConfig ; constructor ( adapter , config ) { super ( adapter , config ) ; this . _sn = 0 ; this . http = adapter . http . extend ( { headers : { 'Authorization' : `Bot ${ config . token }` , 'Content-Type' : 'application/json' , } , } ) ; } async request < T = any > ( method , path , data ? , headers = { } ) { data = data instanceof FormData ? data : JSON . stringify ( snakeCase ( data ) ) ; const response = await this . http ( method , path , { data , headers } ) ; return camelize ( response ) . data ; } private _prepareHandle ( channelId , content , guildId ) { let path ; const params = { } as KHL . MessageParams ; const session = this . createSession ( { channelId , content , guildId } ) ; if ( channelId . length > 30 ) { params . chatCode = channelId ; session . subtype = 'private' ; path = '/user-chat/create-msg' ; } else { params . targetId = channelId ; session . subtype = 'group' ; path = '/message/create' ; } return [ path , params , session ] ; } private async _sendHandle ( [ path , params , session ] , type , content ) { params . type = type ; params . content = content ; const message = await this . request ( 'POST' , path , params ) ; session . messageId = message . msgId ; this . app . emit ( session , 'send' , session ) ; } private async _transformUrl ( { type , data } ) { if ( data . url . startsWith ( 'file://' ) || data . url . startsWith ( 'base64://' ) ) { const payload = new FormData ( ) ; payload . append ( 'file' , data . url . startsWith ( 'file://' ) ? createReadStream ( data . url . slice ( 8 ) ) : Buffer . from ( data . url . slice ( 9 ) , 'base64' ) ) ; const { url } = await this . request ( 'POST' , '/asset/create' , payload , payload . getHeaders ( ) ) ; data . url = url ; } else if ( ! data . url . includes ( 'kaiheila' ) ) { const res = await this . app . http . get < internal . Readable > ( data . url , { headers : { accept : type } , responseType : 'stream' , } ) ; const payload = new FormData ( ) ; payload . append ( 'file' , res ) ; const { url } = await this . request ( 'POST' , '/asset/create' , payload , payload . getHeaders ( ) ) ; data . url = url ; console . log ( url ) ; } } private async _sendCard ( handle , chain , useMarkdown ) { const type = useMarkdown ? 'kmarkdown' : 'plain-text' ; let text = { type , content : '' } ; let card = { type : 'card' , modules : [ ] } ; const output = [ ] ; const flushText = ( ) => { text . content = text . content . trim ( ) ; if ( ! text . content ) return ; card . modules . push ( { type : 'section' , text } ) ; text = { type , content : '' } ; } ; const flushCard = ( ) => { flushText ( ) ; if ( ! card . modules . length ) return ; output . push ( card ) ; card = { type : 'card' , modules : [ ] } ; } ; for ( const { type , data } of chain ) { if ( type === 'text' ) { text . content += data . content ; } else if ( type === 'at' ) { if ( data . id ) { text . content += `@user#${ data . id }` ; } else if ( data . type === 'all' ) { text . content += '@全体成员' ; } else if ( data . type === 'here' ) { text . content += '@在线成员' ; } else if ( data . role ) { text . content += `@role:${ data . role };` ; } } else if ( type === 'sharp' ) { text . content += `#channel:${ data . id };` ; } else if ( attachmentTypes . includes ( type ) ) { flushText ( ) ; await this . _transformUrl ( { type , data } ) ; if ( type === 'image' ) { card . modules . push ( { type : 'image-group' , elements : [ { type : 'image' , src : data . url , } ] , } ) ; } else { card . modules . push ( { type : type as never , src : data . url , } ) ; } } else if ( type === 'card' ) { flushCard ( ) ; output . push ( JSON . parse ( data . content ) ) ; } } flushCard ( ) ; await this . _sendHandle ( handle , KHL . Type . card , JSON . stringify ( output ) ) ; } private async _sendSeparate ( handle , chain , useMarkdown ) { let textBuffer = '' ; const type = useMarkdown ? KHL . Type . kmarkdown : KHL . Type . text ; const flush = async ( ) => { textBuffer = textBuffer . trim ( ) ; if ( ! textBuffer ) return ; await this . _sendHandle ( handle , type , textBuffer ) ; handle [ 1 ] . quote = null ; textBuffer = '' ; } ; for ( const { type , data } of chain ) { if ( type === 'text' ) { textBuffer += data . content ; } else if ( type === 'at' ) { if ( data . id ) { textBuffer += `@user#${ data . id }` ; } else if ( data . type === 'all' ) { textBuffer += '@全体成员' ; } else if ( data . type === 'here' ) { textBuffer += '@在线成员' ; } else if ( data . role ) { textBuffer += `@role:${ data . role };` ; } } else if ( type === 'sharp' ) { textBuffer += `#channel:${ data . id };` ; } else if ( attachmentTypes . includes ( type ) ) { await flush ( ) ; await this . _transformUrl ( { type , data } ) ; await this . _sendHandle ( handle , KHL . Type [ type ] , data . url ) ; } else if ( type === 'card' ) { await flush ( ) ; await this . _sendHandle ( handle , KHL . Type . card , JSON . stringify ( [ JSON . parse ( data . content ) ] ) ) ; } } await flush ( ) ; } async sendMessage ( channelId , content , guildId ? ) { const handle = this . _prepareHandle ( channelId , content , guildId ) ; const [ , params , session ] = handle ; if ( await this . app . serial ( session , 'before-send' , session ) ) return ; let useMarkdown = false ; const chain = segment . parse ( content ) ; if ( chain [ 0 ] . type === 'quote' ) { params . quote = chain . shift ( ) . data . id ; } if ( chain [ 0 ] . type === 'markdown' ) { useMarkdown = true ; chain . shift ( ) ; } const { attachMode } = this . config ; const hasAttachment = chain . some ( node => attachmentTypes . includes ( node . type ) ) ; const useCard = hasAttachment && ( attachMode === 'card' || attachMode === 'mixed' && chain . length > 1 ) ; if ( useCard ) { await this . _sendCard ( handle , chain , useMarkdown ) ; } else { await this . _sendSeparate ( handle , chain , useMarkdown ) ; } return [ session . messageId ] ; } async sendPrivateMessage ( targetId , content ) { const { code } = await this . request ( 'POST' , '/user-chat/create' , { targetId } ) ; return this . sendMessage ( code , content ) ; } async deleteMessage ( channelId , msgId ) { if ( channelId . length > 30 ) { await this . request ( 'POST' , '/user-chat/delete-msg' , { msgId } ) ; } else { await this . request ( 'POST' , '/message/delete' , { msgId } ) ; } } async editMessage ( channelId , msgId , content ) { if ( channelId . length > 30 ) { await this . request ( 'POST' , '/user-chat/update-msg' , { msgId , content } ) ; } else { await this . request ( 'POST' , '/message/update' , { msgId , content } ) ; } } async $createReaction ( channelId , messageId , emoji ) { if ( channelId . length > 30 ) { await this . request ( 'POST' , '/direct-message/add-reaction' , { msg_id : messageId , emoji } ) ; } else { await this . request ( 'POST' , '/message/add-reaction' , { msg_id : messageId , emoji } ) ; } } async $deleteReaction ( channelId , messageId , emoji , userId ? ) { if ( channelId . length > 30 ) { await this . request ( 'POST' , '/direct-message/delete-reaction' , { msg_id : messageId , emoji } ) ; } else { await this . request ( 'POST' , '/message/delete-reaction' , { msg_id : messageId , emoji , user_id : userId } ) ; } } async getSelf ( ) { const data = adaptUser ( await this . request < KHL . Self > ( 'GET' , '/user/me' ) ) ; renameProperty ( data , 'selfId' as never , 'userId' ) ; return data ; } async getGuildList ( ) { const { items } = await this . request < KHL . GuildList > ( 'GET' , '/guild/list' ) ; return items . map ( adaptGroup ) ; } async getGuildMemberList ( ) { const { items } = await this . request < KHL . GuildMemberList > ( 'GET' , '/guild/user-list' ) ; return items . map ( adaptAuthor ) ; } async setGroupNickname ( guildId , userId , nickname ) { await this . request ( 'POST' , '/guild/nickname' , { guildId , userId , nickname } ) ; } async leaveGroup ( guildId ) { await this . request ( 'POST' , '/guild/leave' , { guildId } ) ; } async kickGroup ( guildId , userId ) { await this . request ( 'POST' , '/guild/kickout' , { guildId , userId } ) ; } }