UNPKG

wx

Version:

W(ei)X(in) -- (minimalist) WeChat Middleware for Express.js

1,256 lines (1,239 loc) 51.1 kB
// Generated by CoffeeScript 1.10.0 (function() { var _, _s, api_binary, api_common, api_qrcode, async, crypto, exec, express, fs, getRawBody, path, qs, redis, request, xml2js, slice = [].slice; fs = require('fs'); path = require('path'); crypto = require('crypto'); exec = require('child_process').exec; redis = require('redis'); _ = require('underscore'); _s = require('underscore.string'); qs = require('qs'); request = require('request'); getRawBody = require('raw-body'); xml2js = require('xml2js'); express = require('express'); async = require('async'); api_common = 'https://api.weixin.qq.com/cgi-bin'; api_qrcode = 'https://mp.weixin.qq.com/cgi-bin'; api_binary = 'http://file.api.weixin.qq.com/cgi-bin'; module.exports = function(arg) { var WXMsgCrypt, access_token, app_id, app_secret, click_handlers, debug, encoding_aes_key, fetch_access_token, host, identify_qrcode, json_2_markdown, key_access_token, lock_fetching, markdown_2_json, markdown_2_json_placeholder, options, populate_user, port, qrcode_long_poll_timeout, qrcode_permanent_channel, qrcode_permanent_maximum, qrcode_temporary_expires, redis_client, redis_options, redis_pubsub, ref, regex_media_id, render_admin, reply, router, scan_handlers, text_handlers, token, unsent_dt_responses, unsent_mb_responses, user_actions, wrap, wx, wx_msg_crypt; token = arg.token, app_id = arg.app_id, app_secret = arg.app_secret, encoding_aes_key = arg.encoding_aes_key, redis_options = arg.redis_options, populate_user = arg.populate_user, debug = arg.debug; ref = redis_options != null ? redis_options : { port: null, host: null, options: null }, port = ref.port, host = ref.host, options = ref.options; redis_pubsub = redis.createClient(port, host, options); redis_client = redis.createClient(port, host, options); access_token = null; if (encoding_aes_key) { WXMsgCrypt = require('./wx-msg-crypt'); wx_msg_crypt = new WXMsgCrypt(token, encoding_aes_key, app_id); } if (populate_user == null) { populate_user = true; } click_handlers = {}; scan_handlers = {}; text_handlers = []; qrcode_permanent_maximum = 100000; qrcode_temporary_expires = 1800; qrcode_long_poll_timeout = 30; qrcode_permanent_channel = 'permanent'; unsent_dt_responses = {}; unsent_mb_responses = {}; key_access_token = "WX:ACCESS_TOKEN:" + app_id; redis_pubsub.subscribe(key_access_token, 'WX:SCAN:TEMPORARY', 'WX:SEND:MOBILE', 'WX:SEND:DESKTOP'); redis_pubsub.on('message', (function(_this) { return function(channel, message) { var desktop_callback, dt_res, i, len, mb_res, mobile, name, query, ref1, ref2, ref3, req, res, results, session; message = JSON.parse("" + message); switch (channel) { case key_access_token: return access_token = message; case 'WX:SCAN:TEMPORARY': if (!(dt_res = unsent_dt_responses[message.id])) { return; } delete unsent_dt_responses[message.id]; ref1 = JSON.parse(message.id), session = ref1[0], name = ref1[1], query = ref1[2]; req = dt_res[0], res = dt_res[1]; res.header('Access-Control-Allow-Origin', req.headers.origin); res.header('Access-Control-Allow-Credentials', true); if (scan_handlers[name]) { _(req).extend(message.content); _(req.user != null ? req.user : req.user = {}).extend(message.user, user_actions); mobile = reply(req, message.id); desktop_callback = function() { return res.send([message.msg_id].concat(Array.prototype.slice.call(arguments))); }; return async.eachSeries(scan_handlers[name], function(handler, callback) { return handler(req, mobile, desktop_callback, callback); }, function(err) { if (err) { return console.error(err); } }); } else { return res.send([message.user]); } break; case 'WX:SEND:MOBILE': if (!(mb_res = unsent_mb_responses[message.id])) { return; } delete unsent_mb_responses[message.id]; return mb_res[message.type](message.content); case 'WX:SEND:DESKTOP': if (!(dt_res = unsent_dt_responses[message.id])) { return; } delete unsent_dt_responses[message.id]; ref2 = JSON.parse(message.id), session = ref2[0], name = ref2[1], query = ref2[2]; if (name === qrcode_permanent_channel) { results = []; for (i = 0, len = dt_res.length; i < len; i++) { ref3 = dt_res[i], req = ref3[0], res = ref3[1]; res.header('Access-Control-Allow-Origin', req.headers.origin); res.header('Access-Control-Allow-Credentials', true); results.push(res.send(message.content)); } return results; } else { dt_res[1].header('Access-Control-Allow-Origin', dt_res[0].headers.origin); dt_res[1].header('Access-Control-Allow-Credentials', true); return dt_res[1].send(message.content); } } }; })(this)); wx = router = express.Router(); lock_fetching = 5; (fetch_access_token = (function(_this) { return function() { redis_client.watch(key_access_token); return redis_client.ttl(key_access_token, function(err, ttl) { return redis_client.get(key_access_token, function(err, _access_token) { var ref1; if (ttl > lock_fetching) { return redis_client.multi().exec(function(err, multi_res) { if (!multi_res) { return fetch_access_token(); } access_token = _access_token; return setTimeout(fetch_access_token, (ttl - lock_fetching) * 1000); }); } else if ((ref1 = "" + _access_token) !== 'FETCHING') { return redis_client.multi().setex(key_access_token, lock_fetching, 'FETCHING').exec(function(err, multi_res) { if (!multi_res) { return fetch_access_token(); } return request({ method: 'GET', url: api_common + "/token?grant_type=client_credential&appid=" + app_id + "&secret=" + app_secret, json: true }, function(err, res) { var expires_in, ref2; if (!res) { return fetch_access_token(); } if (res.body.errcode) { console.error('微信认证失败,登录微信公共平台获取开发者凭据 https://mp.weixin.qq.com'); return redis_client.del(key_access_token); } else { ref2 = res.body, _access_token = ref2.access_token, expires_in = ref2.expires_in; redis_client.setex(key_access_token, expires_in, _access_token); redis_client.publish(key_access_token, JSON.stringify(_access_token)); return setTimeout(fetch_access_token, (expires_in - lock_fetching) * 1000); } }); }); } else { return setTimeout(fetch_access_token, ttl * 1000); } }); }); }; })(this))(); identify_qrcode = function(req) { var name, query, ref1, session; name = ((ref1 = req.params.name) != null ? ref1 : '').toLowerCase(); query = _(req.query).clone(); delete query.t; if ((1 <= name && name <= qrcode_permanent_maximum)) { session = ''; query = _({ scene_id: +name }).extend(query); name = qrcode_permanent_channel; } else { session = req.session.id; } return [session, name, query]; }; router.get('/qrcode/:name?', (function(_this) { return function(req, res) { var create, name, query, ref1, session; ref1 = identify_qrcode(req), session = ref1[0], name = ref1[1], query = ref1[2]; create = function(scene_id) { var json; json = { 'action_name': name === qrcode_permanent_channel ? 'QR_LIMIT_SCENE' : 'QR_SCENE', 'action_info': { 'scene': { 'scene_id': scene_id } } }; if (name !== qrcode_permanent_channel) { json.expire_seconds = qrcode_temporary_expires; } return request({ method: 'POST', url: api_common + "/qrcode/create?access_token=" + access_token, json: json }, function(err, result) { var key, ticket, value; if (err) { console.error(err); return res.status(500).end(); } ticket = result.body.ticket; key = "WX:TICKETS:" + ticket; value = JSON.stringify([session, name, query]); if (name === qrcode_permanent_channel) { redis_client.set(key, value); } else { redis_client.setex(key, 1800, value); } return res.redirect(api_qrcode + "/showqrcode?ticket=" + (encodeURI(ticket))); }); }; if (name === qrcode_permanent_channel) { return create(query.scene_id); } else { return redis_client.incr('WX:SCENE_ID', function(err, scene_id) { return create(scene_id + qrcode_permanent_maximum); }); } }; })(this)); router.get('/scan/:name?', function(req, res) { var id, name, query, ref1, session; ref1 = identify_qrcode(req), session = ref1[0], name = ref1[1], query = ref1[2]; id = JSON.stringify([session, name, query]); if (name === qrcode_permanent_channel) { if (unsent_dt_responses[id] == null) { unsent_dt_responses[id] = []; } unsent_dt_responses[id].push([req, res]); } else { unsent_dt_responses[id] = [req, res]; redis_client.setex("WX:SCAN:QUERY:" + id, qrcode_long_poll_timeout, 1); } return setTimeout(function() { var dt_res; if (!(dt_res = unsent_dt_responses[id])) { return; } if (name === qrcode_permanent_channel) { if (dt_res.some(function(arg1) { var _req; _req = arg1[0]; return _req === req; })) { unsent_dt_responses[id] = unsent_dt_responses[id].filter(function(arg1) { var _req; _req = arg1[0]; return _req !== req; }); if (!unsent_dt_responses[id].length) { delete unsent_dt_responses[id]; } return res.status(404).end(); } } else { if (dt_res[0] === req) { delete unsent_dt_responses[id]; } return res.status(404).end(); } }, qrcode_long_poll_timeout * 1000); }); router.get('/wx.js', function(req, res) { return res.sendFile(__dirname + "/wx_client.js"); }); router.get('/ace/ace.js', function(req, res) { return res.sendFile(__dirname + "/ace/ace.js"); }); router.get('/ace/mode-markdown.js', function(req, res) { return res.sendFile(__dirname + "/ace/mode-markdown.js"); }); router.get('/ace/theme-terminal.js', function(req, res) { return res.sendFile(__dirname + "/ace/theme-terminal.js"); }); markdown_2_json_placeholder = 'function markdown_2_json() {}'; render_admin = function(markdown) { return ("" + (fs.readFileSync(__dirname + "/admin.html"))).replace('MARKDOWN', markdown).replace(markdown_2_json_placeholder, markdown_2_json.toString()).replace('APPSECRET', app_secret); }; router.get('/admin', function(req, res) { return res.sendFile(__dirname + "/login.html"); }); router.post('/admin', function(req, res) { if (req.body.app_secret !== app_secret) { return res.send(fs.readFileSync(__dirname + "/login.html").toString().replace('ERROR', 'show')); } else if (req.body.action === '开始管理') { return wx.get_menu(function(err, json) { if (err) { return res.status(500).send(err); } return res.send(render_admin(json_2_markdown(json))); }); } else if (req.body.action === '更新菜单') { return wx.create_menu(markdown_2_json(req.body.buttons), function(err) { if (err) { console.error(err); if (err) { return res.status(500).end(); } } return wx.get_menu(function(err, json) { if (err) { console.error(err); return res.status(500).end(); } return res.send(render_admin(json_2_markdown(json))); }); }); } }); router.use((function(_this) { return function(arg1, res, next) { var message, nonce, ref1, signature, timestamp; ref1 = arg1.query, signature = ref1.signature, timestamp = ref1.timestamp, nonce = ref1.nonce; message = _([token, timestamp, nonce]).sort().join(''); if (signature === crypto.createHash('sha1').update(message).digest('hex')) { return next(); } return res.status(401).end(); }; })(this)); router.get('/', function(arg1, res) { var echostr; echostr = arg1.query.echostr; return res.send(echostr); }); router.post('/', (function(_this) { return function(req, res, next) { return getRawBody(req, { length: req.headers['content-length'], limit: '1mb', encoding: 'utf8' }, function(err, string) { var callback; if (err) { return res.status(400).end(); } if (debug) { console.info(string); } callback = function(err, result) { var message, process_message, ref1, ref2; if (err) { return res.status(400).end(); } message = Object.keys(result.xml).reduce(function(memo, key) { memo[_s.underscored(key)] = result.xml[key][0]; return memo; }, {}); if (message.msg_id == null) { message.msg_id = message.from_user_name + "@" + (Date.now()); } process_message = function(user) { var handlers, msg_type, scan, subscribe; _(req).extend(message); _(req.user != null ? req.user : req.user = {}).extend(user); _(res).extend(reply(req)); scan = function(params) { var name, process_scan, query, session; process_scan = function() { var desktop_callback, id, mobile; id = JSON.stringify([session, name, query]); if (name === qrcode_permanent_channel) { if (scan_handlers[name]) { _(req).extend(message); _(req.user != null ? req.user : req.user = {}).extend(user, user_actions); req.params.scene_id = query.scene_id; _(req.query).extend(query); req.url += "&" + qs.stringify(query); mobile = res; desktop_callback = function() { return redis_client.publish('WX:SEND:DESKTOP', JSON.stringify({ id: id, content: [message.msg_id].concat(Array.prototype.slice.call(arguments)) })); }; return async.eachSeries(scan_handlers[name], function(handler, callback) { return handler(req, mobile, desktop_callback, callback); }, function(err) { if (err) { console.error(err); return mobile.status(500).end(); } else { return mobile.ok(); } }); } else { res.ok(); return redis_client.publish('WX:SEND:DESKTOP', JSON.stringify({ id: id, content: [message.msg_id, user] })); } } else { return redis_client.exists("WX:SCAN:QUERY:" + id, (function(_this) { return function(err, querying) { if (querying) { redis_client.del("WX:SCAN:QUERY:" + id); redis_client.publish('WX:SCAN:TEMPORARY', JSON.stringify({ id: id, user: user, content: message, msg_id: message.msg_id })); if (scan_handlers[name]) { return unsent_mb_responses[id] = res; } else { return res.ok(); } } else if (scan_handlers[name]) { _(req.query).extend(query); req.url += "&" + qs.stringify(query); return async.eachSeries(scan_handlers[name], function(scan_handler, callback) { return scan_handler(req, res, (function() {}), callback); }, function(err) { if (err) { console.error(err); return res.status(500).end(); } else { return res.ok(); } }); } else { return res.ok(); } }; })(this)); } }; if (params) { session = params[0], name = params[1], query = params[2]; return process_scan(); } else { return redis_client.get("WX:TICKETS:" + message.ticket, (function(_this) { return function(err, result) { var ref1, ref2, scene_id; if (result) { ref1 = JSON.parse("" + result), session = ref1[0], name = ref1[1], query = ref1[2]; } else if ((1 <= (ref2 = (scene_id = message.event_key.match(/\d+/)[0])) && ref2 <= qrcode_permanent_maximum)) { session = ''; name = qrcode_permanent_channel; query = { scene_id: +scene_id }; } else { return res.ok(); } return process_scan(); }; })(this)); } }; switch (msg_type = req.msg_type.toLowerCase()) { case 'text': return async.eachSeries(text_handlers, function(arg1, callback) { var handlers, match, pattern; pattern = arg1[0], handlers = arg1[1]; if (match = message.content.trim().match(pattern)) { _(req.params).extend(match); return async.eachSeries(handlers, function(handler, callback) { return handler(req, res, callback); }, callback); } else { return callback(); } }, function(err) { if (err) { console.error(err); return res.status(500).end(); } else { return res.ok(); } }); case 'image': case 'voice': case 'video': case 'location': case 'link': case 'device_text': return async.eachSeries(this[msg_type + "_handlers"] || [], function(handler, callback) { return handler(req, res, callback); }, function(err) { if (err) { console.error(err); return res.status(500).end(); } else { return res.ok(); } }); case 'event': switch (req.event.toLowerCase()) { case 'subscribe': subscribe = function() { return async.eachSeries(this.subscribe_handlers || [], function(handler, callback) { return handler(req, res, callback); }, function(err) { if (err) { console.error(err); return res.status(500).end(); } else { return res.ok(); } }); }; if (!message.ticket) { return subscribe(); } return redis_client.get("WX:TICKETS:" + message.ticket, (function(_this) { return function(err, result) { var name, query, ref1, ref2, scene_id, session; if (result) { ref1 = JSON.parse("" + result), session = ref1[0], name = ref1[1], query = ref1[2]; } else if ((1 <= (ref2 = (scene_id = message.event_key.match(/\d+/)[0])) && ref2 <= qrcode_permanent_maximum)) { session = ''; name = qrcode_permanent_channel; query = { scene_id: +scene_id }; } else { return subscribe(); } if (scan_handlers[name]) { return scan([session, name, query]); } else { subscribe(); return redis_client.publish('WX:SEND:DESKTOP', JSON.stringify({ id: JSON.stringify([session, name, query]), content: [message.msg_id, user] })); } }; })(this)); case 'unsubscribe': return async.eachSeries(this.unsubscribe_handlers || [], function(handler, callback) { return handler(req, res, callback); }, function(err) { if (err) { console.error(err); return res.status(500).end(); } else { return res.ok(); } }); case 'click': if (handlers = click_handlers[req.event_key]) { return async.eachSeries(handlers, function(handler, callback) { return handler(req, res, callback); }, function(err) { if (err) { console.error(err); return res.status(500).end(); } else { return res.ok(); } }); } else { return res.ok(); } break; case 'scan': return scan(); case 'templatesendjobfinish': return async.eachSeries(this.templatesendjobfinish_handlers || [], function(handler, callback) { return handler(req, res, callback); }, function(err) { if (err) { console.error(err); return res.status(500).end(); } else { return res.ok(); } }); default: return res.ok(); } } }; if (populate_user && ((ref1 = (ref2 = message.event) != null ? ref2.toLowerCase() : void 0) !== 'unsubscribe')) { return wx.user(message.from_user_name, function(err, user) { if (err) { if (debug) { console.error(err); } return process_message(wx.user(message.from_user_name)); } return process_message(user); }); } else { return process_message(wx.user(message.from_user_name)); } }; if (encoding_aes_key) { return wx_msg_crypt.decrypt(string, callback); } else { return xml2js.parseString(string, callback); } }); }; })(this)); regex_media_id = /^[\w\_\-]{64}$/; reply = function(req, id) { var message; if (id) { return ['text', 'image', 'voice', 'video', 'music', 'news', 'transfer', 'ok'].reduce(function(memo, type) { memo[type] = function(content) { return redis_client.publish('WX:SEND:MOBILE', JSON.stringify({ id: id, type: type, content: content })); }; return memo; }, {}); } if (encoding_aes_key) { message = function(message) { return wx_msg_crypt.encrypt("<xml> <ToUserName><![CDATA[" + req.from_user_name + "]]></ToUserName> <FromUserName><![CDATA[" + req.to_user_name + "]]></FromUserName> <CreateTime>" + (~~(Date.now() / 1000)) + "</CreateTime> " + message + " </xml>", req.query.timestamp, req.query.nonce); }; } else { message = function(message) { return "<xml> <ToUserName><![CDATA[" + req.from_user_name + "]]></ToUserName> <FromUserName><![CDATA[" + req.to_user_name + "]]></FromUserName> <CreateTime>" + (~~(Date.now() / 1000)) + "</CreateTime> " + message + " </xml>"; }; } return { text: function(text) { return this.send(message("<MsgType><![CDATA[text]]></MsgType> <Content><![CDATA[" + text + "]]></Content>")); }, image: function(image) { var send; send = (function(_this) { return function(image) { return _this.send(message("<MsgType><![CDATA[image]]></MsgType> <Image><MediaId><![CDATA[" + image + "]]></MediaId></Image>")); }; })(this); if (typeof image === 'string' && image.match(regex_media_id)) { return send(image); } else { return wx.upload('image', image, (function(_this) { return function(err, res) { if (image = res != null ? res.media_id : void 0) { return send(image); } else { console.error(err || res); return _this.status(500).end(); } }; })(this)); } }, voice: function(voice) { var send; send = (function(_this) { return function(voice) { return _this.send(message("<MsgType><![CDATA[voice]]></MsgType> <Voice><MediaId><![CDATA[" + voice + "]]></MediaId></Voice>")); }; })(this); if (voice.match(regex_media_id)) { return send(voice); } else { return wx.upload('voice', voice, (function(_this) { return function(err, res) { if (!(voice = res != null ? res.media_id : void 0)) { return _this.status(500).end(); } return send(voice); }; })(this)); } }, video: function(video) { var send; send = (function(_this) { return function(arg1) { var description, title, video; video = arg1.video, title = arg1.title, description = arg1.description; return _this.send(message("<MsgType><![CDATA[video]]></MsgType> <Video> <MediaId><![CDATA[" + video + "]]></MediaId> <Title><![CDATA[" + title + "]]></Title> <Description><![CDATA[" + description + "]]></Description> </Video>")); }; })(this); if (video.video.match(regex_media_id)) { return send(video); } else { return wx.upload('video', video.video, (function(_this) { return function(err, res) { if (!(video.video = res != null ? res.media_id : void 0)) { return _this.status(500).end(); } return send(video); }; })(this)); } }, music: function(music) { var send; send = (function(_this) { return function(arg1) { var description, hq_music_url, music_url, thumb_media, title; title = arg1.title, description = arg1.description, music_url = arg1.music_url, hq_music_url = arg1.hq_music_url, thumb_media = arg1.thumb_media; return _this.send(message("<MsgType><![CDATA[music]]></MsgType> <Music> <Title><![CDATA[" + title + "]]></Title> <Description><![CDATA[" + description + "]]></Description> <MusicUrl><![CDATA[" + music_url + "]]></MusicUrl> <HQMusicUrl><![CDATA[" + hq_music_url + "]]></HQMusicUrl> <ThumbMediaId><![CDATA[" + thumb_media + "]]></ThumbMediaId> </Music>")); }; })(this); if (music.thumb_media.match(regex_media_id)) { return send(music); } else { return wx.upload('thumb', music.thumb_media, (function(_this) { return function(err, res) { if (!(music.thumb_media = res != null ? res.thumb_media_id : void 0)) { return _this.status(500).end(); } return send(music); }; })(this)); } }, news: function(articles) { articles = [].concat(articles).map(function(arg1) { var description, pic_url, title, url; title = arg1.title, description = arg1.description, pic_url = arg1.pic_url, url = arg1.url; return "<item> <Title><![CDATA[" + title + "]]></Title> <Description><![CDATA[" + description + "]]></Description> <PicUrl><![CDATA[" + pic_url + "]]></PicUrl> <Url><![CDATA[" + url + "]]></Url> </item>"; }); return this.send(message("<MsgType><![CDATA[news]]></MsgType> <ArticleCount>" + articles.length + "</ArticleCount> <Articles>" + (articles.join('')) + "</Articles>")); }, transfer: function() { return this.send(message("<MsgType><![CDATA[transfer_customer_service]]></MsgType>")); }, device: function(content) { content = (new Buffer(content)).toString('base64'); return this.send(message("<MsgType><![CDATA[device_text]]></MsgType> <DeviceType><![CDATA[" + req.device_type + "]]></DeviceType> <DeviceID><![CDATA[" + req.device_id + "]]></DeviceID> <SessionID>" + req.session_id + "</SessionID> <Content><![CDATA[" + content + "]]></Content>")); }, ok: function() { return this.status(200).end(); } }; }; user_actions = { image_url: function(size) { if (!this.headimgurl) { return null; } return this.headimgurl.replace(/\/0$/i, "/" + size); }, text: function(text, callback) { return request({ method: 'POST', url: api_common + "/message/custom/send?access_token=" + access_token, json: { touser: this.openid, msgtype: 'text', text: { content: text } } }, wrap(callback)); }, image: function(image, callback) { var send; send = (function(_this) { return function(image) { return request({ method: 'POST', url: api_common + "/message/custom/send?access_token=" + access_token, json: { touser: _this.openid, msgtype: 'image', image: { media_id: image } } }, wrap(callback)); }; })(this); if (typeof image === 'string' && image.match(regex_media_id)) { return send(image); } else { return wx.upload('image', image, function(err, res) { if (!(image = res != null ? res.media_id : void 0)) { return typeof callback === "function" ? callback(err || res) : void 0; } return send(image); }); } }, voice: function(voice, callback) { var send; send = (function(_this) { return function(voice) { return request({ method: 'POST', url: api_common + "/message/custom/send?access_token=" + access_token, json: { touser: _this.openid, msgtype: 'voice', voice: { media_id: voice } } }, wrap(callback)); }; })(this); if (voice.match(regex_media_id)) { return send(voice); } else { return wx.upload('voice', voice, function(err, res) { if (!(voice = res != null ? res.media_id : void 0)) { return typeof callback === "function" ? callback(err || res) : void 0; } return send(voice); }); } }, video: function(video, callback) { var send; send = (function(_this) { return function(arg1) { var description, title, video; title = arg1.title, description = arg1.description, video = arg1.video; return request({ method: 'POST', url: api_common + "/message/custom/send?access_token=" + access_token, json: { touser: _this.openid, msgtype: 'video', video: { title: title, description: description, media_id: video } } }, wrap(callback)); }; })(this); if (video.video.match(regex_media_id)) { return send(video); } else { return wx.upload('video', video.video, function(err, res) { if (!(video.video = res != null ? res.media_id : void 0)) { return typeof callback === "function" ? callback(err || res) : void 0; } return send(video); }); } }, music: function(music, callback) { var send; send = (function(_this) { return function(arg1) { var description, hq_music_url, music_url, thumb_media, title; title = arg1.title, description = arg1.description, music_url = arg1.music_url, hq_music_url = arg1.hq_music_url, thumb_media = arg1.thumb_media; return request({ method: 'POST', url: api_common + "/message/custom/send?access_token=" + access_token, json: { touser: _this.openid, msgtype: 'music', music: { title: title, description: description, musicurl: music_url, hqmusicurl: hq_music_url, thumb_media_id: thumb_media } } }, wrap(callback)); }; })(this); if (music.thumb_media.match(regex_media_id)) { return send(music); } else { return wx.upload('thumb', music.thumb_media, function(err, res) { if (!(music.thumb_media = res != null ? res.thumb_media_id : void 0)) { return typeof callback === "function" ? callback(err || res) : void 0; } return send(music); }); } }, news: function(news, callback) { return request({ method: 'POST', url: api_common + "/message/custom/send?access_token=" + access_token, json: { touser: this.openid, msgtype: 'news', news: { articles: news.map(function(arg1) { var description, pic_url, title, url; title = arg1.title, description = arg1.description, pic_url = arg1.pic_url, url = arg1.url; return { title: title, description: description, picurl: pic_url, url: url }; }) } } }, wrap(callback)); }, template: function(arg1, callback) { var data, template_id, topcolor, url; template_id = arg1.template_id, topcolor = arg1.topcolor, url = arg1.url, data = arg1.data; return request({ method: 'POST', url: api_common + "/message/template/send?access_token=" + access_token, json: { touser: this.openid, template_id: template_id, url: url, topcolor: topcolor || '#FF0000', data: data } }, wrap(callback)); } }; wrap = function(callback) { if (callback == null) { callback = function() {}; } return function(err, res) { var ref1; if (err || ((ref1 = res.body) != null ? ref1.errcode : void 0)) { return callback(err || res.body); } return callback(null, res.body); }; }; wx.markdown_2_json = markdown_2_json = function(markdown) { var base, buttons, i, json, len, line, match, parse, ref1, sub_buttons; parse = function(line) { var match; if (match = line.match(/\[(.*)\]\((.*)\)/)) { return { type: 'view', name: match[1], url: match[2] }; } else if (match = line.match(/\[(.*)\]\((.*)/)) { return { type: 'click', name: match[1], key: match[1] }; } else { return { type: 'click', name: line, key: line }; } }; buttons = []; ref1 = markdown.split(/\n/); for (i = 0, len = ref1.length; i < len; i++) { line = ref1[i]; line = line.trim(); if (match = line.match(/^[\+\+](.+)/)) { buttons.push(parse(match[1].trim())); } else if (match = line.match(/^[\-\-](.+)/)) { sub_buttons = (base = buttons[buttons.length - 1]).sub_button != null ? base.sub_button : base.sub_button = []; sub_buttons.push(parse(match[1].trim())); } } return json = buttons; }; json_2_markdown = function(json) { var button, buttons, i, j, len, len1, markdown, ref1, ref2, ref3, ref4, ref5, sub_button; markdown = ''; ref1 = buttons = json; for (i = 0, len = ref1.length; i < len; i++) { button = ref1[i]; if ((ref2 = button.sub_button) != null ? ref2.length : void 0) { markdown += "+ " + button.name + "\n"; ref3 = button.sub_button; for (j = 0, len1 = ref3.length; j < len1; j++) { sub_button = ref3[j]; if ((ref4 = sub_button.type) === 'view') { markdown += " - [" + sub_button.name + "](" + sub_button.url + ")\n"; } else { markdown += " - " + sub_button.name + "\n"; } } } else if ((ref5 = button.type) === 'view') { markdown += "+ [" + button.name + "](" + button.url + ")\n"; } else { markdown += "+ " + button.name + "\n"; } } return markdown; }; return _.extend(router, { access_token: function() { return access_token; }, text: (function(_this) { return function() { var handlers, pattern; handlers = 1 <= arguments.length ? slice.call(arguments, 0) : []; pattern = _.first(handlers); if (_.isRegExp(pattern)) { pattern = handlers.shift(); } else if (_.isFunction(pattern)) { pattern = /.*/; } else if (_.isString(pattern)) { pattern = new RegExp(handlers.shift(), 'i'); } text_handlers = _.filter(text_handlers, function(arg1) { var pattern_exist; pattern_exist = arg1[0]; return pattern.toString() !== pattern_exist.toString(); }); text_handlers.push([pattern, handlers]); return _this; }; })(this), image: (function(_this) { return function() { var image_handlers; image_handlers = 1 <= arguments.length ? slice.call(arguments, 0) : []; _this.image_handlers = image_handlers; return _this; }; })(this), voice: (function(_this) { return function() { var voice_handlers; voice_handlers = 1 <= arguments.length ? slice.call(arguments, 0) : []; _this.voice_handlers = voice_handlers; return _this; }; })(this), video: (function(_this) { return function() { var video_handlers; video_handlers = 1 <= arguments.length ? slice.call(arguments, 0) : []; _this.video_handlers = video_handlers; return _this; }; })(this), location: (function(_this) { return function() { var location_handlers; location_handlers = 1 <= arguments.length ? slice.call(arguments, 0) : []; _this.location_handlers = location_handlers; return _this; }; })(this), link: (function(_this) { return function() { var link_handlers; link_handlers = 1 <= arguments.length ? slice.call(arguments, 0) : []; _this.link_handlers = link_handlers; return _this; }; })(this), device: (function(_this) { return function() { var device_text_handlers; device_text_handlers = 1 <= arguments.length ? slice.call(arguments, 0) : []; _this.device_text_handlers = device_text_handlers; return _this; }; })(this), subscribe: (function(_this) { return function() { var subscribe_handlers; subscribe_handlers = 1 <= arguments.length ? slice.call(arguments, 0) : []; _this.subscribe_handlers = subscribe_handlers; return _this; }; })(this), unsubscribe: (function(_this) { return function() { var unsubscribe_handlers; unsubscribe_handlers = 1 <= arguments.length ? slice.call(arguments, 0) : []; _this.unsubscribe_handlers = unsubscribe_handlers; return _this; }; })(this), templatesendjobfinish: (function(_this) { return function() { var templatesendjobfinish_handlers; templatesendjobfinish_handlers = 1 <= arguments.length ? slice.call(arguments, 0) : []; _this.templatesendjobfinish_handlers = templatesendjobfinish_handlers; return _this; }; })(this), click: (function(_this) { return function() { var handlers, key; key = arguments[0], handlers = 2 <= arguments.length ? slice.call(arguments, 1) : []; click_handlers[key] = handlers; return _this; }; })(this), scan: (function(_this) { return function() { var channel, handlers; handlers = 1 <= arguments.length ? slice.call(arguments, 0) : []; switch (typeof _.first(handlers)) { case 'function': channel = ''; break; case 'string': channel = handlers.shift().toLowerCase(); } scan_handlers[channel] = handlers; return _this; }; })(this), user: (function(_this) { return function(openid, callback) { if (callback) { request({ method: 'GET', url: api_common + "/user/info?access_token=" + access_token + "&openid=" + openid, json: true }, function(err, res) { if (err || res.body.errcode) { return callback(err || res.body); } return callback(null, _(res.body).extend(user_actions)); }); return _this; } else { return _({ openid: openid }).extend(user_actions); } }; })(this), upload: (function(_this) { return function(type, media, callback) { var form, r; if (callback == null) { callback = function() {}; } r = request.post({ url: api_binary + "/media/upload?access_token=" + access_token + "&type=" + type, json: true }, function(err, res) { if (err) { return callback(err); } return callback(null, res.body); }); form = r.form(); form.append('media', typeof media === 'string' ? fs.createReadStream(media) : media); form.append('hack', ''); return _this; }; })(this), download: (function(_this) { return function(media_id, callback) { if (callback == null) { callback = function() {}; } return request({ url: api_binary + "/media/get?access_token=" + access_token + "&media_id=" + media_id, encoding: null }, function(err, res) { if (err) { return callback(err); } return callback(null, res.body); }); }; })(this), get_menu: function(callback) { if (callback == null) { callback = function() {}; } request({ method: 'GET', url: api_common + "/menu/get?access_token=" + access_token, json: true }, function(err, res) { var ref1, ref2; if (err || ((ref1 = res.body.errcode) !== (void 0) && ref1 !== 46003)) { return callback(err || res.body); } return callback(null, ((ref2 = res.body.menu) != null ? ref2.button : void 0) || []); }); return this; }, create_menu: function(buttons, callback) { request({ method: 'POST', url: api_common + "/menu/create?access_token=" + access_token, json: { button: buttons } }, wrap(callback)); return this; }, delete_menu: function(callback) { request({ method: 'GET', url: api_common + "/menu/delete?access_token=" + access_token, json: true }, wrap(callback)); return this; }, subscribers: function(next_openid, callback) { if (typeof next_openid === 'function') { callback = next_openid; next_openid = null; } request({ method: 'GET', url: api_common + "/user/get?access_token=" + access_token + (next_openid ? "&next_openid=" + next_openid : ''), json: true }, wrap(callback)); return this; }, populate_subscribers: function(start, end, callback) { var i, index, index_tasks, next_openid, results, total; index = ~~(end / 10000) + 1; next_openid = null; total = null; index_tasks = (function() { results = []; for (var i = 0; 0 <= index ? i <= index : i >= index; 0 <= index ? i++ : i--){ results.push(i); } return results; }).apply(this).map(function() { return function(callback) { var _callback; _callback = function(err, res) { if (err) { console.error(err); return callback(err); } next_openid = res.data.next_openid; total = res.total; return callback(null, res); }; return request({ method: 'GET', url: api_common + "/user/get?access_token=" + access_token + (next