提交 f26cb42d 编写于 作者: Huan (李卓桓)'s avatar Huan (李卓桓)

#4 Add more support code for saving media file, and prepare for sending attachments

上级 3e0b8ac3
......@@ -10,13 +10,14 @@
/* tslint:disable:variable-name */
const QrcodeTerminal = require('qrcode-terminal')
import * as util from 'util'
// import { inspect } from 'util'
import { createWriteStream, writeFileSync } from 'fs'
import {
Config
// , Message
, MessageType
, Wechaty
Config,
Message,
MsgType,
Wechaty,
} from '../'
const bot = Wechaty.instance({ profile: Config.DEFAULT_PROFILE })
......@@ -28,28 +29,44 @@ bot
}
console.log(`${url}\n[${code}] Scan QR Code in above url to login: `)
})
.on('login' , user => console.log(`${user} logined`))
.on('message', m => {
console.log(`RECV: ${m}`)
console.log(util.inspect(m))
// console.log(inspect(m))
saveRawObj(m.rawObj)
if ( m.type() === MessageType.IMAGE
|| m.type() === MessageType.EMOTICON
|| m.type() === MessageType.VIDEO
|| m.type() === MessageType.VOICE
|| m.type() === MessageType.MICROVIDEO
if ( m.type() === MsgType.IMAGE
|| m.type() === MsgType.EMOTICON
|| m.type() === MsgType.VIDEO
|| m.type() === MsgType.VOICE
|| m.type() === MsgType.MICROVIDEO
|| m.type() === MsgType.APP
|| (m.type() === MsgType.TEXT && m.typeSub() === MsgType.LOCATION) // LOCATION
) {
const filename = m.id + m.ext()
saveMediaFile(m)
}
})
.init()
.catch(e => console.error('bot.init() error: ' + e))
function saveMediaFile(message: Message) {
const filename = message.filename()
console.log('IMAGE local filename: ' + filename)
const fileStream = require('fs').createWriteStream(filename)
const fileStream = createWriteStream(filename)
m.readyStream()
.then(stream => stream.pipe(fileStream))
console.log('start to readyStream()')
message.readyStream()
.then(stream => {
stream.pipe(fileStream)
.on('close', () => {
console.log('finish readyStream()')
})
})
.catch(e => console.log('stream error:' + e))
}
}
})
.init()
.catch(e => console.error('bot.init() error: ' + e))
function saveRawObj(o) {
writeFileSync('rawObj.log', JSON.stringify(o, null, ' ') + '\n\n\n', { flag: 'a' })
}
......@@ -8,7 +8,7 @@ import { FriendRequest } from './src/friend-request'
import { IoClient } from './src/io-client'
import {
Message
, MessageType
, MsgType
} from './src/message'
import { Puppet } from './src/puppet'
import { PuppetWeb } from './src/puppet-web/'
......@@ -24,7 +24,7 @@ export {
, FriendRequest
, IoClient
, Message
, MessageType
, MsgType
, Puppet
, PuppetWeb
, Room
......
......@@ -7,10 +7,14 @@
*
*/
import {
Config
, log
Config,
log,
} from './config'
import { Message } from './message'
import {
AppMsgType,
Message,
MsgType,
} from './message'
import { UtilLib } from './util-lib'
import { PuppetWeb } from './puppet-web/puppet-web'
import { Bridge } from './puppet-web/bridge'
......@@ -31,27 +35,67 @@ export class MediaMessage extends Message {
try {
await super.ready()
let url: string
let url: string|null = null
switch (this.type()) {
case Message.TYPE['EMOTICON']:
case MsgType.EMOTICON:
url = await this.bridge.getMsgEmoticon(this.id)
break
case Message.TYPE['IMAGE']:
case MsgType.IMAGE:
url = await this.bridge.getMsgImg(this.id)
break
case Message.TYPE['VIDEO']:
case Message.TYPE['MICROVIDEO']:
case MsgType.VIDEO:
case MsgType.MICROVIDEO:
url = await this.bridge.getMsgVideo(this.id)
break
case Message.TYPE['VOICE']:
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:
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)
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')
}
this.obj.url = url
// return this // IMPORTANT!
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)
......@@ -59,24 +103,50 @@ export class MediaMessage extends Message {
}
}
public ext(): string {
private ext(): string {
switch (this.type()) {
case Message.TYPE['EMOTICON']:
return '.gif'
case MsgType.EMOTICON:
return 'gif'
case Message.TYPE['IMAGE']:
return '.jpg'
case MsgType.IMAGE:
return 'jpg'
case Message.TYPE['VIDEO']:
case Message.TYPE['MICROVIDEO']:
return '.mp4'
case MsgType.VIDEO:
case MsgType.MICROVIDEO:
return 'mp4'
case Message.TYPE['VOICE']:
return '.mp3'
case MsgType.VOICE:
return 'mp3'
default:
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')
}
let filename = this.rawObj.FileName || this.rawObj.MediaId || this.rawObj.MsgId
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> {
......@@ -87,22 +157,18 @@ export class MediaMessage extends Message {
// })
// }
public readyStream(): Promise<NodeJS.ReadableStream> {
return this.ready()
.then(() => {
public async readyStream(): Promise<NodeJS.ReadableStream> {
try {
await this.ready()
// FIXME: decoupling needed
return (Config.puppetInstance() as PuppetWeb)
.browser.readCookie()
})
.then(cookies => {
const cookies = await (Config.puppetInstance() as PuppetWeb).browser.readCookie()
if (!this.obj.url) {
throw new Error('no url')
}
return UtilLib.downloadStream(this.obj.url, cookies)
})
.catch(e => {
log.warn('MediaMessage', 'stream() exception: %s', e.message)
return UtilLib.urlStream(this.obj.url, cookies)
} catch (e) {
log.warn('MediaMessage', 'stream() exception: %s', e.stack)
throw e
})
}
}
}
......@@ -36,12 +36,18 @@ export type MessageRawObj = {
MMAppMsgDesc: string // class="desc" ng-bind="message.MMAppMsgDesc"
/**
* Attachment
*
* MsgType == MSGTYPE_APP && message.AppMsgType == CONF.APPMSGTYPE_ATTACH
*/
MMAppMsgFileExt: string // doc, docx ...
FileName: string
MMAppMsgFileSize: number
MMAppMsgDownloadUrl: string // <a download ng-if="message.MMFileStatus == CONF.MM_SEND_FILE_STATUS_SUCCESS
FileName: string // FileName: '钢甲互联项目BP1108.pdf',
FileSize: number // FileSize: '2845701',
MediaId: string // MediaId: '@crypt_b1a45e3f_c21dceb3ac01349...
MMAppMsgFileExt: string // doc, docx ... 'undefined'?
MMAppMsgFileSize: string // '2.7MB',
MMAppMsgDownloadUrl: string // 'https://file.wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetmedia?sender=@4f549c2dafd5ad731afa4d857bf03c10&mediaid=@crypt_b1a45e3f
// <a download ng-if="message.MMFileStatus == CONF.MM_SEND_FILE_STATUS_SUCCESS
// && (massage.MMStatus == CONF.MSG_SEND_STATUS_SUCC || massage.MMStatus === undefined)
// " href="{{message.MMAppMsgDownloadUrl}}">下载</a>
MMUploadProgress: number // < 100
......@@ -63,10 +69,10 @@ export type MessageRawObj = {
* MsgType == CONF.MSGTYPE_VOICE : ng-style="{'width':40 + 7*message.VoiceLength/1000}
*/
MsgType: number
AppMsgType: number // message.MsgType == CONF.MSGTYPE_APP && message.AppMsgType == CONF.APPMSGTYPE_URL
AppMsgType: AppMsgType // message.MsgType == CONF.MSGTYPE_APP && message.AppMsgType == CONF.APPMSGTYPE_URL
// message.MsgType == CONF.MSGTYPE_TEXT && message.SubMsgType != CONF.MSGTYPE_LOCATION
SubMsgType: number // "msgType":"{{message.MsgType}}","subType":{{message.SubMsgType||0}},"msgId":"{{message.MsgId}}"
SubMsgType: MsgType // "msgType":"{{message.MsgType}}","subType":{{message.SubMsgType||0}},"msgId":"{{message.MsgId}}"
/**
* Status-es
......@@ -82,6 +88,8 @@ export type MessageRawObj = {
*/
MMLocationUrl: string // ng-if="message.MsgType == CONF.MSGTYPE_TEXT && message.SubMsgType == CONF.MSGTYPE_LOCATION"
// <a href="{{message.MMLocationUrl}}" target="_blank">
// 'http://apis.map.qq.com/uri/v1/geocoder?coord=40.075041,116.338994'
MMLocationDesc: string // MMLocationDesc: '北京市昌平区回龙观龙腾苑(五区)内(龙腾街南)',
/**
* MsgType == CONF.MSGTYPE_EMOTICON
......@@ -133,7 +141,27 @@ export type MessageTypeMap = {
// , MessageTypeValue: MessageTypeName
}
export const enum MessageType {
export const enum AppMsgType {
TEXT = 1,
IMG = 2,
AUDIO = 3,
VIDEO = 4,
URL = 5,
ATTACH = 6,
OPEN = 7,
EMOJI = 8,
VOICE_REMIND = 9,
SCAN_GOOD = 10,
GOOD = 13,
EMOTION = 15,
CARD_TICKET = 16,
REALTIME_SHARE_LOCATION = 17,
TRANSFERS = 2e3,
RED_ENVELOPES = 2001,
READER_TYPE = 100001,
}
export enum MsgType {
TEXT = 1,
IMAGE = 3,
VOICE = 34,
......@@ -151,7 +179,7 @@ export const enum MessageType {
MICROVIDEO = 62,
SYSNOTICE = 9999,
SYS = 10000,
RECALLED = 10002
RECALLED = 10002,
}
export class Message implements Sayable {
......@@ -187,8 +215,8 @@ export class Message implements Sayable {
throw Error('abstract method')
}
public ext(): string {
throw Error('abstract method')
public filename(): string {
throw Error('not a media message')
}
constructor(public rawObj?: MessageRawObj) {
......@@ -207,14 +235,15 @@ export class Message implements Sayable {
// Transform rawObj to local m
private parse(rawObj): MessageObj {
const obj: MessageObj = {
id: rawObj.MsgId
, type: rawObj.MsgType
, from: rawObj.MMActualSender // MMPeerUserName
, to: rawObj.ToUserName
, content: rawObj.MMActualContent // Content has @id prefix added by wx
, status: rawObj.Status
, digest: rawObj.MMDigest
, date: rawObj.MMDisplayTime // Javascript timestamp of milliseconds
id: rawObj.MsgId,
type: rawObj.MsgType,
from: rawObj.MMActualSender, // MMPeerUserName
to: rawObj.ToUserName,
content: rawObj.MMActualContent, // Content has @id prefix added by wx
status: rawObj.Status,
digest: rawObj.MMDigest,
date: rawObj.MMDisplayTime, // Javascript timestamp of milliseconds
url: rawObj.Url || rawObj.MMAppMsgDownloadUrl || rawObj.MMLocationUrl
}
// FIXME: has ther any better method to know the room ID?
......@@ -230,6 +259,7 @@ export class Message implements Sayable {
// } else {
// obj.room = undefined
}
return obj
}
public toString() {
......@@ -334,8 +364,22 @@ export class Message implements Sayable {
return this.obj.content
}
public type(): MessageType {
return this.obj.type as MessageType
public type(): MsgType {
return this.obj.type as MsgType
}
public typeSub(): MsgType {
if (!this.rawObj) {
throw new Error('no rawObj')
}
return this.rawObj.SubMsgType
}
public typeApp(): AppMsgType {
if (!this.rawObj) {
throw new Error('no rawObj')
}
return this.rawObj.AppMsgType
}
public typeEx() { return Message.TYPE[this.obj.type] }
......
......@@ -121,12 +121,15 @@ export class Bridge {
})
}
public getUserName(): Promise<string> {
return this.proxyWechaty('getUserName')
.catch(e => {
public async getUserName(): Promise<string> {
log.verbose('PuppetWebBridge', 'getUserName()')
try {
return await this.proxyWechaty('getUserName')
} catch (e) {
log.error('PuppetWebBridge', 'getUserName() exception: %s', e.message)
throw e
})
}
}
public async contactRemark(contactId: string, remark: string): Promise<boolean> {
......@@ -294,6 +297,17 @@ export class Bridge {
}
}
public async getMsgPublicLinkImg(id): Promise<string> {
log.verbose('PuppetWebBridge', 'getMsgPublicLinkImg(%s)', id)
try {
return await this.proxyWechaty('getMsgPublicLinkImg', id)
} catch (e) {
log.silly('PuppetWebBridge', 'proxyWechaty(getMsgPublicLinkImg, %d) exception: %s', id, e.message)
throw e
}
}
public getContact(id: string): Promise<string> {
if (id !== id) { // NaN
const err = new Error('NaN! where does it come from?')
......@@ -365,7 +379,7 @@ export class Bridge {
throw new Error('there is no WechatyBro in browser(yet)')
}
} catch (e) {
log.error('PuppetWebBridge', 'proxyWechaty() noWechaty exception: %s', e.message)
log.error('PuppetWebBridge', 'proxyWechaty() noWechaty exception: %s', e.stack)
throw e
}
......
......@@ -16,14 +16,15 @@
*
*/
import {
WatchdogFood
, ScanInfo
, log
WatchdogFood,
ScanInfo,
log,
} from '../config'
import { Contact } from '../contact'
import {
Message
, MediaMessage
Message,
MediaMessage,
MsgType,
} from '../message'
import { Firer } from './firer'
......@@ -351,11 +352,11 @@ async function onServerMessage(this: PuppetWeb, data): Promise<void> {
*/
switch (m.type()) { // data.MsgType
case Message.TYPE['VERIFYMSG']:
case MsgType.VERIFYMSG:
Firer.checkFriendRequest.call(this, m)
break
case Message.TYPE['SYS']:
case MsgType.SYS:
if (m.room()) {
Firer.checkRoomJoin.call(this , m)
Firer.checkRoomLeave.call(this , m)
......@@ -373,14 +374,22 @@ async function onServerMessage(this: PuppetWeb, data): Promise<void> {
console.log(m.type())
switch (m.type()) {
case Message.TYPE['EMOTICON']:
case Message.TYPE['IMAGE']:
case Message.TYPE['VIDEO']:
case Message.TYPE['VOICE']:
case Message.TYPE['MICROVIDEO']:
case MsgType.EMOTICON:
case MsgType.IMAGE:
case MsgType.VIDEO:
case MsgType.VOICE:
case MsgType.MICROVIDEO:
case MsgType.APP:
log.verbose('PuppetWebEvent', 'onServerMessage() EMOTICON/IMAGE/VIDEO/VOICE/MICROVIDEO message')
m = new MediaMessage(data)
break
case MsgType.TEXT:
if (m.typeSub() === MsgType.LOCATION) {
log.verbose('PuppetWebEvent', 'onServerMessage() (TEXT&LOCATION) message')
m = new MediaMessage(data)
}
break
}
// To Be Deleted: set self...
......
......@@ -315,11 +315,12 @@ export class PuppetWeb extends Puppet {
, room ? room.topic() : (to as Contact).name()
, content
)
try {
await this.bridge.send(destination.id, content)
.catch(e => {
} catch(e) {
log.error('PuppetWeb', 'send() exception: %s', e.message)
throw e
})
}
return
}
......@@ -349,36 +350,32 @@ export class PuppetWeb extends Puppet {
* logout from browser, then server will emit `logout` event
*/
public async logout(): Promise<void> {
try {
await this.bridge.logout()
.catch(e => {
} catch (e) {
log.error('PuppetWeb', 'logout() exception: %s', e.message)
throw e
})
return
}
public getContact(id: string): Promise<any> {
if (!this.bridge) {
throw new Error('PuppetWeb has no bridge for getContact()')
}
return this.bridge.getContact(id)
.catch(e => {
public async getContact(id: string): Promise<any> {
try {
return await this.bridge.getContact(id)
} catch(e) {
log.error('PuppetWeb', 'getContact(%d) exception: %s', id, e.message)
throw e
})
}
}
public logined(): boolean { return !!(this.user) }
public ding(data?: any): Promise<string> {
if (!this.bridge) {
return Promise.reject(new Error('ding fail: no bridge(yet)!'))
}
return this.bridge.ding(data)
.catch(e => {
public async ding(data?: any): Promise<string> {
try {
return await this.bridge.ding(data)
} catch (e) {
log.warn('PuppetWeb', 'ding(%s) rejected: %s', data, e.message)
throw e
})
}
}
public async contactRemark(contact: Contact, remark: string): Promise<boolean> {
......
......@@ -435,6 +435,12 @@
return location + path
}
function getMsgPublicLinkImg(id) {
var location = window.location.href.replace(/\/$/, '')
var path = '/cgi-bin/mmwebwx-bin/webwxgetpubliclinkimg?url=xxx&msgid=' + id + '&pictype=location'
return location + path
}
function send(ToUserName, Content) {
var chatFactory = WechatyBro.glue.chatFactory
var confFactory = WechatyBro.glue.confFactory
......@@ -766,6 +772,7 @@
, getMsgEmoticon
, getMsgVideo
, getMsgVoice
, getMsgPublicLinkImg
// for Wechaty Contact Class
, contactFindAsync
......
......@@ -110,7 +110,7 @@ test('downloadStream() for media', t => {
})
server.listen(8000)
UtilLib.downloadStream('http://127.0.0.1:8000/ding', [{name: 'life', value: 42}])
UtilLib.urlStream('http://127.0.0.1:8000/ding', [{name: 'life', value: 42}])
.then(s => {
s.on('data', (chunk) => {
// console.log(`BODY: ${chunk}`)
......
......@@ -5,7 +5,9 @@
* https://github.com/wechaty/wechaty
*
*/
import * as https from 'https'
import * as http from 'http'
import * as url from 'url'
import { log } from './config'
......@@ -96,46 +98,84 @@ export class UtilLib {
)
}
public static downloadStream(url: string, cookies: any[]): Promise<NodeJS.ReadableStream> {
public static urlStream(href: string, cookies: any[]): Promise<NodeJS.ReadableStream> {
// const myurl = 'http://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetmsgimg?&MsgID=3080011908135131569&skey=%40crypt_c117402d_53a58f8fbb21978167a3fc7d3be7f8c9'
url = url.replace(/^https/i, 'http') // use http for better performance
const options = require('url').parse(url)
// url = url.replace(/^https/i, 'http') // use http for better performance
console.log(href)
const u = url.parse(href)
const protocol: 'https:'|'http:' = u.protocol as any
console.log(u)
let options
// let request
if (protocol === 'https:') {
// request = https.request.bind(https)
options = u as https.RequestOptions
options.agent = https.globalAgent
} else if (protocol === 'http:') {
// request = http.request.bind(http)
options = u as http.RequestOptions
options.agent = http.globalAgent
} else {
throw new Error('protocol unknown: ' + protocol)
}
options.headers = {
// Accept: 'image/webp,image/*,*/*;q=0.8'
Accept: '*/*'
, Referer: 'https://wx.qq.com/'
, Range: 'bytes=0-'
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36',
// Accept: 'image/webp,image/*,*/*;q=0.8',
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', // MsgType.IMAGE | VIDEO
// Accept: '*/*',
Host: options.hostname, // 'wx.qq.com', // MsgType.VIDEO | IMAGE
Referer: protocol + '//wx.qq.com/',
// 'Upgrade-Insecure-Requests': 1, // MsgType.VIDEO | IMAGE
Range: 'bytes=0-',
, 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36'
// , 'Accept-Encoding': 'gzip, deflate, sdch'
, 'Accept-Encoding': 'identity;q=1, *;q=0'
, 'Accept-Language': 'zh-CN,zh;q=0.8'
// 'Accept-Encoding': 'gzip, deflate, sdch',
'Accept-Encoding': 'gzip, deflate, sdch, br', // MsgType.IMAGE | VIDEO
// 'Accept-Encoding': 'identity;q=1, *;q=0',
'Accept-Language': 'zh-CN,zh;q=0.8', // MsgType.IMAGE | VIDEO
// 'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.6,en-US;q=0.4,en;q=0.2',
}
options.agent = http.globalAgent
/**
* 'pgv_pvi=6639183872; pgv_si=s8359147520;
* webwxuvid=747895d9dac5a25dd3a78175a5e931d879e026cacaf3ac06de0bd5f0714 ... ;
* mm_lang=zh_CN; MM_WX_NOTIFY_STATE=1; MM_WX_SOUND_STATE=1; wxloadtime=1465928826_expired;
* wxpluginkey=1465901102; wxuin=1211516682; wxsid=zMT7Gb24aTQzB1rA;
* webwx_data_ticket=gSeBbuhX+0kFdkXbgeQwr6Ck'
* pgv_pvi=6639183872; pgv_si=s8359147520; webwx_data_ticket=gSeBbuhX+0kFdkXbgeQwr6Ck
*/
options.headers.Cookie = cookies.map(c => `${c['name']}=${c['value']}`).join('; ')
options.headers['Cookie'] = cookies.map(c => `${c['name']}=${c['value']}`).join('; ')
// log.verbose('Util', 'Cookie: %s', options.headers.Cookie)
console.log(options)
return new Promise((resolve, reject) => {
const req = http.request(options, (res) => {
/*
const req = request(options, (res) => {
// console.log(`STATUS: ${res.statusCode}`);
// console.log(`HEADERS: ${JSON.stringify(res.headers)}`);
// res.setEncoding('utf8');
resolve(res)
})
*/
let req
if (protocol === 'https:') {
req = https.request(options, resolve)
} else {
req = http.request(options, resolve)
}
req.on('error', (e) => {
log.warn('WebUtil', `downloadStream() problem with request: ${e.message}`)
reject(e)
})
req.end()
req.end(() => {
log.verbose('UtilLib', 'urlStream() req.end() request sent')
})
})
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册