提交 11760a52 编写于 作者: M Mukaiu 提交者: Huan (李卓桓)

fix #4 send image/video (#337)

* #1 发送图片测试

* tslint

* 调整文件路径为stream

* remove bl,mime

* #4 implement
contact.say(new MediaMessage('/tmp/file.jpg'))
message.say(new MediaMessage('/tmp/haha.gif'))

* clean message.mediaId

* move MediaMessage to message.ts

* package: add bl 1.2.0

* merge Chatie-master

* Merge branch 'master' into pr/4

# Conflicts:
#	package.json

* restore puppet-web.ts format
上级 1e47b713
......@@ -15,6 +15,7 @@ import {
Config,
Wechaty,
log,
MediaMessage,
} from '../'
const welcome = `
......@@ -71,6 +72,9 @@ bot
if (/^(ding|ping|bing)$/i.test(m.content()) && !m.self()) {
m.say('dong')
log.info('Bot', 'REPLY: dong')
} else if (/^code$/i.test(m.content()) && !m.self()) {
m.say(new MediaMessage(__dirname + '/../image/BotQrcode.png'))
log.info('Bot', 'REPLY: Img')
}
} catch (e) {
log.error('Bot', 'on(message) exception: %s' , e)
......
......@@ -115,6 +115,7 @@
"retry-promise": "1.0.0",
"socket.io": "1.7.3",
"selenium-webdriver": "3.3.0",
"bl": "^1.2.0",
"ws": "2.2.2"
},
"devDependencies": {
......
......@@ -2,7 +2,10 @@ import {
Config,
Sayable,
} from './config'
import { Message } from './message'
import {
Message,
MediaMessage,
} from './message'
import { PuppetWeb } from './puppet-web'
import { UtilLib } from './util-lib'
import { Wechaty } from './wechaty'
......@@ -578,7 +581,11 @@ export class Contact implements Sayable {
* await contact.say('welcome to wechaty!')
* ```
*/
public async say(content: string): Promise<void> {
public async say(text: string)
public async say(mediaMessage: MediaMessage)
public async say(textOrMedia: string | MediaMessage): Promise<void> {
const content = textOrMedia instanceof MediaMessage ? textOrMedia.filename() : textOrMedia
log.verbose('Contact', 'say(%s)', content)
const wechaty = Wechaty.instance()
......@@ -587,11 +594,17 @@ export class Contact implements Sayable {
if (!user) {
throw new Error('no user')
}
const m = new Message()
let m
if (typeof textOrMedia === 'string') {
m = new Message()
m.content(textOrMedia)
} else if (textOrMedia instanceof MediaMessage) {
m = textOrMedia
} else {
throw new Error('not support args')
}
m.from(user)
m.to(this)
m.content(content)
log.silly('Contact', 'say() from: %s to: %s content: %s', user.name(), this.name(), content)
await wechaty.send(m)
......
/**
*
* wechaty: Wechat for Bot. and for human who talk to bot/robot
*
* Licenst: ISC
* https://github.com/zixia/wechaty
*
*/
import * as moment from 'moment'
import {
Config,
log,
} from './config'
import {
AppMsgType,
Message,
MsgType,
} from './message'
import { UtilLib } from './util-lib'
import { PuppetWeb } from './puppet-web/puppet-web'
import { Bridge } from './puppet-web/bridge'
export class MediaMessage extends Message {
private bridge: Bridge
constructor(rawObj) {
super(rawObj)
// FIXME: decoupling needed
this.bridge = (Config.puppetInstance() as PuppetWeb)
.bridge
}
public async ready(): Promise<void> {
log.silly('MediaMessage', 'ready()')
try {
await super.ready()
let url: string|null = null
switch (this.type()) {
case MsgType.EMOTICON:
url = await this.bridge.getMsgEmoticon(this.id)
break
case MsgType.IMAGE:
url = await this.bridge.getMsgImg(this.id)
break
case MsgType.VIDEO:
case MsgType.MICROVIDEO:
url = await this.bridge.getMsgVideo(this.id)
break
case MsgType.VOICE:
url = await this.bridge.getMsgVoice(this.id)
break
case MsgType.APP:
if (!this.rawObj) {
throw new Error('no rawObj')
}
switch (this.typeApp()) {
case AppMsgType.ATTACH:
if (!this.rawObj.MMAppMsgDownloadUrl) {
throw new Error('no MMAppMsgDownloadUrl')
}
// had set in Message
// url = this.rawObj.MMAppMsgDownloadUrl
break
case AppMsgType.URL:
case AppMsgType.READER_TYPE:
if (!this.rawObj.Url) {
throw new Error('no Url')
}
// had set in Message
// url = this.rawObj.Url
break
default:
const e = new Error('ready() unsupported typeApp(): ' + this.typeApp())
log.warn('MediaMessage', e.message)
this.dumpRaw()
throw e
}
break
case MsgType.TEXT:
if (this.typeSub() === MsgType.LOCATION) {
url = await this.bridge.getMsgPublicLinkImg(this.id)
}
break
default:
throw new Error('not support message type for MediaMessage')
}
if (!url) {
if (!this.obj.url) {
throw new Error('no obj.url')
}
url = this.obj.url
}
this.obj.url = url
} catch (e) {
log.warn('MediaMessage', 'ready() exception: %s', e.message)
throw e
}
}
private ext(): string {
switch (this.type()) {
case MsgType.EMOTICON:
return 'gif'
case MsgType.IMAGE:
return 'jpg'
case MsgType.VIDEO:
case MsgType.MICROVIDEO:
return 'mp4'
case MsgType.VOICE:
return 'mp3'
case MsgType.APP:
switch (this.typeApp()) {
case AppMsgType.URL:
return 'url' // XXX
}
break
case MsgType.TEXT:
if (this.typeSub() === MsgType.LOCATION) {
return 'jpg'
}
break
}
throw new Error('not support type: ' + this.type())
}
public filename(): string {
if (!this.rawObj) {
throw new Error('no rawObj')
}
const objFileName = this.rawObj.FileName || this.rawObj.MediaId || this.rawObj.MsgId
let filename = moment().format('YYYY-MM-DD HH:mm:ss')
+ ' #' + this._counter
+ ' ' + this.getSenderString()
+ ' ' + objFileName
filename = filename.replace(/ /g, '_')
const re = /\.[a-z0-9]{1,7}$/i
if (!re.test(filename)) {
const ext = this.rawObj.MMAppMsgFileExt || this.ext()
filename += '.' + ext
}
return filename
}
// private getMsgImg(id: string): Promise<string> {
// return this.bridge.getMsgImg(id)
// .catch(e => {
// log.warn('MediaMessage', 'getMsgImg(%d) exception: %s', id, e.message)
// throw e
// })
// }
public async readyStream(): Promise<NodeJS.ReadableStream> {
try {
await this.ready()
// FIXME: decoupling needed
const cookies = await (Config.puppetInstance() as PuppetWeb).browser.readCookie()
if (!this.obj.url) {
throw new Error('no url')
}
return UtilLib.urlStream(this.obj.url, cookies)
} catch (e) {
log.warn('MediaMessage', 'stream() exception: %s', e.stack)
throw e
}
}
}
......@@ -6,6 +6,10 @@
* https://github.com/wechaty/wechaty
*
*/
import * as moment from 'moment'
import * as fs from 'fs'
import * as path from 'path'
import {
Config,
RecommendInfo,
......@@ -16,6 +20,8 @@ import {
import { Contact } from './contact'
import { Room } from './room'
import { UtilLib } from './util-lib'
import { PuppetWeb } from './puppet-web/puppet-web'
import { Bridge } from './puppet-web/bridge'
export type MsgRawObj = {
MsgId: string,
......@@ -492,10 +498,15 @@ export class Message implements Sayable {
})
}
public say(content: string, replyTo?: Contact|Contact[]): Promise<any> {
log.verbose('Message', 'say(%s, %s)', content, replyTo)
public say(text: string, replyTo?: Contact | Contact[]): Promise<any>
public say(mediaMessage: MediaMessage, replyTo?: Contact | Contact[]): Promise<any>
const m = new Message()
public say(textOrMedia: string | MediaMessage, replyTo?: Contact|Contact[]): Promise<any> {
const content = textOrMedia instanceof MediaMessage ? textOrMedia.filename() : textOrMedia
log.verbose('Message', 'say(%s, %s)', content, replyTo)
let m
if (typeof textOrMedia === 'string') {
m = new Message()
const room = this.room()
if (room) {
m.room(room)
......@@ -503,7 +514,7 @@ export class Message implements Sayable {
if (!replyTo) {
m.to(this.from())
m.content(content)
m.content(textOrMedia)
} else if (this.room()) {
let mentionList
......@@ -514,9 +525,20 @@ export class Message implements Sayable {
m.to(replyTo)
mentionList = '@' + replyTo.name()
}
m.content(mentionList + ' ' + content)
m.content(mentionList + ' ' + textOrMedia)
}
} else if (textOrMedia instanceof MediaMessage) {
m = textOrMedia
const room = this.room()
if (room) {
m.room(room)
}
if (!replyTo) {
m.to(this.from())
}
}
return Config.puppetInstance()
.send(m)
}
......@@ -525,7 +547,197 @@ export class Message implements Sayable {
Message.initType()
export * from './message-media'
export class MediaMessage extends Message {
private bridge: Bridge
private fileStream: NodeJS.ReadableStream
private fileName: string // 'music'
private fileExt: string // 'mp3'
constructor(rawObj: object)
constructor(filePath: string)
constructor(rawObjOrFilePath: object | string) {
if (typeof rawObjOrFilePath === 'string') {
super()
this.fileStream = fs.createReadStream(rawObjOrFilePath)
const pathInfo = path.parse(rawObjOrFilePath)
this.fileName = pathInfo.name
this.fileExt = pathInfo.ext.replace(/^\./, '')
} else if (rawObjOrFilePath instanceof Object) {
super(rawObjOrFilePath as any)
} else {
throw new Error('not supported construct param')
}
// FIXME: decoupling needed
this.bridge = (Config.puppetInstance() as PuppetWeb)
.bridge
}
public async ready(): Promise<void> {
log.silly('MediaMessage', 'ready()')
try {
await super.ready()
let url: string|null = null
switch (this.type()) {
case MsgType.EMOTICON:
url = await this.bridge.getMsgEmoticon(this.id)
break
case MsgType.IMAGE:
url = await this.bridge.getMsgImg(this.id)
break
case MsgType.VIDEO:
case MsgType.MICROVIDEO:
url = await this.bridge.getMsgVideo(this.id)
break
case MsgType.VOICE:
url = await this.bridge.getMsgVoice(this.id)
break
case MsgType.APP:
if (!this.rawObj) {
throw new Error('no rawObj')
}
switch (this.typeApp()) {
case AppMsgType.ATTACH:
if (!this.rawObj.MMAppMsgDownloadUrl) {
throw new Error('no MMAppMsgDownloadUrl')
}
// had set in Message
// url = this.rawObj.MMAppMsgDownloadUrl
break
case AppMsgType.URL:
case AppMsgType.READER_TYPE:
if (!this.rawObj.Url) {
throw new Error('no Url')
}
// had set in Message
// url = this.rawObj.Url
break
default:
const e = new Error('ready() unsupported typeApp(): ' + this.typeApp())
log.warn('MediaMessage', e.message)
this.dumpRaw()
throw e
}
break
case MsgType.TEXT:
if (this.typeSub() === MsgType.LOCATION) {
url = await this.bridge.getMsgPublicLinkImg(this.id)
}
break
default:
throw new Error('not support message type for MediaMessage')
}
if (!url) {
if (!this.obj.url) {
throw new Error('no obj.url')
}
url = this.obj.url
}
this.obj.url = url
} catch (e) {
log.warn('MediaMessage', 'ready() exception: %s', e.message)
throw e
}
}
public ext(): string {
if (this.fileExt)
return this.fileExt
switch (this.type()) {
case MsgType.EMOTICON:
return 'gif'
case MsgType.IMAGE:
return 'jpg'
case MsgType.VIDEO:
case MsgType.MICROVIDEO:
return 'mp4'
case MsgType.VOICE:
return 'mp3'
case MsgType.APP:
switch (this.typeApp()) {
case AppMsgType.URL:
return 'url' // XXX
}
break
case MsgType.TEXT:
if (this.typeSub() === MsgType.LOCATION) {
return 'jpg'
}
break
}
throw new Error('not support type: ' + this.type())
}
public filename(): string {
if (this.fileName && this.fileExt) {
return this.fileName + '.' + this.fileExt
}
if (!this.rawObj) {
throw new Error('no rawObj')
}
const objFileName = this.rawObj.FileName || this.rawObj.MediaId || this.rawObj.MsgId
let filename = moment().format('YYYY-MM-DD HH:mm:ss')
+ ' #' + this._counter
+ ' ' + this.getSenderString()
+ ' ' + objFileName
filename = filename.replace(/ /g, '_')
const re = /\.[a-z0-9]{1,7}$/i
if (!re.test(filename)) {
const ext = this.rawObj.MMAppMsgFileExt || this.ext()
filename += '.' + ext
}
return filename
}
// private getMsgImg(id: string): Promise<string> {
// return this.bridge.getMsgImg(id)
// .catch(e => {
// log.warn('MediaMessage', 'getMsgImg(%d) exception: %s', id, e.message)
// throw e
// })
// }
public async readyStream(): Promise<NodeJS.ReadableStream> {
if (this.fileStream)
return this.fileStream
try {
await this.ready()
// FIXME: decoupling needed
const cookies = await (Config.puppetInstance() as PuppetWeb).browser.readCookie()
if (!this.obj.url) {
throw new Error('no url')
}
return UtilLib.urlStream(this.obj.url, cookies)
} catch (e) {
log.warn('MediaMessage', 'stream() exception: %s', e.stack)
throw e
}
}
}
/*
* join room in mac client: https://support.weixin.qq.com/cgi-bin/
......
......@@ -351,6 +351,43 @@ export class Bridge {
/////////////////////////////////
}
public async getBaseRequest(): Promise<string> {
log.verbose('PuppetWebBridge', 'getBaseRequest()')
try {
return await this.proxyWechaty('getBaseRequest')
} catch (e) {
log.silly('PuppetWebBridge', 'proxyWechaty(getBaseRequest) exception: %s', e.message)
throw e
}
}
public async getPassticket(): Promise<string> {
log.verbose('PuppetWebBridge', 'getPassticket()')
try {
return await this.proxyWechaty('getPassticket')
} catch (e) {
log.silly('PuppetWebBridge', 'proxyWechaty(getPassticket) exception: %s', e.message)
throw e
}
}
public sendMedia(toUserName: string, mediaId: string, type: number): Promise<void> {
if (!toUserName) {
throw new Error('UserName not found')
}
if (!mediaId) {
throw new Error('cannot say nothing')
}
return this.proxyWechaty('sendMedia', toUserName, mediaId, type)
.catch(e => {
log.error('PuppetWebBridge', 'sendMedia() exception: %s', e.message)
throw e
})
}
/**
* Proxy Call to Wechaty in Bridge
*/
......
......@@ -22,7 +22,10 @@ import {
} from '../config'
import { Contact } from '../contact'
import { Message } from '../message'
import {
Message,
MediaMessage,
} from '../message'
import { Puppet } from '../puppet'
import { Room } from '../room'
import { UtilLib } from '../util-lib'
......@@ -33,6 +36,17 @@ import { Event } from './event'
import { Server } from './server'
import { Watchdog } from './watchdog'
import * as request from 'request'
import * as bl from 'bl'
type MediaType = 'pic' | 'video' | 'doc'
const enum UploadMediaType {
IMAGE = 1,
VIDEO = 2,
AUDIO = 3,
ATTACHMENT = 4,
}
export type PuppetWebSetting = {
head?: HeadName,
profile?: string,
......@@ -296,11 +310,148 @@ export class PuppetWeb extends Puppet {
throw new Error('PuppetWeb.self() no this.user')
}
public async send(message: Message): Promise<void> {
private async getBaseRequest(): Promise<any> {
try {
let json = await this.bridge.getBaseRequest();
let obj = JSON.parse(json)
return obj.BaseRequest
} catch (e) {
log.error('PuppetWeb', 'send() exception: %s', e.message)
throw e
}
}
private async uploadMedia(mediaMessage: MediaMessage, toUserName: string): Promise<string> {
if (!mediaMessage)
throw new Error('require mediaMessage')
let filename = mediaMessage.filename()
let ext = mediaMessage.ext()
let contentType = UtilLib.mime(ext)
let mediatype: MediaType
switch (ext) {
case 'bmp':
case 'jpeg':
case 'jpg':
case 'png':
mediatype = 'pic'
break
case 'mp4':
mediatype = 'video'
break
default:
mediatype = 'doc'
}
let readStream = await mediaMessage.readyStream()
let buffer = <Buffer>await new Promise((resolve, reject) => {
readStream.pipe(bl((err, data) => {
if (err) reject(err)
else resolve(data)
}))
})
let md5 = UtilLib.md5(buffer)
let baseRequest = await this.getBaseRequest()
let passTicket = await this.bridge.getPassticket()
let cookie = await this.browser.readCookie()
let first = cookie.find(c => c.name === 'webwx_data_ticket')
let webwxDataTicket = first && first.value
let size = buffer.length
let hostname = this.browser.hostname
let uploadMediaRequest = {
BaseRequest: baseRequest,
FileMd5: md5,
FromUserName: this.self().id,
ToUserName: toUserName,
UploadType: 2,
ClientMediaId: +new Date,
MediaType: UploadMediaType.ATTACHMENT,
StartPos: 0,
DataLen: size,
TotalLen: size,
}
let formData = {
id: 'WU_FILE_1',
name: filename,
type: contentType,
lastModifiedDate: Date().toString(),
size: size,
mediatype,
uploadmediarequest: JSON.stringify(uploadMediaRequest),
webwx_data_ticket: webwxDataTicket,
pass_ticket: passTicket || '',
filename: {
value: buffer,
options: {
filename,
contentType,
size,
},
},
}
let mediaId = await new Promise((resolve, reject) => {
request.post({
url: `https://file.${hostname}/cgi-bin/mmwebwx-bin/webwxuploadmedia?f=json`,
headers: {
Referer: `https://${hostname}`,
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36',
},
formData,
}, function (err, res, body) {
if (err) reject(err)
else {
let obj = JSON.parse(body)
resolve(obj.MediaId)
}
})
})
if (!mediaId)
throw new Error('upload fail')
return mediaId as string
}
public async sendMedia(message: MediaMessage): Promise<void> {
const to = message.to()
const room = message.room()
const content = message.content()
let destinationId
if (room) {
destinationId = room.id
} else {
if (!to) {
throw new Error('PuppetWeb.send(): message with neither room nor to?')
}
destinationId = to.id
}
const mediaId = await this.uploadMedia(message, destinationId)
let msgType = UtilLib.msgType(message.ext())
log.silly('PuppetWeb', 'send() destination: %s, mediaId: %s)',
destinationId,
mediaId,
)
try {
await this.bridge.sendMedia(destinationId, mediaId, msgType)
} catch (e) {
log.error('PuppetWeb', 'send() exception: %s', e.message)
throw e
}
return
}
public async send(message: Message | MediaMessage): Promise<void> {
const to = message.to()
const room = message.room()
let destinationId
......@@ -313,6 +464,11 @@ export class PuppetWeb extends Puppet {
destinationId = to.id
}
if (message instanceof MediaMessage) {
await this.sendMedia(message)
} else {
const content = message.content()
log.silly('PuppetWeb', 'send() destination: %s, content: %s)',
destinationId,
content,
......@@ -324,8 +480,9 @@ export class PuppetWeb extends Puppet {
log.error('PuppetWeb', 'send() exception: %s', e.message)
throw e
}
}
return
}
}
/**
* Bot say...
......
......@@ -441,6 +441,36 @@
return location + path
}
function getBaseRequest() {
var accountFactory = WechatyBro.glue.accountFactory
var BaseRequest = accountFactory.getBaseRequest()
return JSON.stringify(BaseRequest)
}
function getPassticket() {
var accountFactory = WechatyBro.glue.accountFactory
return accountFactory.getPassticket()
}
function sendMedia(ToUserName, MediaId,Type) {
var chatFactory = WechatyBro.glue.chatFactory
var confFactory = WechatyBro.glue.confFactory
if (!chatFactory || !confFactory) {
log('send() chatFactory or confFactory not exist.')
return false
}
var m = chatFactory.createMessage({
ToUserName: ToUserName
, MediaId: MediaId
, MsgType: Type
})
chatFactory.appendMessage(m)
return chatFactory.sendMessage(m)
}
function send(ToUserName, Content) {
var chatFactory = WechatyBro.glue.chatFactory
var confFactory = WechatyBro.glue.confFactory
......@@ -826,6 +856,9 @@
, getMsgVideo: getMsgVideo
, getMsgVoice: getMsgVoice
, getMsgPublicLinkImg: getMsgPublicLinkImg
, getBaseRequest: getBaseRequest
, getPassticket: getPassticket
, sendMedia: sendMedia
// for Wechaty Contact Class
, contactFindAsync: contactFindAsync
......
......@@ -4,7 +4,10 @@ import {
Sayable,
} from './config'
import { Contact } from './contact'
import { Message } from './message'
import {
Message,
MediaMessage,
} from './message'
import { StateMonitor } from './state-monitor'
import { Room } from './room'
......@@ -30,7 +33,7 @@ export abstract class Puppet extends EventEmitter implements Sayable {
public abstract self(): Contact
public abstract send(message: Message): Promise<void>
public abstract send(message: Message | MediaMessage): Promise<void>
public abstract say(content: string): Promise<void>
public abstract reset(reason?: string): void
......
......@@ -8,7 +8,9 @@
import * as https from 'https'
import * as http from 'http'
import * as url from 'url'
import * as crypto from 'crypto'
import { MsgType } from './message'
import { log } from './config'
/**
......@@ -241,4 +243,43 @@ export class UtilLib {
return currentPort + n
}
}
public static md5(buffer: Buffer): string {
let md5sum = crypto.createHash('md5')
md5sum.update(buffer)
return md5sum.digest('hex')
}
public static msgType(ext): MsgType {
switch (ext) {
case 'bmp':
case 'jpeg':
case 'jpg':
case 'png':
return MsgType.IMAGE
case 'mp4':
return MsgType.VIDEO
default:
return MsgType.APP
}
}
public static mime(ext): string {
switch (ext) {
case 'pdf':
return 'application/pdf'
case 'bmp':
return 'image/bmp'
case 'jpeg':
return 'image/jpeg'
case 'jpg':
return 'image/jpeg'
case 'png':
return 'image/png'
case 'mp4':
return 'video/mp4'
default:
return 'application/octet-stream'
}
}
}
......@@ -12,7 +12,10 @@ import {
import { Contact } from './contact'
import { FriendRequest } from './friend-request'
import { Message } from './message'
import {
Message,
MediaMessage,
} from './message'
import { Puppet } from './puppet'
import { PuppetWeb } from './puppet-web/'
import { Room } from './room'
......@@ -420,7 +423,7 @@ export class Wechaty extends EventEmitter implements Sayable {
/**
* @todo document me
*/
public async send(message: Message): Promise<void> {
public async send(message: Message | MediaMessage): Promise<void> {
if (!this.puppet) {
throw new Error('no puppet')
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册