From 888091beef8edf775845aed3147919422b9db704 Mon Sep 17 00:00:00 2001 From: Denton Liu Date: Sat, 13 Aug 2022 20:42:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=B7=98=E5=AE=9D=E5=AE=A2?= =?UTF-8?q?=E4=BC=98=E6=83=A0=E5=88=86=E4=BA=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- README.md | 367 +------------- config/index.js | 21 +- coupon/index.js | 14 + coupon/taobao.js | 180 +++++++ coupon/tbsdk/.gitignore | 5 + coupon/tbsdk/README.md | 69 +++ coupon/tbsdk/examples/apiTest.js | 27 ++ coupon/tbsdk/examples/httpServer.js | 14 + coupon/tbsdk/examples/tmcTest.js | 8 + coupon/tbsdk/index.js | 11 + coupon/tbsdk/lib/api/dingtalkClient.js | 149 ++++++ coupon/tbsdk/lib/api/network.js | 641 +++++++++++++++++++++++++ coupon/tbsdk/lib/api/topClient.js | 164 +++++++ coupon/tbsdk/lib/spiUtil.js | 155 ++++++ coupon/tbsdk/lib/tmc/common.js | 38 ++ coupon/tbsdk/lib/tmc/tmcClient.js | 153 ++++++ coupon/tbsdk/lib/tmc/tmcCodec.js | 128 +++++ coupon/tbsdk/lib/topUtil.js | 125 +++++ coupon/tbsdk/package.json | 27 ++ index.js | 6 +- utils/index.js | 9 +- 22 files changed, 1943 insertions(+), 371 deletions(-) create mode 100644 coupon/index.js create mode 100644 coupon/taobao.js create mode 100644 coupon/tbsdk/.gitignore create mode 100644 coupon/tbsdk/README.md create mode 100644 coupon/tbsdk/examples/apiTest.js create mode 100644 coupon/tbsdk/examples/httpServer.js create mode 100644 coupon/tbsdk/examples/tmcTest.js create mode 100644 coupon/tbsdk/index.js create mode 100644 coupon/tbsdk/lib/api/dingtalkClient.js create mode 100644 coupon/tbsdk/lib/api/network.js create mode 100644 coupon/tbsdk/lib/api/topClient.js create mode 100644 coupon/tbsdk/lib/spiUtil.js create mode 100644 coupon/tbsdk/lib/tmc/common.js create mode 100644 coupon/tbsdk/lib/tmc/tmcClient.js create mode 100644 coupon/tbsdk/lib/tmc/tmcCodec.js create mode 100644 coupon/tbsdk/lib/topUtil.js create mode 100644 coupon/tbsdk/package.json 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 @@ -## 微信每日说 +## 微信机器人 -[![node version](https://img.shields.io/badge/node-%3E%3D16-blue.svg)](http://nodejs.cn/download/) -[![node version](https://img.shields.io/badge/wechaty-%3E%3D1.20.2-blue.svg)](https://github.com/Chatie/wechaty) -![](https://img.shields.io/badge/Window-green.svg) -![](https://img.shields.io/badge/Mac-yellow.svg) -![](https://img.shields.io/badge/Centos-blue.svg) - -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 -``` - -### 下载代码 - -![download-project](https://user-gold-cdn.xitu.io/2019/6/16/16b5fcb3ea7ee507?w=1917&h=937&f=png&s=180655) - -```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,所以会比较慢,稍等一下,出现二维码后即可拿出微信扫描 - -![](https://user-gold-cdn.xitu.io/2019/6/16/16b5fa4678361c14?w=969&h=724&f=png&s=51158) - -执行成功后可看到 - -![](https://user-gold-cdn.xitu.io/2019/6/16/16b5fa9bc1f5c76e?w=977&h=322&f=png&s=25797) - -## 效果展示 - -![](https://user-gold-cdn.xitu.io/2019/6/16/16b5fbf97805f02e?w=959&h=779&f=png&s=73686) - -
- - -
- -
- - -
- -## 常见问题处理 (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 下部署出现以下问题 - ![](http://image.bloggeng.com/14481551970095_.pic_hd.jpg) - 问题原因:[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 - - 把下载的文件放到如下图路径,并解压到当前文件夹中即可 - ![](http://image.bloggeng.com/14241551970542_.pic_hd.jpg) - - - 下载 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 防火墙;如果公司网络有限制的话也可能引起无法启动问题 - ![](http://image.bloggeng.com/WechatIMG7619.png) - -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://user-gold-cdn.xitu.io/2019/2/28/1693401c6c3e6b02?w=430&h=430&f=png&s=53609) - -赶快亲自试一试吧,相信你会挖掘出更多好玩的功能 - -关注公众号:随时获取最新消息 - -![](https://user-gold-cdn.xitu.io/2019/3/1/169381d277ba6401?w=258&h=258&f=png&s=42373) - -## 鸣谢 - -感谢[天行数据](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, };