diff --git a/.gitignore b/.gitignore
index 7c7da8b..acba75f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,4 +15,5 @@
*/log
.env
*.memory-card.json
-package-lock.json
\ No newline at end of file
+package-lock.json
+*.local
\ No newline at end of file
diff --git a/README.md b/README.md
index f0e3b61..28be550 100644
--- a/README.md
+++ b/README.md
@@ -1,367 +1,8 @@
-## 微信每日说
+## 微信机器人
-[](http://nodejs.cn/download/)
-[](https://github.com/Chatie/wechaty)
-
-
-
-
-wechatBot 是基于 node 与 [wechaty](https://github.com/Chatie/wechaty) 的微信小情话工具。最初功能只有每日发送天气和一句情话,后来添加了智能机器人聊天功能。但由于本项目面向小白用户与刚接触 node 开发的用户,故拆分了两个项目,一个是功能专一面向小白的 [《微信每日说》](https://github.com/gengchen528/wechatBot) (也就是本项目) ,另一个也在我的仓库下 [《智能微秘书》](https://github.com/gengchen528/wechat-assistant-pro) 面向有较多编程经验的用户。下面主要介绍微信每日说的使用
-
-## 最新通知 喜大普奔
-
-由于wechaty的升级,现已支持所有微信登录,就算你的微信之前不能登录web版,现在也可以用了,赶快来体验吧。
-
-### ~~遗憾的通知~~
-
-uos 又可以重新使用了~~由于UOS桌面版协议微信已经关闭了,没法再继续用桌面版协议登录了,现在只能换回web协议了。可以登录网页版微信的账号可以继续用,不能登录网页版协议的就不能用了。或者你可以申请Wechaty 的ipad local协议的token可以免费试用7天 。申请地址: https://github.com/padlocal/wechaty-puppet-padlocal~~
-
-### 主要功能
-
-- [x] 定时给女朋友发送每日天气提醒,以及每日一句
-- [x] 天行机器人自动陪女朋友聊天(需要自己申请[天行机器人](https://www.tianapi.com/signup.html?source=474284281)api,不过目前开源的机器人 api 都不要抱太大希望,因为很傻的,如果你有发现好的机器人可以来推荐)
-- [x] 垃圾分类功能,使用方法:?垃圾名称
-- [x] 想要更多群管理,自动加好友功能,群定时任务,好友定时任务,并体验在线配置服务,请移步[《智能微秘书》](https://github.com/gengchen528/wechat-assistant-pro)
-
-### 可选聊天机器人
-
-- 天行机器人: 默认设置为天行机器人(智能化程度一般),还是建议大家自行注册自己账号 [天行数据官网](https://www.tianapi.com/signup.html?source=474284281)
-- 图灵机器人: 目前比较智能的机器人,但是需要注册后进行身份认证,才可调用,且每天只可免费调用 100 次(收费标准 99 元/月,每天 1000 次)[图灵官网](http://www.tuling123.com)
-
-## 天行数据需要申请的api (重要)
-
-如遇到获取不到天气数据,或者机器人无法自动回复等问题,请登录天行数据个人中心查看是否申请了对应的接口权限,以下链接为快速申请链接:
-
-* 天行机器人: [https://www.tianapi.com/apiview/47](https://www.tianapi.com/apiview/47)
-* 天气查询:[https://www.tianapi.com/apiview/72](https://www.tianapi.com/apiview/72)
-* 垃圾分类: [https://www.tianapi.com/apiview/97](https://www.tianapi.com/apiview/97)
-* 土味情话: [https://www.tianapi.com/apiview/80](https://www.tianapi.com/apiview/80)
-* 天行图灵机器人: [https://www.tianapi.com/apiview/98](https://www.tianapi.com/apiview/98)
-
-## 环境
-
-- `node.js` ( 16 > version, 推荐使用 **V16**)
-- `Mac / Linux / Windows`
-
-## docker 部署
-
-### 直接拉取镜像(推荐)
-
-由于自己构建部分依赖安装比较慢,或者经常会卡住,所以本项目已经提前构建好发布到dockerhub了,直接pull就行了
-
-#### step1: 拉取镜像
-
-```shell
-
-docker pull aibotk/wechat-bot
-
-```
-
-#### step2: 配置`config/index.js`
-
-目录`config/index.js`中的内容按照说明配置,请注意阅读说明
-
-#### step3: 启动docker
-
-以下两个命令自己选择一个执行就行,执行的时候会下载puppet,可能会比较慢,耐心等待一下即可
-
-1、请在项目根目录执行,这个命令是前台执行可以直接看到log日志的,但是没法关闭,只能销毁终端实例
-
-```shell
-
-docker run -e TZ="Asia/Shanghai" --name=chatBot --volume="$(pwd)/config/":/bot/config aibotk/wechat-bot
-
-```
-
-2、这个命令可以在后台运行,多了一个`-d`
-
-```shell
-
-docker run -e TZ="Asia/Shanghai" -d --name=chatBot --volume="$(pwd)/config/":/bot/config aibotk/wechat-bot
-
-```
-
-[如何查看docker日志](https://www.cnblogs.com/mydesky2012/p/11430394.html)
-
-### 自行构建docker镜像 (不建议)
-
-需要提前安装 docker 环境,并且配置好`config/index.js`中的内容
-
-```shell script
-docker build -t wechat-bot .
-docker run wechat-bot
-```
-
-## 安装配置
-
-视频教程: 《三步教你用 Node 做一个微信哄女友神器》
-
-### 下载安装 node
-
-访问 node 官网:[http://nodejs.cn/download/](http://nodejs.cn/download/),下载系统对应版本的 node 安装包,并执行安装。
-
-> 1. windows 下安装步骤详细参考 [NodeJs 安装 Windwos 篇](https://www.cnblogs.com/liuqiyun/p/8133904.html)
-> 2. Mac 下安装详细步骤参考 [NodeJs 安装 Mac 篇](https://blog.csdn.net/qq_32407233/article/details/83758899)
-> 3. Linux 下安装详细步骤参考 [NodeJs 安装 Linux 篇](https://www.cnblogs.com/liuqi/p/6483317.html)
-
-### 配置 npm 源
-
-配置 `npm` 源为淘宝源(重要,因为需要安装 `chromium`,不配置的话下载会失败或者速度很慢,因为这个玩意 140M 左右)
-
-```bash
-npm config set registry https://registry.npmmirror.com/
-npm config set disturl https://npm.taobao.org/dist
-npm config set puppeteer_download_host https://npm.taobao.org/mirrors
-```
-
-### 下载代码
-
-
-
-```bash
-# 如果没有安装 git,也可直接下载项目zip包
-git clone https://github.com/leochen-g/wechatBot.git
-cd wechatBot
-npm install
-```
-
-### 项目配置
-
-所有配置项均在 `config/index.js` 文件中
-
-```javascript
- // 配置文件
- module.exports = {
- // 每日说配置项(必填项)
- NAME: 'leo助手', //女朋友备注姓名
- NICKNAME: 'leo助手', //女朋友昵称
- MEMORIAL_DAY: '2015/04/18', //你和女朋友的纪念日
- CITY: '上海', //女朋友所在城市(城市名称,不要带“市”)
- SENDDATE: '0 6 8 * * *', //定时发送时间 每天8点06分0秒发送,规则见 /schedule/index.js
- TXAPIKEY: '', //此处须填写个人申请的天行apikey,请替换成自己的(自行申请天行天气和土味情话的接口) 申请地址https://www.tianapi.com/signup.html?source=474284281
-
- //高级功能配置项(非必填项)
- AUTOREPLY: true, //自动聊天功能 默认开启, 关闭设置为 false
- DEFAULTBOT: '0', //设置默认聊天机器人 0 天行机器人 1 图灵机器人 2 天行对接的图灵机器人,需要到天行机器人官网充值(50元/年,每天1000次)
- AUTOREPLYPERSON: ['好友1备注','好友2备注'], //指定多个好友开启机器人聊天功能 指定好友的备注,最好不要带有特殊字符
- TULINGKEY: '图灵机器人apikey',//图灵机器人apikey,需要自己到图灵机器人官网申请,并且需要认证
-
- }
-```
-
-### 执行
-
-当以上步骤都完成后,在命令行界面输入 `node index.js`,第一次执行会下载 puppeteer,所以会比较慢,稍等一下,出现二维码后即可拿出微信扫描
-
-
-
-执行成功后可看到
-
-
-
-## 效果展示
-
-
-
-
-

-

-
-
-
-

-

-
-
-## 常见问题处理 (FAQ)
-
-问题解决基本方案:
-
-- 先检查 node 版本是否大于 16
-- 确认 npm 或 yarn 已经配置好淘宝源
-- 存在 package-lock.json 文件先删除
-- 删除`node_modules`后重新执行`npm install` 或`cnpm install`
-- 使用最新版[《智能微秘书》](https://github.com/leochen-g/wechat-assistant-pro),摆脱环境问题
-
-1. 我的微信号无法登陆
-
- 最新版代码已经解决不能登录的问题,放心拉最新代码使用就行了
-
- ~~从 2017 年 6 月下旬开始,使用基于 web 版微信接入方案存在大概率的被限制登陆的可能性。 主要表现为:无法登陆 Web 微信,但不影响手机等其他平台。 验证是否被限制登陆: https://wx.qq.com 上扫码查看是否能登陆。 更多内容详见:~~
-
- [~~Can not login with error message: 当前登录环境异常。为了你的帐号安全,暂时不能登录 web 微信。~~](https://github.com/Chatie/wechaty/issues/603)
-
- [[谣言] 微信将会关闭网页版本](https://github.com/Chatie/wechaty/issues/990)
-
- [~~新注册的微信号无法登陆~~](https://github.com/Chatie/wechaty/issues/872)
-
-2. 类似 Failed to download Chromium rxxx 的问题
- `ERROR: Failed to download Chromium r515411! Set "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" env variable to skip download.{ Error: read ETIMEDOUT at _errnoException (util.js:1041:11) at TLSWrap.onread (net.js:606:25) code: 'ETIMEDOUT', errno: 'ETIMEDOUT', syscall: 'read' }`
-
- 解决方案:[https://github.com/GoogleChrome/puppeteer/issues/1597](https://github.com/GoogleChrome/puppeteer/issues/1597)
-
- `npm config set puppeteer_download_host=https://npm.taobao.org/mirrors`
-
- `sudo npm install puppeteer --unsafe-perm=true --allow-root`
-
-3. 执行 `npm run start` 时无法安装 puppet-puppeteer&&Chromium
-
- - Centos7 下部署出现以下问题
- 
- 问题原因:[https://segmentfault.com/a/1190000011382062](https://segmentfault.com/a/1190000011382062)
- 解决方案:
- #依赖库
- yum install pango.x86_64 libXcomposite.x86_64 libXcursor.x86_64 libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXtst.x86_64 cups-libs.x86_64 libXScrnSaver.x86_64 libXrandr.x86_64 GConf2.x86_64 alsa-lib.x86_64 atk.x86_64 gtk3.x86_64 -y
-
- #字体
- yum install ipa-gothic-fonts xorg-x11-fonts-100dpi xorg-x11-fonts-75dpi xorg-x11-utils xorg-x11-fonts-cyrillic xorg-x11-fonts-Type1 xorg-x11-fonts-misc -y
- - ubuntu 下,下载 puppeteer 失败
- 问题原因:[https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#chrome-headless-doesnt-launch-on-unix](https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#chrome-headless-doesnt-launch-on-unix)
- 解决方案:
-
- sudo apt-get gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
-
- - windows 下,下载 puppeteer 失败
-
- 链接:https://pan.baidu.com/s/1YF09nELpO-4KZh3D2nAOhA
- 提取码:0mrz
-
- 把下载的文件放到如下图路径,并解压到当前文件夹中即可
- 
-
- - 下载 puppeteer 失败,Linux 和 Mac 执行以下命令
- `PUPPETEER_DOWNLOAD_HOST = https://npm.taobao.org/mirrors npm install wechaty-puppet-wechat`
-
- - 下载 puppeteer 失败,Windows 执行以下命令
-
- `SET PUPPETEER_DOWNLOAD_HOST = https://npm.taobao.org/mirrors npm install wechaty-puppet-wechat`
-
-4. 如图所示问题解决办法,关闭 win / mac 防火墙;如果公司网络有限制的话也可能引起无法启动问题
- 
-
-5. 支持 红包、转账、朋友圈… 吗 ?
-
- 支付相关 - 红包、转账、收款 等都不支持
-
-6. 更多关于 wechaty 功能相关接口
-
- [参考 wechaty 官网文档](https://wechaty.js.org/docs/)
-
-7. 希望输出运行日志相关 DEBUG 信息, 并保存到本地
- - 在运行前, 系统里输入 `export WECHATY_LOG=verbose` 就能将默认日志输出改为详细 (其他等级参考[官方文档](https://www.npmjs.com/package/brolog#loglevelnewlevel))
- - 保存到本地, 在支持 `bash` 环境的命令行中, 可以用这样的方式启动程序: `node index.js 2>&1 | tee bot.log`, 这样控制台和后台会同时显示/存储日志信息.
-
-8 .CentOS 安装 better-sqlite3 报错的问题
-
-* 首先执行 `sudo yum install centos-release-scl-rh`,`sudo yum install devtoolset-8-build `这两个方法
-
-* 安装相应的gdb,`sudo yum install devtoolset-8-gdb`
-
-* 同样,也可以安装相应版本的 gcc 和 g++,`sudo yum install devtoolset-8-gcc devtoolset-8-gcc-c++`
-
-* yum安装完后,原来的gcc不覆盖,所以需要执行enable脚本更新环境变量,`sudo source /opt/rh/devtoolset-8/enable`
-
-* 可以通过加入到profile里面开机自动`source, vim /etc/profile`, 跳到最后一行加入以下内容,`source /opt/rh/devtoolset-8/enable`
-
-参考文章 [better-sqlite3](https://www.cnblogs.com/clwsec/p/12493653.html)
-
-有其他问题也可添加小助手微信后,发送`'加群'`进入微信每日说技术交流群
-
-## 注意
-
-本项目属于个人兴趣开发,开源出来是为了技术交流,请勿使用此项目做违反微信规定或者其他违法事情。
-建议使用小号进行测试,有被微信封禁网页端登录权限的风险(客户端不受影响),请确保自愿使用。因为个人使用不当导致网页端登录权限被封禁,均与作者无关,谢谢理解
-
-## 最后
-
-加好友后发送`加群`,会自动拉你进入群聊,同时此微信号有更多高级功能等待你的发现。
-
-
-
-赶快亲自试一试吧,相信你会挖掘出更多好玩的功能
-
-关注公众号:随时获取最新消息
-
-
-
-## 鸣谢
-
-感谢[天行数据](https://www.tianapi.com/)提供,天气,土味情话,智能机器人 api 等接口
-
-## 捐助
-
-如果您认为这个项目对你有所帮助,是否可以为它捐助一点资金呢?
-
-不管钱多钱少,您的捐助将会激励我持续开发新的功能!🎉
-
-感谢您的支持!
-
-捐助方法如下:
-
-
-

-

-
+werobo fork自[wechatBot](https://github.com/gengchen528/wechatBot),保留了之前所有的功能。根据自己的需要添加了淘宝客优惠分享功能。关于具体的配置和使用,可以前往wechatBot项目查看。
## 更新日志
-2022-07-15
-
-- 升级Wechaty 到1.x,支持UOS协议
-
-2021-05-20
-
-- 添加docker支持,摆脱环境困扰
-
-2021-04-13
-
-- 重大更新,被限制web登录的微信也可以使用了
-
-2021-02-08
-
-- 更新 wechaty 版本
- 2020-12-07
-- 更新 wechaty 版本
-
-2020-04-02
-
-- 添加 docker 部署支持
-- 更新 wechaty 版本
-
-2019-07-05
-
-- 添加垃圾分类功能,默认开启,使用方法: ?垃圾名称
-
-2019-07-04
-
-- 添加天行数据的图灵机器人接口支持()
-
-2019-07-02
-
-- 添加机器人多人回复配置项
-- 添加图灵机器人与天行机器人可选配置项
-
-2019-06-27
-
-- 更新天气接口使用天行 api
-- 每日说添加每日情话
-- 依赖中直接加入`wechaty-puppet-puppeteer`安装
-- `.npmrc`中设置项目 npm 源为淘宝源
-- 添加错误解决方案
-
-2019-06-16
-
-- 更新 wechaty 版本,更改图灵机器人为天行机器人,简化操作配置,修改说明文档,更适合小白用户
-
-2019-03-06
-
-- 添加图灵机器人配置项,需要先去注册图灵机器人,[网址](http://www.tuling123.com)
-
-2019-03-04
-
-- 进群后播报欢迎词
-
-2019-03-02:
-
-- 添加自动加好友,自动拉群可配置项
-- 重启后可维持登录状态
+v0.1.0
++ 添加淘宝客优惠分享
diff --git a/config/index.js b/config/index.js
index 80a444b..b11856d 100644
--- a/config/index.js
+++ b/config/index.js
@@ -1,17 +1,17 @@
// 配置文件
module.exports = {
// 每日说配置项(必填项)
- NAME: 'Leo_chen', //女朋友备注姓名
- NICKNAME: 'Leo_chen', //女朋友昵称
+ NAME: '', //女朋友备注姓名
+ NICKNAME: '', //女朋友昵称
MEMORIAL_DAY: '2015/04/18', //你和女朋友的纪念日
CITY: '上海', //女朋友所在城市(城市名称,不要带“市”)
SENDDATE: '0 19 17 * * *', //定时发送时间 每天8点06分0秒发送,规则见 /schedule/index.js
- TXAPIKEY: '天行key', //此处须填写个人申请的天行apikey,请替换成自己的 申请地址https://www.tianapi.com/signup.html?source=474284281
+ TXAPIKEY: '', //此处须填写个人申请的天行apikey,请替换成自己的 申请地址https://www.tianapi.com/signup.html?source=474284281
// 高级功能配置项(非必填项)
AUTOREPLY: true, //自动聊天功能 默认开启, 关闭设置为: false
DEFAULTBOT: '0', //设置默认聊天机器人 0 天行机器人 1 图灵机器人 2 天行对接的图灵机器人,需要到天行机器人官网充值(50元/年,每天1000次)
- AUTOREPLYPERSON: ['Leo_chen','好友2备注'], //指定多个好友开启机器人聊天功能 指定好友的备注,最好不要带有特殊字符
+ AUTOREPLYPERSON: [''], //指定多个好友开启机器人聊天功能 指定好友的备注,最好不要带有特殊字符
TULINGKEY: '图灵机器人apikey',//图灵机器人apikey,需要自己到图灵机器人官网申请,并且需要认证
// (自定义) 如果你有 DIY 和基本的编程基础, 可以在这自己定义变量, 用于 js 文件访问, 包括设置简单的定时任务, 例如可以定义 task 数组
@@ -19,4 +19,17 @@ module.exports = {
// {nick: 'personA', time: '午饭后', emoji: '🌞', action: 'eat xx', date: '0 0 12 * * *'},
// {nick: 'personB', time: '晚饭前', emoji: '🌔', action: 'eat xx', date: '0 0 18 * * *'},
// {nick: 'personC', time: '睡前', emoji: '🌚', action: 'sleep', date: '0 0 22 * * *'}],
+
+ // 定义优惠平台配置
+ coupons: {
+ taobao: {
+ enable: true,
+ appKey: '', // 淘宝开放平台申请的appkey
+ appSecret: '', // 淘宝开放平台申请的appkey
+ adzoneId: '', // 推广位id,格式如mm_xxx_xxx_xxx,只需要最后一个_后面的数字
+ groups: [
+ {groupName: '淘宝综合优惠群', groupMaterialId: '3756,28026,27446,13366,3786', schedule: '*/10 * * * * *'},
+ ],
+ }
+ }
}
diff --git a/coupon/index.js b/coupon/index.js
new file mode 100644
index 0000000..46db2b0
--- /dev/null
+++ b/coupon/index.js
@@ -0,0 +1,14 @@
+const config = require('../config/index.js')
+const Taobao = require('./taobao')
+
+
+function startCoupon(bot) {
+ if (!config.coupons) {
+ return
+ }
+
+ taobao = new Taobao(config.coupons.taobao || {})
+ taobao.start(bot)
+}
+
+module.exports = startCoupon
\ No newline at end of file
diff --git a/coupon/taobao.js b/coupon/taobao.js
new file mode 100644
index 0000000..074fc98
--- /dev/null
+++ b/coupon/taobao.js
@@ -0,0 +1,180 @@
+const { FileBox } = require('file-box')
+
+const ApiClient = require('./tbsdk').ApiClient
+const schedule = require('../schedule')
+const { wait } = require('../utils')
+
+function Taobao(options) {
+ this.options = options // 加载配置
+ this.materials = new Map() // 初始化物料缓存
+ this.materialPage = new Map() // 初始化物料id的分页数据
+}
+
+/**
+ * 启动淘宝优惠分享
+ *
+ * @param {WechatyInterface} bot 微信机器人对象
+ */
+Taobao.prototype.start = function(bot) {
+ if (!this.options.enable) {
+ return
+ }
+
+ const groups = this.options.groups
+ for (let group of groups) {
+ schedule.setSchedule(group.schedule, async () => {
+ const mateiralIds = group.groupMaterialId.split(',')
+ this.sendMessage(bot, group.groupName, mateiralIds)
+ })
+ }
+}
+
+/**
+ * 发送消息
+ *
+ * @param {WechatyInterface} bot 微信机器人对象
+ * @param {String} groupName 群名
+ * @param {Array} mateiralIds 物料id数组
+ */
+Taobao.prototype.sendMessage = async function(bot, groupName, mateiralIds) {
+ // 从多个物料id中随机选取一个
+ const count = mateiralIds.length
+ const randomIndex = Math.floor(Math.random() * count)
+
+ let res = null
+ try {
+ res = await this.getMaterial(mateiralIds[randomIndex])
+ } catch(e) {
+ console.log(e)
+ return
+ }
+
+ if (res == null) {
+ return
+ }
+
+ const room = await bot.Room.find({ topic: groupName })
+ if (!room) {
+ console.log(`没有找到微信群:${groupName}`)
+ return
+ }
+
+ let shareUrl = ''
+ let couponAmount = 0
+
+ if (res.coupon_share_url) {
+ shareUrl = "https:" + res.coupon_share_url
+ couponAmount = res.coupon_amount
+ } else {
+ shareUrl = "https:" + res.click_url
+ }
+
+ const pictUrl = "https:" + res.pict_url
+ const title = res.title
+ const zkFinalPrice = res.zk_final_price
+ const finalPrice = (parseFloat(zkFinalPrice) - parseFloat(couponAmount)).toFixed(2)
+
+ console.log(`分享商品:${title}`)
+
+ // 发送图片
+ const fb = FileBox.fromUrl(pictUrl)
+ await room.say(fb)
+ await wait(2)
+
+ // 发送商品名和优惠
+ let text = `${title}\n【在售价】¥${zkFinalPrice}\n【券后价】¥${finalPrice}`
+ await room.say(text)
+ await wait(Math.round(Math.random() * 3))
+
+ // 发送淘口令
+ text = await this.createTaobaoPwd(shareUrl)
+ if (text === null) {
+ return
+ }
+
+ const firstIndex = text.indexOf('¥')
+ const lastIndex = text.indexOf('¥', firstIndex + 1)
+ text = text.substring(firstIndex, lastIndex + 1)
+ await room.say(text)
+ await wait(2)
+}
+
+/**
+ * 创建淘口令
+ * 接口详情:https://bigdata.taobao.com/api.htm?docId=31127&docType=2
+ *
+ * @param {String} url 联盟官方渠道获取的淘客推广链接
+ * @returns {String} 淘口令字符串
+ */
+Taobao.prototype.createTaobaoPwd = async function(url) {
+ try {
+ const res = await this.request('taobao.tbk.tpwd.create', {
+ url,
+ })
+
+ return res.data.model
+ } catch(e) {
+ console.log(e)
+ return null
+ }
+}
+
+/**
+ * 调用物料精选接口获取一条优惠数据
+ * 接口详情:https://bigdata.taobao.com/api.htm?docId=33947&docType=2
+ *
+ * @param {String|Number} materialId 官方的物料Id(详细物料id见:https://market.m.taobao.com/app/qn/toutiao-new/index-pc.html#/detail/10628875?_k=gpov9a)
+ * @returns {Object} 优惠信息对象
+ */
+Taobao.prototype.getMaterial = async function (materialId) {
+ if (this.materials.has(materialId) && this.materials.get(materialId).length > 0) {
+ return this.materials[materialId].pop()
+ }
+
+ let pageNo = 1
+ if (this.materialPage.has(materialId)) {
+ pageNo = this.materialPage.get(materialId) + 1
+ }
+ this.materialPage.set(materialId, pageNo)
+
+ try {
+ const res = await this.request('taobao.tbk.dg.optimus.material', {
+ 'page_size': 10, // 每次取10条数据
+ 'page_no': pageNo,
+ 'adzone_id': this.options.adzoneId,
+ 'material_id': materialId,
+ })
+
+ if (!res.result_list) {
+ return null
+ }
+
+ // 缓存数据,减少接口请求次数
+ this.materials[materialId] = res.result_list.map_data;
+ return this.materials[materialId].pop()
+ } catch(e) {
+ console.log(e)
+ return null
+ }
+}
+
+Taobao.prototype.request = function (apiname, params) {
+ const client = new ApiClient({
+ 'appkey': this.options.appKey,
+ 'appsecret': this.options.appSecret,
+ 'REST_URL': 'http://gw.api.taobao.com/router/rest'
+ });
+
+ return new Promise(function(resolve, reject) {
+ client.execute(apiname, params, function(error, response) {
+ if (error) {
+ console.log(error)
+ reject(error)
+ }
+
+ resolve(response)
+ })
+ })
+}
+
+module.exports = Taobao
\ No newline at end of file
diff --git a/coupon/tbsdk/.gitignore b/coupon/tbsdk/.gitignore
new file mode 100644
index 0000000..1c72d60
--- /dev/null
+++ b/coupon/tbsdk/.gitignore
@@ -0,0 +1,5 @@
+/Java/target
+/java/.*
+/bin/
+/bin/
+node_modules/
diff --git a/coupon/tbsdk/README.md b/coupon/tbsdk/README.md
new file mode 100644
index 0000000..02b273e
--- /dev/null
+++ b/coupon/tbsdk/README.md
@@ -0,0 +1,69 @@
+# Taobao TOP API Node SDK
+
+[淘宝开放平台](http://open.taobao.com/doc2/api_list.htm) API Node SDK
+
+## Get Started
+
+#### Rest API Demo
+```js
+ApiClient = require('../index.js').ApiClient;
+
+var client = new ApiClient({
+ 'appkey':'****',
+ 'appsecret':'************************',
+ 'REST_URL':'http://api.daily.taobao.net/router/rest'
+ });
+
+client.execute('taobao.user.get',
+ {
+ 'fields':'nick,type,sex,location',
+ 'nick':'sandbox_c_1'
+ },
+ function (error,response) {
+ if(!error)
+ console.log(response);
+ else
+ console.log(error);
+ })
+```
+
+#### Top Message Demo
+
+```js
+
+var TmcClient = require('../index.js').TmcClient;
+
+var tmcClient = new TmcClient('*******','************************','default');
+
+tmcClient.connect('ws://mc.api.daily.taobao.net/',
+ function (message,status) {
+ console.log(message);
+ });
+
+```
+
+#### Dingtalk Message Demo
+
+```js
+
+DingtalkClient = require('../index.js').DingtalkClient;
+
+var client = new DingtalkClient({
+ 'appkey':'*****',
+ 'appsecret':'**********************',
+ 'REST_URL':'https://eco.taobao.com/router/rest'
+ });
+
+client.execute('taobao.user.get',
+ {
+ 'fields':'nick,type,sex,location',
+ 'nick':'sandbox_c_1'
+ },
+ function (error,response) {
+ if(!error)
+ console.log(response);
+ else
+ console.log(error);
+ })
+
+```
\ No newline at end of file
diff --git a/coupon/tbsdk/examples/apiTest.js b/coupon/tbsdk/examples/apiTest.js
new file mode 100644
index 0000000..ff8770d
--- /dev/null
+++ b/coupon/tbsdk/examples/apiTest.js
@@ -0,0 +1,27 @@
+/**
+ * Module dependencies.
+ */
+
+ApiClient = require('../index.js').ApiClient;
+
+var client = new ApiClient({
+ 'appkey':'********',
+ 'appsecret':'*********************',
+ 'url':'http://gw.api.taobao.com/router/rest'
+});
+
+client.executeWithHeader('alipay.user.trade.search',
+ {
+ 'page_no':1,
+ 'page_size':100,
+ 'start_time':'2017-03-21 00:00:00',
+ 'end_time':'2017-03-23 23:59:59',
+ 'session':'70000100f25719047abee9303ca8ee5d2e84f19cdd4edfb48d5e917a3e9a4aca99aaf042153472040'
+ },
+ {},
+ function (error,response) {
+ if(!error)
+ console.log(response);
+ else
+ console.log(error);
+ })
diff --git a/coupon/tbsdk/examples/httpServer.js b/coupon/tbsdk/examples/httpServer.js
new file mode 100644
index 0000000..16f89e7
--- /dev/null
+++ b/coupon/tbsdk/examples/httpServer.js
@@ -0,0 +1,14 @@
+var http = require('http')
+var spiUtil = require('../lib/spiUtil');
+
+http.createServer(function (request, response) {
+ var body = [];
+ console.log(request.headers) ;
+ request.on('data', function (chunk) {
+ body.push(chunk);
+ }) ;
+ request.on('end', function () {
+ response.write(""+spiUtil.checkSignForSpi(request.url,body,request.headers,'********************'));
+ response.end();
+ });
+}).listen(8888) ;
\ No newline at end of file
diff --git a/coupon/tbsdk/examples/tmcTest.js b/coupon/tbsdk/examples/tmcTest.js
new file mode 100644
index 0000000..26d4bb1
--- /dev/null
+++ b/coupon/tbsdk/examples/tmcTest.js
@@ -0,0 +1,8 @@
+var TmcClient = require('../index.js').TmcClient;
+
+var tmcClient = new TmcClient('*****','************************','default');
+
+tmcClient.connect('ws://mc.api.daily.taobao.net/',
+ function (message,status) {
+ console.log(message);
+ });
\ No newline at end of file
diff --git a/coupon/tbsdk/index.js b/coupon/tbsdk/index.js
new file mode 100644
index 0000000..705f4cc
--- /dev/null
+++ b/coupon/tbsdk/index.js
@@ -0,0 +1,11 @@
+'use strict';
+
+var apiClient = require('./lib/api/topClient.js').TopClient;
+var dingtalkClient = require('./lib/api/dingtalkClient.js').DingTalkClient;
+var tmcClient = require('./lib/tmc/tmcClient.js').TmcClient;
+
+module.exports = {
+ ApiClient: apiClient,
+ TmcClient: tmcClient,
+ DingTalkClient: dingtalkClient
+};
diff --git a/coupon/tbsdk/lib/api/dingtalkClient.js b/coupon/tbsdk/lib/api/dingtalkClient.js
new file mode 100644
index 0000000..5d1ebb6
--- /dev/null
+++ b/coupon/tbsdk/lib/api/dingtalkClient.js
@@ -0,0 +1,149 @@
+var util = require('../topUtil.js');
+var RestClient = require('./network.js')
+var Stream = require('stream')
+
+/**
+ * Dingtalk API Client.
+ *
+ * @param {Object} options.
+ * @constructor
+ */
+
+function DingtalkClient(options) {
+ if (!(this instanceof DingtalkClient)) {
+ return new DingtalkClient(options);
+ }
+ options = options || {};
+ this.url = options.url || 'https://eco.taobao.com/router/rest';
+}
+
+/**
+ * Invoke an api by method name.
+ *
+ * @param {String} method, method name
+ * @param {Object} params
+ * @param {Array} reponseNames, e.g. ['tmall_selected_items_search_response', 'tem_list', 'selected_item']
+ * @param {Object} defaultResponse
+ * @param {Function(err, response)} callback
+ */
+DingtalkClient.prototype.invoke = function (type,method, params,reponseNames, callback) {
+ params.method = method;
+ this.request(type,params,function (err, result) {
+ if (err) {
+ return callback(err);
+ }
+ var response = result;
+ if (reponseNames && reponseNames.length > 0) {
+ for (var i = 0; i < reponseNames.length; i++) {
+ var name = reponseNames[i];
+ response = response[name];
+ if (response === undefined) {
+ break;
+ }
+ }
+ }
+ callback(null, response);
+ });
+};
+
+/**
+ * Request API.
+ *
+ * @param {Object} params
+ * @param {String} [type='GET']
+ * @param {Function(err, result)} callback
+ * @public
+ */
+DingtalkClient.prototype.request = function (type,params,callback) {
+ var err = util.checkRequired(params, 'method');
+ if (err) {
+ return callback(err);
+ }
+ var args = {
+ timestamp: this.timestamp(),
+ format: 'json',
+ v: '2.0',
+ sign_method: 'md5'
+ };
+
+ var request = null;
+ if(type == 'get'){
+ request = RestClient.get(this.url);
+ }else{
+ request = RestClient.post(this.url);
+ }
+
+ for (var key in params) {
+ if(typeof params[key] === 'object' && Buffer.isBuffer(params[key])){
+ request.attach(key,params[key],{knownLength:params[key].length,filename:key})
+ } else if(typeof params[key] === 'object' && Stream.Readable(params[key]) && !util.is(params[key]).a(String)){
+ request.attach(key, params[key]);
+ } else if(typeof params[key] === 'object'){
+ args[key] = JSON.stringify(params[key]);
+ } else{
+ args[key] = params[key];
+ }
+ }
+
+ args.sign = this.sign(args);
+ for(var key in args){
+ request.field(key, args[key]);
+ }
+
+ request.end(function(response){
+ if(response.statusCode == 200){
+ var data = response.body;
+ var errRes = data && data.error_response;
+ if (errRes) {
+ callback(errRes, data);
+ }else{
+ callback(err, data);
+ }
+ }else{
+ err = new Error('NetWork-Error');
+ err.name = 'NetWork-Error';
+ err.code = 15;
+ err.sub_code = response.statusCode;
+ callback(err, null);
+ }
+ })
+};
+
+/**
+ * Get now timestamp with 'yyyy-MM-dd HH:mm:ss' format.
+ * @return {String}
+ */
+DingtalkClient.prototype.timestamp = function () {
+ return util.YYYYMMDDHHmmss();
+};
+
+/**
+ * Sign API request.
+ * see http://open.taobao.com/doc/detail.htm?id=111#s6
+ *
+ * @param {Object} params
+ * @return {String} sign string
+ */
+DingtalkClient.prototype.sign = function (params) {
+ var sorted = Object.keys(params).sort();
+ var basestring = this.appsecret;
+ for (var i = 0, l = sorted.length; i < l; i++) {
+ var k = sorted[i];
+ basestring += k + params[k];
+ }
+ basestring += this.appsecret;
+ return util.md5(basestring).toUpperCase();
+};
+
+/**
+ * execute top api
+ */
+DingtalkClient.prototype.execute = function (apiname,params,callback) {
+ this.invoke('post',apiname, params, [util.getApiResponseName(apiname)], callback);
+};
+
+DingtalkClient.prototype.get = function (apiname,params,callback) {
+ this.invoke('get',apiname, params, [util.getApiResponseName(apiname)], callback);
+};
+
+exports.DingtalkClient = DingtalkClient;
diff --git a/coupon/tbsdk/lib/api/network.js b/coupon/tbsdk/lib/api/network.js
new file mode 100644
index 0000000..e43add1
--- /dev/null
+++ b/coupon/tbsdk/lib/api/network.js
@@ -0,0 +1,641 @@
+var StringDecoder = require('string_decoder').StringDecoder
+var FormData = require('form-data')
+var Stream = require('stream')
+var mime = require('mime')
+var path = require('path')
+var URL = require('url')
+var fs = require('fs')
+
+/**
+ * Define form mime type
+ */
+// mime.define({
+// 'application/x-www-form-urlencoded': ['form', 'urlencoded', 'form-data']
+// })
+
+/**
+ * Initialize our Rest Container
+ */
+var RestClient = function (method, uri, headers, body, callback) {
+ var restClient = function (uri, headers, body, callback) {
+ var $this = {
+ /**
+ * Stream Multipart form-data request
+ *
+ * @type {Boolean}
+ */
+ _stream: false,
+
+ /**
+ * Container to hold multipart form data for processing upon request.
+ *
+ * @type {Array}
+ * @private
+ */
+ _multipart: [],
+
+ /**
+ * Container to hold form data for processing upon request.
+ *
+ * @type {Array}
+ * @private
+ */
+ _form: [],
+
+ /**
+ * Request option container for details about the request.
+ *
+ * @type {Object}
+ */
+ options: {
+ /**
+ * Url obtained from request method arguments.
+ *
+ * @type {String}
+ */
+ url: uri,
+
+ /**
+ * Method obtained from request method arguments.
+ *
+ * @type {String}
+ */
+ method: method,
+
+ /**
+ * List of headers with case-sensitive fields.
+ *
+ * @type {Object}
+ */
+ headers: {}
+ },
+
+ hasHeader: function (name) {
+ var headers
+ var lowercaseHeaders
+
+ name = name.toLowerCase()
+ headers = Object.keys($this.options.headers)
+ lowercaseHeaders = headers.map(function (header) {
+ return header.toLowerCase()
+ })
+
+ for (var i = 0; i < lowercaseHeaders.length; i++) {
+ if (lowercaseHeaders[i] === name) {
+ return headers[i]
+ }
+ }
+
+ return false
+ },
+
+ field: function (name, value, options) {
+ return handleField(name, value, options)
+ },
+
+ attach: function (name, path, options) {
+ options = options || {}
+ options.attachment = true
+ return handleField(name, path, options)
+ },
+
+ rawField: function (name, value, options) {
+ $this._multipart.push({
+ name: name,
+ value: value,
+ options: options,
+ attachment: options.attachment || false
+ })
+ },
+
+ header: function (field, value) {
+ if (is(field).a(Object)) {
+ for (var key in field) {
+ if (field.hasOwnProperty(key)) {
+ $this.header(key, field[key])
+ }
+ }
+
+ return $this
+ }
+
+ var existingHeaderName = $this.hasHeader(field)
+ $this.options.headers[existingHeaderName || field] = value
+
+ return $this
+ },
+
+ type: function (type) {
+ $this.header('Content-Type', does(type).contain('/')
+ ? type
+ : mime.lookup(type))
+ return $this
+ },
+
+ send: function (data) {
+ var type = $this.options.headers[$this.hasHeader('content-type')]
+
+ if ((is(data).a(Object) || is(data).a(Array)) && !Buffer.isBuffer(data)) {
+ if (!type) {
+ $this.type('form')
+ type = $this.options.headers[$this.hasHeader('content-type')]
+ $this.options.body = RestClient.serializers.form(data)
+ } else if (~type.indexOf('json')) {
+ $this.options.json = true
+
+ if ($this.options.body && is($this.options.body).a(Object)) {
+ for (var key in data) {
+ if (data.hasOwnProperty(key)) {
+ $this.options.body[key] = data[key]
+ }
+ }
+ } else {
+ $this.options.body = data
+ }
+ } else {
+ $this.options.body = RestClient.Request.serialize(data, type)
+ }
+ } else if (is(data).a(String)) {
+ if (!type) {
+ $this.type('form')
+ type = $this.options.headers[$this.hasHeader('content-type')]
+ }
+
+ if (type === 'application/x-www-form-urlencoded') {
+ $this.options.body = $this.options.body
+ ? $this.options.body + '&' + data
+ : data
+ } else {
+ $this.options.body = ($this.options.body || '') + data
+ }
+ } else {
+ $this.options.body = data
+ }
+
+ return $this
+ },
+
+ end: function (callback) {
+ var Request
+ var header
+ var parts
+ var form
+
+ function handleRequestResponse (error, response, body) {
+ var result = {}
+ // Handle pure error
+ if (error && !response) {
+ result.error = error
+
+ if (callback) {
+ callback(result)
+ }
+
+ return
+ }
+
+ if (!response) {
+ console.log('This is odd, report this action / request to: http://github.com/mashape/RestClient-nodejs')
+
+ result.error = {
+ message: 'No response found.'
+ }
+
+ if (callback) {
+ callback(result)
+ }
+
+ return
+ }
+
+ // Create response reference
+ result = response
+
+ body = body || response.body
+ result.raw_body = body
+ result.headers = response.headers
+
+ if (body) {
+ type = RestClient.type(result.headers['content-type'], true)
+ if (type) data = RestClient.Response.parse(body, type)
+ else data = body
+ }
+ result.body = data
+
+ ;(callback) && callback(result)
+ }
+
+ function handleGZIPResponse (response) {
+ if (/^(deflate|gzip)$/.test(response.headers['content-encoding'])) {
+ var unzip = zlib.createUnzip()
+ var stream = new Stream()
+ var _on = response.on
+ var decoder
+
+ // Keeping node happy
+ stream.req = response.req
+
+ // Make sure we emit prior to processing
+ unzip.on('error', function (error) {
+ // Catch the parser error when there is no content
+ if (error.errno === zlib.Z_BUF_ERROR || error.errno === zlib.Z_DATA_ERROR) {
+ stream.emit('end')
+ return
+ }
+
+ stream.emit('error', error)
+ })
+
+ // Start the processing
+ response.pipe(unzip)
+
+ // Ensure encoding is captured
+ response.setEncoding = function (type) {
+ decoder = new StringDecoder(type)
+ }
+
+ // Capture decompression and decode with captured encoding
+ unzip.on('data', function (buffer) {
+ if (!decoder) return stream.emit('data', buffer)
+ var string = decoder.write(buffer)
+ if (string.length) stream.emit('data', string)
+ })
+
+ // Emit yoself
+ unzip.on('end', function () {
+ stream.emit('end')
+ })
+
+ response.on = function (type, next) {
+ if (type === 'data' || type === 'end') {
+ stream.on(type, next)
+ } else if (type === 'error') {
+ _on.call(response, type, next)
+ } else {
+ _on.call(response, type, next)
+ }
+ }
+ }
+ }
+
+ function handleFormData (form) {
+ for (var i = 0; i < $this._multipart.length; i++) {
+ var item = $this._multipart[i]
+
+ if (item.attachment && is(item.value).a(String)) {
+ if (does(item.value).contain('http://') || does(item.value).contain('https://')) {
+ item.value = RestClient.request(item.value)
+ } else {
+ item.value = fs.createReadStream(path.resolve(item.value))
+ }
+ }
+ form.append(item.name, item.value, item.options)
+ }
+
+ return form
+ }
+
+ if ($this._multipart.length && !$this._stream && $this.options.method != 'get') {
+ header = $this.options.headers[$this.hasHeader('content-type')]
+ parts = URL.parse($this.options.url)
+ form = new FormData()
+
+ if (header) {
+ $this.options.headers['content-type'] = header.split(';')[0] + '; boundary=' + form.getBoundary()
+ } else {
+ $this.options.headers['content-type'] = 'multipart/form-data; boundary=' + form.getBoundary()
+ }
+
+ return handleFormData(form).submit({
+ protocol: parts.protocol,
+ port: parts.port,
+ host: parts.hostname,
+ path: parts.path,
+ method: $this.options.method,
+ headers: $this.options.headers
+ }, function (error, response) {
+ var decoder = new StringDecoder('utf8')
+
+ if (error) {
+ return handleRequestResponse(error, response)
+ }
+
+ if (!response.body) {
+ response.body = ''
+ }
+
+ // Node 10+
+ response.resume()
+
+ // GZIP, Feel me?
+ handleGZIPResponse(response)
+
+ // Fallback
+ response.on('data', function (chunk) {
+ if (typeof chunk === 'string') response.body += chunk
+ else response.body += decoder.write(chunk)
+ })
+
+ // After all, we end up here
+ response.on('end', function () {
+ return handleRequestResponse(error, response)
+ })
+ })
+ }
+
+ Request = RestClient.request($this.options, handleRequestResponse)
+ Request.on('response', handleGZIPResponse)
+
+ if ($this._multipart.length && $this._stream) {
+ handleFormData(Request.form())
+ }
+
+ return Request
+ }
+ }
+
+ /**
+ * Alias for _.header_
+ * @type {Function}
+ */
+ $this.headers = $this.header
+
+ /**
+ * Alias for _.header_
+ *
+ * @type {Function}
+ */
+ $this.set = $this.header
+
+ /**
+ * Alias for _.end_
+ *
+ * @type {Function}
+ */
+ $this.complete = $this.end
+
+ /**
+ * Aliases for _.end_
+ *
+ * @type {Object}
+ */
+
+ $this.as = {
+ json: $this.end,
+ binary: $this.end,
+ string: $this.end
+ }
+
+ /**
+ * Handles Multipart Field Processing
+ *
+ * @param {String} name
+ * @param {Mixed} value
+ * @param {Object} options
+ */
+ function handleField (name, value, options) {
+ var serialized
+ var length
+ var key
+ var i
+
+ options = options || { attachment: false }
+
+ if (is(name).a(Object)) {
+ for (key in name) {
+ if (name.hasOwnProperty(key)) {
+ handleField(key, name[key], options)
+ }
+ }
+ } else {
+ if (is(value).a(Array)) {
+ for (i = 0, length = value.length; i < length; i++) {
+ serialized = handleFieldValue(value[i])
+ if (serialized) {
+ $this.rawField(name, serialized, options)
+ }
+ }
+ } else if (value != null) {
+ $this.rawField(name, handleFieldValue(value), options)
+ }
+ }
+
+ return $this
+ }
+
+ /**
+ * Handles Multipart Value Processing
+ *
+ * @param {Mixed} value
+ */
+ function handleFieldValue (value) {
+ if (!(value instanceof Buffer || typeof value === 'string')) {
+ if (is(value).a(Object)) {
+ if (value instanceof Stream.Readable) {
+ return value
+ } else {
+ return RestClient.serializers.json(value)
+ }
+ } else {
+ return value.toString()
+ }
+ } else return value
+ }
+
+ if (headers && typeof headers === 'function') {
+ callback = headers
+ headers = null
+ } else if (body && typeof body === 'function') {
+ callback = body
+ body = null
+ }
+
+ if (headers) $this.set(headers)
+ if (body) $this.send(body)
+
+ return callback ? $this.end(callback) : $this
+ }
+
+ return uri ? restClient(uri, headers, body, callback) : restClient
+}
+
+/**
+ * Expose the underlying layer.
+ */
+RestClient.request = require('request')
+RestClient.pipe = RestClient.request.pipe
+
+
+RestClient.type = function (type, parse) {
+ if (typeof type !== 'string') return false
+ return parse ? type.split(/ *; */).shift() : (RestClient.types[type] || type)
+}
+
+
+RestClient.trim = ''.trim
+ ? function (s) { return s.trim() }
+ : function (s) { return s.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '') }
+
+RestClient.parsers = {
+ string: function (data) {
+ var obj = {}
+ var pairs = data.split('&')
+ var parts
+ var pair
+
+ for (var i = 0, len = pairs.length; i < len; ++i) {
+ pair = pairs[i]
+ parts = pair.split('=')
+ obj[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1])
+ }
+
+ return obj
+ },
+
+ json: function (data) {
+ try {
+ data = JSON.parse(data)
+ } catch (e) {}
+
+ return data
+ }
+}
+
+/**
+ * Serialization methods for different data types.
+ *
+ * @type {Object}
+ */
+RestClient.serializers = {
+ form: function (obj) {
+ return QueryString.stringify(obj)
+ },
+
+ json: function (obj) {
+ return JSON.stringify(obj)
+ }
+}
+
+/**
+ * RestClient Request Utility Methods
+ *
+ * @type {Object}
+ */
+RestClient.Request = {
+ serialize: function (string, type) {
+ var serializer = RestClient.firstMatch(type, RestClient.enum.serialize)
+ return serializer ? serializer(string) : string
+ }
+}
+
+RestClient.Response = {
+ parse: function (string, type) {
+ var parser = RestClient.firstMatch(type, RestClient.enum.parse)
+ return parser ? parser(string) : string
+ }
+}
+
+/**
+ * Enum Structures
+ *
+ * @type {Object}
+ */
+RestClient.enum = {
+ serialize: {
+ 'application/x-www-form-urlencoded': RestClient.serializers.form,
+ 'application/json': RestClient.serializers.json,
+ 'text/javascript': RestClient.serializers.json
+ },
+
+ parse: {
+ 'application/x-www-form-urlencoded': RestClient.parsers.string,
+ 'application/json': RestClient.parsers.json,
+ 'text/javascript': RestClient.parsers.json
+ },
+
+ methods: [
+ 'GET',
+ 'HEAD',
+ 'PUT',
+ 'POST',
+ 'PATCH',
+ 'DELETE',
+ 'OPTIONS'
+ ]
+}
+
+RestClient.matches = function matches (string, map) {
+ var results = []
+
+ for (var key in map) {
+ if (typeof map.length !== 'undefined') {
+ key = map[key]
+ }
+
+ if (string.indexOf(key) !== -1) {
+ results.push(map[key])
+ }
+ }
+
+ return results
+}
+
+RestClient.firstMatch = function firstMatch (string, map) {
+ return RestClient.matches(string, map)[0]
+}
+
+/**
+ * Generate sugar for request library.
+ *
+ * This allows us to mock super-agent chaining style while using request library under the hood.
+ */
+function setupMethod (method) {
+ RestClient[method] = RestClient(method)
+}
+
+for (var i = 0; i < RestClient.enum.methods.length; i++) {
+ var method = RestClient.enum.methods[i].toLowerCase()
+ setupMethod(method)
+}
+
+function is (value) {
+ return {
+ a: function (check) {
+ if (check.prototype) check = check.prototype.constructor.name
+ var type = Object.prototype.toString.call(value).slice(8, -1).toLowerCase()
+ return value != null && type === check.toLowerCase()
+ }
+ }
+}
+
+/**
+ * Simple Utility Methods for checking information about a value.
+ *
+ * @param {Mixed} value Could be anything.
+ * @return {Object}
+ */
+function does (value) {
+ var arrayIndexOf = (Array.indexOf ? function (arr, obj, from) {
+ return arr.indexOf(obj, from)
+ } : function (arr, obj, from) {
+ var l = arr.length
+ var i = from ? parseInt((1 * from) + (from < 0 ? l : 0), 10) : 0
+ i = i < 0 ? 0 : i
+ for (; i < l; i++) if (i in arr && arr[i] === obj) return i
+ return -1
+ })
+
+ return {
+ contain: function (field) {
+ if (is(value).a(String)) return value.indexOf(field) > -1
+ if (is(value).a(Object)) return value.hasOwnProperty(field)
+ if (is(value).a(Array)) return !!~arrayIndexOf(value, field)
+ return false
+ }
+ }
+}
+
+/**
+ * Expose the RestClient Container
+ */
+module.exports = exports = RestClient
diff --git a/coupon/tbsdk/lib/api/topClient.js b/coupon/tbsdk/lib/api/topClient.js
new file mode 100644
index 0000000..062220d
--- /dev/null
+++ b/coupon/tbsdk/lib/api/topClient.js
@@ -0,0 +1,164 @@
+var util = require('../topUtil.js');
+var RestClient = require('./network.js')
+var Stream = require('stream')
+
+/**
+ * TOP API Client.
+ *
+ * @param {Object} options, must set `appkey` and `appsecret`.
+ * @constructor
+ */
+
+function TopClient(options) {
+ if (!(this instanceof TopClient)) {
+ return new TopClient(options);
+ }
+ options = options || {};
+ if (!options.appkey || !options.appsecret) {
+ throw new Error('appkey or appsecret need!');
+ }
+ this.url = options.url || 'http://gw.api.taobao.com/router/rest';
+ this.appkey = options.appkey;
+ this.appsecret = options.appsecret;
+}
+
+/**
+ * Invoke an api by method name.
+ *
+ * @param {String} method, method name
+ * @param {Object} params
+ * @param {Array} reponseNames, e.g. ['tmall_selected_items_search_response', 'tem_list', 'selected_item']
+ * @param {Object} defaultResponse
+ * @param {Function(err, response)} callback
+ */
+TopClient.prototype.invoke = function (type,method, params,reponseNames,httpHeaders,callback) {
+ params.method = method;
+ this.request(type,params,httpHeaders,function (err, result) {
+ if (err) {
+ return callback(err);
+ }
+ var response = result;
+ if (reponseNames && reponseNames.length > 0) {
+ for (var i = 0; i < reponseNames.length; i++) {
+ var name = reponseNames[i];
+ response = response[name];
+ if (response === undefined) {
+ break;
+ }
+ }
+ }
+ callback(null, response);
+ });
+};
+
+/**
+ * Request API.
+ *
+ * @param {Object} params
+ * @param {String} [type='GET']
+ * @param {Function(err, result)} callback
+ * @public
+ */
+TopClient.prototype.request = function (type,params,httpHeaders,callback) {
+ var err = util.checkRequired(params, 'method');
+ if (err) {
+ return callback(err);
+ }
+ var args = {
+ timestamp: this.timestamp(),
+ format: 'json',
+ app_key: this.appkey,
+ v: '2.0',
+ sign_method: 'md5'
+ };
+
+ var request = null;
+ if(type == 'get'){
+ request = RestClient.get(this.url);
+ }else{
+ request = RestClient.post(this.url);
+ }
+
+ for (var key in params) {
+ if(typeof params[key] === 'object' && Buffer.isBuffer(params[key])){
+ request.attach(key,params[key],{knownLength:params[key].length,filename:key})
+ } else if(typeof params[key] === 'object' && Stream.Readable(params[key]) && !util.is(params[key]).a(String)){
+ request.attach(key, params[key]);
+ } else if(typeof params[key] === 'object'){
+ args[key] = JSON.stringify(params[key]);
+ } else{
+ args[key] = params[key];
+ }
+ }
+
+ args.sign = this.sign(args);
+
+ for(var key in httpHeaders) {
+ request.header(key,httpHeaders[key]);
+ }
+
+ for(var key in args){
+ request.field(key, args[key]);
+ }
+
+ request.end(function(response){
+ if(response.statusCode == 200){
+ var data = response.body;
+ var errRes = data && data.error_response;
+ if (errRes) {
+ callback(errRes, data);
+ }else{
+ callback(err, data);
+ }
+ }else{
+ err = new Error('NetWork-Error');
+ err.name = 'NetWork-Error';
+ err.code = 15;
+ err.sub_code = response.statusCode;
+ callback(err, null);
+ }
+ })
+};
+
+/**
+ * Get now timestamp with 'yyyy-MM-dd HH:mm:ss' format.
+ * @return {String}
+ */
+TopClient.prototype.timestamp = function () {
+ return util.YYYYMMDDHHmmss();
+};
+
+/**
+ * Sign API request.
+ * see http://open.taobao.com/doc/detail.htm?id=111#s6
+ *
+ * @param {Object} params
+ * @return {String} sign string
+ */
+TopClient.prototype.sign = function (params) {
+ var sorted = Object.keys(params).sort();
+ var basestring = this.appsecret;
+ for (var i = 0, l = sorted.length; i < l; i++) {
+ var k = sorted[i];
+ basestring += k + params[k];
+ }
+ basestring += this.appsecret;
+ return util.md5(basestring).toUpperCase();
+};
+
+/**
+ * execute top api
+ */
+TopClient.prototype.execute = function (apiname,params,callback) {
+ this.invoke('post',apiname, params, [util.getApiResponseName(apiname)], [], callback);
+};
+
+TopClient.prototype.executeWithHeader = function (apiname,params,httpHeaders,callback) {
+ this.invoke('post',apiname, params, [util.getApiResponseName(apiname)], httpHeaders || [], callback);
+};
+
+TopClient.prototype.get = function (apiname,params,callback) {
+ this.invoke('get',apiname, params, [util.getApiResponseName(apiname)], [], callback);
+};
+
+exports.TopClient = TopClient;
diff --git a/coupon/tbsdk/lib/spiUtil.js b/coupon/tbsdk/lib/spiUtil.js
new file mode 100644
index 0000000..91bde06
--- /dev/null
+++ b/coupon/tbsdk/lib/spiUtil.js
@@ -0,0 +1,155 @@
+var util = require('./topUtil.js');
+var iconv = require('iconv-lite');
+var URL = require('url');
+var urlencode = require('urlencode');
+
+var ipFileds = ["X-Real-IP", "X-Forwarded-For", "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR"];
+
+String.prototype.contains = function(target){
+ return this.indexOf(target) > -1;
+}
+
+/**
+ * 校验SPI请求签名,不支持带上传文件的HTTP请求。
+ *
+ * @param bizParams 业务参数
+ * @param httpHeaders http头部信息
+ * @param secret APP密钥
+ * @param charset 目标编码
+ * @return boolean
+ */
+exports.checkSignForSpi = function checkSignForSpi(url,body,httpHeaders,secret) {
+ var ctype = httpHeaders['content-type'];
+ if(!ctype){
+ ctype = httpHeaders['Content-Type'];
+ }
+ if(!ctype){
+ return false;
+ }
+
+ var charset = this.getResponseCharset(ctype);
+ var urlParams = URL.parse(url).query.split("&");
+ var bizParams = buildBizParams(urlParams);
+ return checkSignInternal(bizParams,body,httpHeaders,secret,charset);
+}
+
+function buildBizParams(urlParams){
+ var bizParams = {};
+ for(var i =0; i < urlParams.length; i++){
+ var params = urlParams[i].split("=");
+ bizParams[params[0]] = params[1];
+ }
+ return bizParams;
+}
+
+/**
+ * 检查发起SPI请求的来源IP是否是TOP机房的出口IP。
+ *
+ * @param request HTTP请求
+ * @param topIpList TOP网关IP出口地址段列表,通过taobao.top.ipout.get获得
+ *
+ * @return boolean true表达IP来源合法,false代表IP来源不合法
+ */
+exports.checkRemoteIp = function checkRemoteIp(httpHeaders,topIpList){
+ var ip = null;
+ for(var i = 0; i < ipFileds.length; i++){
+ var realIp = httpHeaders[ipFileds[i]];
+ if(realIp && 'unknown' != realIp.toLowerCase()){
+ ip = realIp;
+ break;
+ }
+ }
+
+ if(ip){
+ for(var i = 0; i < topIpList.length; i++) {
+ if(ip == topIpList[i]){
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+/**
+ * 检查SPI请求到达服务器端是否已经超过指定的分钟数,如果超过则拒绝请求。
+ *
+ * @return boolean true代表不超过,false代表超过。
+ */
+exports.checkTimestamp = function checkTimestamp(bizParams,minutes){
+ var timestamp = bizParams['timestamp'];
+ if(timestamp){
+ var remove = new Date(timestamp).getTime();
+ var local = new Date().getTime();
+ return (local - remove) <= minutes * 60 * 1000;
+ }
+ return false;
+}
+
+function arrayConcat(bizParams,signHttpParams){
+ if(signHttpParams){
+ for(var i=0; i < signHttpParams.length; i++){
+ bizParams[signHttpParams[i].key] = signHttpParams[i].value;
+ }
+ }
+}
+
+function checkSignInternal(bizParams,body,httpHeaders,secret,charset){
+ var remoteSign = bizParams['sign'];
+ arrayConcat(bizParams,getHeaderMap(httpHeaders));
+ var sorted = Object.keys(bizParams).sort();
+ var bastString = secret;
+ var localSign ;
+ for (var i = 0, l = sorted.length; i < l; i++) {
+ var k = sorted[i];
+ var value = bizParams[k];
+ if(k == 'sign'){
+ continue;
+ }
+ value = urlencode.decode(bizParams[k],charset);
+
+ if(k == 'timestamp'){
+ value = value.replace('+',' ');
+ }
+ k = iconv.encode(k,charset);
+ bastString += k;
+ bastString += value;
+ }
+ if(body){
+ bastString += body;
+ }
+
+ bastString += secret;
+ var buffer = iconv.encode(bastString,charset);
+ localSign = util.md5(buffer).toUpperCase();
+ return localSign == remoteSign;
+}
+
+function getHeaderMap(httpHeaders){
+ var resultMap = {};
+ var signList = httpHeaders['top-sign-list'];
+ if(signList){
+ var targetKeys = signList.split(",");
+ targetKeys.forEach(function(target){
+ resultMap[target] = httpHeaders[target];
+ })
+ }
+ return resultMap;
+}
+
+exports.getResponseCharset = function getResponseCharset(ctype){
+ var charset = 'UTF-8';
+ if(ctype){
+ var params = ctype.split(";");
+ for(var i = 0; i < params.length; i++){
+ var param = params[i].trim();
+ if(param.startsWith('charset')){
+ var pair = param.split("=");
+ charset = pair[1].trim().toUpperCase();
+ }
+ }
+ }
+ if(charset && charset.toLowerCase().startsWith('GB')){
+ charset = "GBK";
+ }
+ return charset;
+}
diff --git a/coupon/tbsdk/lib/tmc/common.js b/coupon/tbsdk/lib/tmc/common.js
new file mode 100644
index 0000000..2ddc83f
--- /dev/null
+++ b/coupon/tbsdk/lib/tmc/common.js
@@ -0,0 +1,38 @@
+var Common = function(){
+}
+
+Common.enum = {
+ MessageType:{
+ CONNECT: 0,
+ CONNECTACK: 1,
+ SEND: 2,
+ SENDACK:3
+ },
+ HeaderType : {
+ EndOfHeaders : 0,
+ Custom: 1,
+ StatusCode : 2,
+ StatusPhrase: 3,
+ Flag : 4,
+ Token : 5
+ },
+
+ ValueFormat : {
+ Void : 0,
+ CountedString : 1,
+ Byte : 2,
+ Int16 : 3,
+ Int32 : 4,
+ Int64 : 5,
+ Date : 6,
+ ByteArray : 7
+ },
+ MessageKind :{
+ None: 0,
+ PullRequest : 1,
+ Confirm : 2,
+ Data : 3
+ }
+}
+
+exports.Common = Common;
\ No newline at end of file
diff --git a/coupon/tbsdk/lib/tmc/tmcClient.js b/coupon/tbsdk/lib/tmc/tmcClient.js
new file mode 100644
index 0000000..8dfc318
--- /dev/null
+++ b/coupon/tbsdk/lib/tmc/tmcClient.js
@@ -0,0 +1,153 @@
+var WebSocket = require('ws');
+var Common = require('./common.js').Common;
+var TmcCodec = require('./tmcCodec.js').TmcCodec;
+var util = require('../topUtil.js');
+
+var codec = new TmcCodec();
+var client ;
+var TmcClient = function TmcClient(appkey,appsecret,groupName) {
+ this._appkey = appkey;
+ this._appsecret = appsecret;
+ this._groupName = groupName;
+ this._uri = 'ws://mc.api.taobao.com/';
+ this._ws = null;
+ this.isReconing = false;
+ this._callback = null;
+ this._interval = null;
+ client = this;
+}
+
+
+TmcClient.prototype.createSign = function(timestamp){
+ var basestring = this._appsecret;
+ basestring += 'app_key' + this._appkey;
+ basestring += 'group_name' + this._groupName;
+ basestring += 'timestamp' + timestamp;
+ basestring += this._appsecret;
+ return util.md5(basestring).toUpperCase();
+}
+
+TmcClient.prototype.createConnectMessage = function() {
+ var msg = {};
+ msg.messageType = Common.enum.MessageType.CONNECT;
+ var timestamp = Date.now();
+ var content = {
+ 'app_key':this._appkey,
+ 'group_name':this._groupName,
+ 'timestamp':timestamp+'',
+ 'sign':this.createSign(timestamp),
+ 'sdk':'NodeJS-1.2.0',
+ 'intranet_ip':util.getLocalIPAdress()
+ };
+ msg.content = content;
+ var buffer = codec.writeMessage(msg);
+ return buffer;
+}
+
+TmcClient.prototype.createPullMessage = function() {
+ var msg = {};
+ msg.protocolVersion = 2;
+ msg.messageType = Common.enum.MessageType.SEND;
+ var content = {
+ '__kind':Common.enum.MessageKind.PullRequest
+ };
+ msg.token = client._token;
+ msg.content = content;
+ var buffer = codec.writeMessage(msg);
+ return buffer;
+}
+
+TmcClient.prototype.createConfirmMessage = function(id) {
+ var msg = {};
+ msg.protocolVersion = 2;
+ msg.messageType = Common.enum.MessageType.SEND;
+ var content = {
+ '__kind':Common.enum.MessageKind.Confirm,
+ 'id':id
+ };
+ msg.token = client._token;
+ msg.content = content;
+ var buffer = codec.writeMessage(msg);
+ return buffer;
+}
+
+TmcClient.prototype.autoPull = function () {
+ if(client._ws){
+ client._ws.send(client.createPullMessage(), { binary: true, mask: true });
+ }
+}
+
+TmcClient.prototype.reconnect = function (duration) {
+ if(this.isReconing)
+ return;
+
+ this.isReconing = true;
+ setTimeout(function timeout() {
+ client.connect(client._uri,client._callback);
+ }, duration);
+}
+
+TmcClient.prototype.connect = function(uri,callback) {
+ this._uri = uri;
+ this._callback = callback;
+
+ if(client._ws != null){
+ client._ws.close();
+ }
+
+ var ws = new WebSocket(this._uri);
+
+ ws.on('open', function open() {
+ client._ws = ws;
+ this.send(client.createConnectMessage(), { binary: true, mask: true });
+ if(!client._interval){
+ client._interval = setInterval(client.autoPull, 5000);
+ }
+ });
+
+ ws.on('message', function(data, flags) {
+ if(flags.binary){
+ var message = codec.readMessage(data);
+ if(message != null && message.messageType == Common.enum.MessageType.CONNECTACK){
+ if(message.statusCode){
+ throw new Error(message.statusPhase);
+ }else{
+ client._token = message.token;
+ console.log("top message channel connect success, token = "+message.token);
+ }
+ }else if(message != null && message.messageType == Common.enum.MessageType.SEND){
+ var status = {success:true};
+ try {
+ client._callback(message,status);
+ }catch (err) {
+ status.success = false;
+ }
+ if(status.success){
+ ws.send(client.createConfirmMessage(message.id), { binary: true, mask: true });
+ }
+ }else{
+ console.log(message);
+ }
+ }
+ });
+
+ ws.on('ping',function(data, flags) {
+ ws.pong(data,{mask: true },true);
+ });
+
+ ws.on('error',function(reason, errorCode) {
+ console.log('tmc client error,reason : '+ reason + ' code : '+ errorCode);
+ console.log('tmc client channel closed begin reconnect');
+ client._ws = null;
+ client.reconnect(15000);
+ });
+
+ ws.on('close', function close() {
+ console.log('tmc client channel closed begin reconnect');
+ client._ws = null;
+ client.reconnect(3000);
+ });
+ this.isReconing = false;
+}
+
+exports.TmcClient = TmcClient;
\ No newline at end of file
diff --git a/coupon/tbsdk/lib/tmc/tmcCodec.js b/coupon/tbsdk/lib/tmc/tmcCodec.js
new file mode 100644
index 0000000..21c228e
--- /dev/null
+++ b/coupon/tbsdk/lib/tmc/tmcCodec.js
@@ -0,0 +1,128 @@
+var Common = require('./common.js').Common;
+
+var TmcCodec = function(){
+
+}
+
+TmcCodec.prototype.writeMessage = function(message) {
+ var buffer = new Buffer(256);
+ buffer.writeUInt8(2,0);
+ buffer.writeUInt8(message.messageType,1);
+ var index = 2;
+
+ if(message.statusCode && message.statusCode > 0){
+ buffer.writeUInt16LE(Common.enum.HeaderType.StatusCode,index);
+ buffer.writeUInt32LE(message.statusCode,index+2);
+ index += 6;
+ }
+
+ if(message.flag && message.flag > 0){
+ buffer.writeUInt16LE(Common.enum.HeaderType.Flag,index);
+ buffer.writeUInt32LE(message.flag,index+2);
+ index += 6;
+ }
+
+ if(message.token){
+ buffer.writeUInt16LE(Common.enum.HeaderType.Token,index);
+ var length = Buffer.byteLength(message.token);
+ buffer.writeUInt32LE(length,index+2);
+ buffer.write(message.token,index+6,'UTF-8');
+ index = index + length + 6;
+ }
+
+ if(message.content){
+ for(var key in message.content){
+ buffer.writeUInt16LE(Common.enum.HeaderType.Custom,index);
+ var length = Buffer.byteLength(key);
+ buffer.writeUInt32LE(length,index+2);
+ buffer.write(key,index+6,'UTF-8');
+ index = index + length + 6;
+
+ length = Buffer.byteLength(message.content[key]);
+ if(length == 0){
+ buffer.writeUInt8(Common.enum.ValueFormat.Void,index);
+ }else{
+ var type = typeof message.content[key];
+ if(key == '__kind'){
+ buffer.writeUInt8(Common.enum.ValueFormat.Byte,index);
+ buffer.writeUInt8(message.content[key],index+1);
+ index += 2;
+ } else if(type == 'number'){
+ buffer.writeUInt8(Common.enum.ValueFormat.Int64,index);
+ const big = ~~(message.content[key] / (0xFFFFFFFF + 1));
+ const low = (message.content[key] % (0xFFFFFFFF + 1));
+ buffer.writeUInt32LE(low,index + 1);
+ buffer.writeUInt32LE(big,index + 5);
+ index += 9;
+ } else{
+ buffer.writeUInt8(Common.enum.ValueFormat.CountedString,index);
+ buffer.writeUInt32LE(length,index+1);
+ buffer.write(message.content[key],index+5,'UTF-8');
+ index = index + length + 5;
+ }
+ }
+ }
+ }
+ buffer.writeUInt16LE(Common.enum.HeaderType.EndOfHeaders,index);
+ return buffer.slice(0,index+2);
+}
+
+TmcCodec.prototype.readMessage = function(buffer) {
+ var message = {};
+ message.protocolVersion = buffer.readUInt8(0);
+ message.messageType = buffer.readUInt8(1);
+ try{
+ var headerType = buffer.readUInt16LE(2);
+ var index = 4;
+ while(headerType != Common.enum.HeaderType.EndOfHeaders){
+ if(headerType === Common.enum.HeaderType.StatusCode){
+ message.statusCode = buffer.readUInt32LE(index);
+ index += 4;
+ } else if(headerType === Common.enum.HeaderType.StatusPhrase){
+ var length = buffer.readUInt32LE(index);
+ message.statusPhase = buffer.toString('UTF-8',index+4,index+length+4);
+ index = index + length + 4;
+ } else if(headerType === Common.enum.HeaderType.Flag){
+ message.flag = buffer.readUInt32LE(index);
+ index += 4;
+ } else if(headerType === Common.enum.HeaderType.Token){
+ var length = buffer.readUInt32LE(index);
+ message.token = buffer.toString('UTF-8',index+4,index+length+4);
+ index = index + length + 4;
+ } else if(headerType === Common.enum.HeaderType.Custom){
+ var length = buffer.readUInt32LE(index);
+ var key = buffer.toString('UTF-8',index+4,index+length+4);
+ index = index + length + 4;
+
+ var format = buffer.readUInt8(index);
+ index += 1;
+ if(format == Common.enum.ValueFormat.Int64 || format == Common.enum.ValueFormat.Date){
+ message[key] = buffer.readUInt32LE(index) + buffer.readUInt32LE(index+4) * 4294967296;
+ index += 8;
+ }else if(format == Common.enum.ValueFormat.CountedString){
+ length = buffer.readUInt32LE(index);
+ message[key] = buffer.toString('UTF-8',index+4,index+length+4);
+ index = index + length + 4;
+ }else if(format == Common.enum.ValueFormat.Byte){
+ message[key] = buffer.readUInt8(index);
+ index += 1;
+ }else if(format == Common.enum.ValueFormat.Int32){
+ message[key] = buffer.readUInt32LE(index);
+ index += 4;
+ }else if(format == Common.enum.ValueFormat.Int16){
+ message[key] = buffer.readUInt16LE(index);
+ index += 2;
+ }
+ }
+ headerType = buffer.readUInt16LE(index);
+ index += 2;
+ }
+ }catch (err) {
+ console.log(err);
+ return null;
+ }
+ return message;
+}
+
+exports.TmcCodec = TmcCodec;
+
diff --git a/coupon/tbsdk/lib/topUtil.js b/coupon/tbsdk/lib/topUtil.js
new file mode 100644
index 0000000..d127cca
--- /dev/null
+++ b/coupon/tbsdk/lib/topUtil.js
@@ -0,0 +1,125 @@
+var crypto = require('crypto');
+var util = require('util');
+var stream = require('stream');
+
+/**
+ * hash
+ *
+ * @param {String} method hash method, e.g.: 'md5', 'sha1'
+ * @param {String|Buffer} s
+ * @param {String} [format] output string format, could be 'hex' or 'base64'. default is 'hex'.
+ * @return {String} md5 hash string
+ * @public
+ */
+exports.hash = function hash(method, s, format) {
+ var sum = crypto.createHash(method);
+ var isBuffer = Buffer.isBuffer(s);
+ if (!isBuffer && typeof s === 'object') {
+ s = JSON.stringify(sortObject(s));
+ }
+ sum.update(s, isBuffer ? 'binary' : 'utf8');
+ return sum.digest(format || 'hex');
+};
+
+/**
+ * md5 hash
+ *
+ * @param {String|Buffer} s
+ * @param {String} [format] output string format, could be 'hex' or 'base64'. default is 'hex'.
+ * @return {String} md5 hash string
+ * @public
+ */
+exports.md5 = function md5(s, format) {
+ return exports.hash('md5', s, format);
+};
+
+exports.YYYYMMDDHHmmss = function (d, options) {
+ d = d || new Date();
+ if (!(d instanceof Date)) {
+ d = new Date(d);
+ }
+
+ var dateSep = '-';
+ var timeSep = ':';
+ if (options) {
+ if (options.dateSep) {
+ dateSep = options.dateSep;
+ }
+ if (options.timeSep) {
+ timeSep = options.timeSep;
+ }
+ }
+ var date = d.getDate();
+ if (date < 10) {
+ date = '0' + date;
+ }
+ var month = d.getMonth() + 1;
+ if (month < 10) {
+ month = '0' + month;
+ }
+ var hours = d.getHours();
+ if (hours < 10) {
+ hours = '0' + hours;
+ }
+ var mintues = d.getMinutes();
+ if (mintues < 10) {
+ mintues = '0' + mintues;
+ }
+ var seconds = d.getSeconds();
+ if (seconds < 10) {
+ seconds = '0' + seconds;
+ }
+ return d.getFullYear() + dateSep + month + dateSep + date + ' ' +
+ hours + timeSep + mintues + timeSep + seconds;
+};
+
+exports.checkRequired = function (params, keys) {
+ if (!Array.isArray(keys)) {
+ keys = [keys];
+ }
+ for (var i = 0, l = keys.length; i < l; i++) {
+ var k = keys[i];
+ if (!params.hasOwnProperty(k)) {
+ var err = new Error('`' + k + '` required');
+ err.name = "ParameterMissingError";
+ return err;
+ }
+ }
+};
+
+exports.getApiResponseName = function(apiName){
+ var reg = /\./g;
+ if(apiName.match("^taobao"))
+ apiName = apiName.substr(7);
+ return apiName.replace(reg,'_')+"_response";
+}
+
+exports.getLocalIPAdress = function (){
+ var interfaces = require('os').networkInterfaces();
+ for(var devName in interfaces){
+ var iface = interfaces[devName];
+ for(var i=0;i=0.8"
+ },
+ "publishConfig": {
+ "registry": "http://registry.npm.alibaba-inc.com"
+ }
+}
diff --git a/index.js b/index.js
index 109c45d..f602928 100644
--- a/index.js
+++ b/index.js
@@ -4,9 +4,10 @@
*/
const { WechatyBuilder } = require('wechaty');
const schedule = require('./schedule/index');
-const config = require('./config/index');
+const config = require('./config/index.js');
const untils = require('./utils/index');
const superagent = require('./superagent/index');
+const startCoupon = require('./coupon')
// 延时函数,防止检测出类似机器人行为操作
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -32,7 +33,8 @@ async function onLogin(user) {
}
// 登陆后创建定时任务
- await initDay();
+ // await initDay();
+ startCoupon(bot)
}
// 登出
diff --git a/utils/index.js b/utils/index.js
index e74030f..dcfcba7 100644
--- a/utils/index.js
+++ b/utils/index.js
@@ -44,7 +44,14 @@ function formatDate(date) {
return year + '-' + month + '-' + day + '日 ' + hour + ':' + min + ' ' + str;
}
+function wait(seconds) {
+ return new Promise(function(resolve) {
+ setTimeout(resolve, seconds * 1000)
+ })
+}
+
module.exports = {
getDay,
- formatDate
+ formatDate,
+ wait,
};