将wechat放到项目里面
This commit is contained in:
parent
5a60702412
commit
cc9825e6a2
2
Gemfile
2
Gemfile
|
@ -6,7 +6,7 @@ unless RUBY_PLATFORM =~ /w32/
|
|||
gem 'iconv'
|
||||
end
|
||||
|
||||
gem 'wechat',git: 'https://github.com/guange2015/wechat.git'
|
||||
gem 'wechat',path: 'lib/wechat'
|
||||
gem 'grack', path:'lib/grack'
|
||||
gem 'gitlab', path: 'lib/gitlab-cli'
|
||||
gem 'rest-client'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
source "http://rubygems.org"
|
||||
source "https://rubygems.org"
|
||||
|
||||
gemspec
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
# Save as .codeclimate.yml (note leading .) in project root directory
|
||||
languages:
|
||||
Ruby: true
|
||||
|
||||
exclude_paths:
|
||||
- "lib/generators/wechat/templates/app/controllers/wechats_controller.rb"
|
|
@ -0,0 +1,17 @@
|
|||
Documentation:
|
||||
Enabled: false
|
||||
|
||||
Metrics/LineLength:
|
||||
Max: 150
|
||||
|
||||
Metrics/AbcSize:
|
||||
Max: 37
|
||||
|
||||
Metrics/ClassLength:
|
||||
Max: 150
|
||||
|
||||
Metrics/MethodLength:
|
||||
Max: 15
|
||||
|
||||
Style/NumericLiterals:
|
||||
MinDigits: 7
|
|
@ -0,0 +1,22 @@
|
|||
language: ruby
|
||||
|
||||
sudo: false
|
||||
|
||||
rvm:
|
||||
- 2.0.0
|
||||
- 2.1
|
||||
- 2.2
|
||||
- 2.3
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- rvm: 2.3
|
||||
|
||||
install:
|
||||
- "travis_retry bundle config build.nokogiri --use-system-libraries"
|
||||
- "travis_retry bundle install --retry 3"
|
||||
|
||||
script: bundle exec rake
|
||||
|
||||
git:
|
||||
depth: 10
|
|
@ -0,0 +1,121 @@
|
|||
# Changelog
|
||||
|
||||
## v0.7.1 (released at 1/11/2016)
|
||||
|
||||
* Fix after using http, upload file function break. #78
|
||||
* Add callback function after_wechat_response support. by @zfben #79
|
||||
* Should using department_id instead of departmentid at enterprise api: user_simplelist/user_list.
|
||||
|
||||
## v0.7.0 (released at 1/1/2016)
|
||||
|
||||
* Using [http](https://github.com/httprb/http) instead of rest-client for performance reason. (not support upload file yet)
|
||||
|
||||
## v0.6.9 (released at 1/6/2016)
|
||||
|
||||
* Fix token refresh bug on multi worker. #76
|
||||
* Rewrite the token relative code to add more storage support in future.
|
||||
|
||||
## v0.6.8 (released at 12/25/2015)
|
||||
|
||||
* Support Rails 5.0.0.beta1.
|
||||
* English README available
|
||||
* Fix oauth2_url calling error, fix #75
|
||||
|
||||
## v0.6.7 (released at 12/18/2015)
|
||||
|
||||
* Add timeout configuration option, close #74
|
||||
* New getuserinfo and oauth2_url to support getting FromUserName from web page.
|
||||
|
||||
## v0.6.6 (released at 12/15/2015)
|
||||
|
||||
* Add jsapi_ticket support for Enterprise Account
|
||||
* Default generated WechatsController < ActionController::Base, as many Rails application may having #authenticate_user or #set_current_user in ApplicationController, so easily affect the first time using experience.
|
||||
* New syntax `on :view, with: 'VIEW_URL'` support.
|
||||
* New command `upload_replaceparty` which combine three sub command to make uploading department easier.
|
||||
* New command `upload_replaceuser` which combine three sub command to make uploading user easier.
|
||||
|
||||
## v0.6.5 (released at 11/24/2015)
|
||||
|
||||
* Handle 48001 error if token is expire/not valid, close #71
|
||||
* ApiLoader will do config reading and initialize the api instead of spreading the logic.
|
||||
|
||||
## v0.6.4 (released at 11/16/2015)
|
||||
|
||||
* Command mode now display different command set based on enterprise/public account setting
|
||||
* Move config logic in command/wechat to ApiLoader class
|
||||
* Unsubscribe can only reply plain text 'success' #68
|
||||
* Fix 404 qrcode download problem, by @huangxiangdan #69
|
||||
|
||||
## v0.6.3 (released at 11/14/2015)
|
||||
|
||||
* Official testing and support public encrypt mode, also fix one cipher bug, many thanks to @hlltc #67
|
||||
* hlltc report public account FILE_BASE no longer needs, clean code #67
|
||||
* Media command line reflect recent Tecent json schema change. #67
|
||||
|
||||
## v0.6.2 (released at 11/05/2015)
|
||||
|
||||
* Tecent report location API changed, so change wechat gems also. #64
|
||||
|
||||
## v0.6.1 (released at 10/20/2015)
|
||||
|
||||
* Handle 40001, invalid credential, access_token is invalid or not latest hint # 57
|
||||
* Support at Rails 4.2.1 wechat can not run #58
|
||||
|
||||
## v0.6.0 (released at 10/08/2015)
|
||||
|
||||
### Scan and Batch job are BREAK CHANGE!
|
||||
|
||||
* Scan 2D barcode using new syntax `on :scan, with: 'BINDING_QR_CODE' ` instead of `on :event, with: 'BINDING_QR_CODE' ` in previous version #55
|
||||
Which will fix can not using `on :event, with: "scan" ` problem
|
||||
* Batch job using new syntax `on :batch_job, with: 'replace_user' `
|
||||
instead of previous `on :event, with: 'replace_user' `.
|
||||
* Click menu support new syntax `on :click, with: 'BOOK_LUNCH' `, but `on :event, with: 'BOOK_LUNCH' ` still supported. perfer `on :click` because it running faster and more nature expression.
|
||||
* Wechat::Responder using Hash for new :client and :batch_job event, avoid time consuming Array match responder
|
||||
* Fix refresh token not working problem under ruby 2.0.0 #54
|
||||
* New department_update, user_batchdelete, convert_to_openid API
|
||||
|
||||
## v0.5.0 (released at 9/25/2015)
|
||||
|
||||
* Only relay on activesupport on run time, so will greatly improve wechat cli startup time
|
||||
* Add rails generator support `rails g wechat:install`
|
||||
* Add batch job support for enterprise account like batch create user/department, both API, callback responder and CLI
|
||||
* Add material management API and CLI
|
||||
* Add tag API and CLI for enterprise account
|
||||
* Add QR code scene function for public account
|
||||
|
||||
## v0.4.2 (released at 9/7/2015)
|
||||
|
||||
* Fix wrong number of arguments at Wechat::Responder.on by using arity #47
|
||||
* Fix can not access wechat method after using instance level context.
|
||||
* Fix skip_verify_ssl parameter error.
|
||||
|
||||
## v0.4.1 (released at 9/6/2015)
|
||||
|
||||
* Limit news articles collection to 10, close #5
|
||||
* Resolve the conflict with gem "responders" by @seamon #45
|
||||
|
||||
## v0.4.0 (released at 9/5/2015)
|
||||
|
||||
* Enable the verify SSL for enterprise mode by default, as security is more importent than speed, but still can switch off by configure
|
||||
* Support scancode_push/scancode_waitmsg event.
|
||||
* New API method can get wechat server IP list
|
||||
* New API to query/create department/media/material
|
||||
* Fix can not read token_file in mingw bug, which introduce at #43
|
||||
|
||||
## v0.3.0 (released at 8/30/2015)
|
||||
|
||||
* New user group management API
|
||||
* Allow transfer to customer service on fallback. #42
|
||||
* Read and write access_token properly using file locking, #43
|
||||
|
||||
## v0.2.0 (released at 8/27/2015)
|
||||
|
||||
* Add wechat enterprise account support
|
||||
* Make responder execute in action context, by @lazing #15
|
||||
* jsapi_ticket support, by @feitian124 #27
|
||||
* Rename gems to wechat and ambitious to being #1 gems about development wechat. thanks Xiaoning transfer this gem name.
|
||||
* Original gem `wechat-rails` author skinnyworm trasfer to Eric-Guo as maintainer
|
||||
|
||||
## v0.1.1
|
||||
|
||||
* Initial release from [wechat-rails](https://github.com/skinnyworm/wechat-rails).
|
|
@ -0,0 +1,11 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
gemspec
|
||||
|
||||
# jquery-rails is used by the dummy application
|
||||
gem 'jquery-rails'
|
||||
gem 'rake', '~> 10.4.2'
|
||||
gem 'codeclimate-test-reporter', group: :test, require: nil
|
||||
|
||||
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
|
||||
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 skinnyworm
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,591 @@
|
|||
WeChat [![Gem Version][version-badge]][rubygems] [![Build Status][travis-badge]][travis] [![Code Climate][codeclimate-badge]][codeclimate] [![Code Coverage][codecoverage-badge]][codecoverage]
|
||||
======
|
||||
|
||||
[![Join the chat][gitter-badge]][gitter] [![Issue Stats][issue-badge]][issuestats] [![PR Stats][pr-badge]][issuestats]
|
||||
|
||||
WeChat gem 可以帮助开发者方便地在Rails环境中集成微信[公众平台](https://mp.weixin.qq.com/)和[企业平台](https://qy.weixin.qq.com)提供的服务,包括:
|
||||
|
||||
- 微信公众/企业平台[主动消息](http://qydev.weixin.qq.com/wiki/index.php?title=%E5%8F%91%E9%80%81%E6%B6%88%E6%81%AF)API(命令行和Web环境都可以使用)
|
||||
- [回调消息](http://qydev.weixin.qq.com/wiki/index.php?title=%E6%8E%A5%E6%94%B6%E6%B6%88%E6%81%AF%E4%B8%8E%E4%BA%8B%E4%BB%B6)(必须运行Web服务器)
|
||||
- [微信JS-SDK](http://qydev.weixin.qq.com/wiki/index.php?title=%E5%BE%AE%E4%BF%A1JS%E6%8E%A5%E5%8F%A3) config接口注入权限验证
|
||||
- OAuth 2.0认证机制
|
||||
|
||||
命令行工具`wechat`可以调用各种无需web环境的API。同时也提供了Rails Controller的responder DSL, 可以帮助开发者方便地在Rails应用中集成微信的消息处理,包括主动推送的和被动响应的消息。
|
||||
|
||||
如果你的App还需要集成微信OAuth2.0, 你可以考虑[omniauth-wechat-oauth2](https://github.com/skinnyworm/omniauth-wechat-oauth2), 以便和devise集成,提供完整的用户认证。
|
||||
|
||||
如果你对如何制作微信网页UI没有灵感,可以参考官方的[weui](https://github.com/weui/weui),针对Rails的Gem是[weui-rails](https://github.com/Eric-Guo/weui-rails)。
|
||||
|
||||
|
||||
## 安装
|
||||
|
||||
Using `gem install`
|
||||
|
||||
```
|
||||
gem install "wechat"
|
||||
```
|
||||
|
||||
Or add to your app's `Gemfile`:
|
||||
|
||||
```
|
||||
gem 'wechat'
|
||||
```
|
||||
|
||||
Run the following command to install it:
|
||||
|
||||
```console
|
||||
bundle install
|
||||
```
|
||||
|
||||
Run the generator:
|
||||
|
||||
```console
|
||||
rails generate wechat:install
|
||||
```
|
||||
|
||||
运行`rails g wechat:install`后会自动生成wechat.yml配置,还有wechat controller及相关路由配置到当前Rails项目。
|
||||
|
||||
|
||||
## 配置
|
||||
|
||||
#### 命令行程序的配置
|
||||
|
||||
要使用命令行程序,需要在home目录中创建一个`~/.wechat.yml`,包含以下内容。其中`access_token`是存放access_token的文件位置。
|
||||
|
||||
```
|
||||
appid: "my_appid"
|
||||
secret: "my_secret"
|
||||
access_token: "/var/tmp/wechat_access_token"
|
||||
```
|
||||
|
||||
Windows或者使用企业号,需要存放在`C:/Users/[user_name]/`下,其中corpid和corpsecret可以从企业号管理界面的设置->权限管理,通过新建任意一个管理组后获取。
|
||||
|
||||
```
|
||||
corpid: "my_appid"
|
||||
corpsecret: "my_secret"
|
||||
agentid: 1 # 企业应用的id,整型。可在应用的设置页面查看
|
||||
access_token: "C:/Users/[user_name]/wechat_access_token"
|
||||
```
|
||||
|
||||
#### Rails 全局配置
|
||||
Rails应用程序中,需要将配置文件放在`config/wechat.yml`,可以为不同environment创建不同的配置。
|
||||
|
||||
公众号配置示例:
|
||||
|
||||
```
|
||||
default: &default
|
||||
appid: "app_id"
|
||||
secret: "app_secret"
|
||||
token: "app_token"
|
||||
access_token: "/var/tmp/wechat_access_token"
|
||||
|
||||
production:
|
||||
appid: <%= ENV['WECHAT_APPID'] %>
|
||||
secret: <%= ENV['WECHAT_APP_SECRET'] %>
|
||||
token: <%= ENV['WECHAT_TOKEN'] %>
|
||||
access_token: <%= ENV['WECHAT_ACCESS_TOKEN'] %>
|
||||
|
||||
development:
|
||||
<<: *default
|
||||
|
||||
test:
|
||||
<<: *default
|
||||
```
|
||||
|
||||
公众号可选安全模式(加密模式),通过添加如下配置可开启加密模式。
|
||||
|
||||
```
|
||||
default: &default
|
||||
encrypt_mode: true
|
||||
encoding_aes_key: "my_encoding_aes_key"
|
||||
```
|
||||
|
||||
企业号配置下必须使用加密模式,其中token和encoding_aes_key可以从企业号管理界面的应用中心->某个应用->模式选择,选择回调模式后获得。
|
||||
|
||||
```
|
||||
default: &default
|
||||
corpid: "corpid"
|
||||
corpsecret: "corpsecret"
|
||||
agentid: 1
|
||||
access_token: "C:/Users/[user_name]/wechat_access_token"
|
||||
token: ""
|
||||
encoding_aes_key: ""
|
||||
jsapi_ticket: "C:/Users/[user_name]/wechat_jsapi_ticket"
|
||||
|
||||
production:
|
||||
corpid: <%= ENV['WECHAT_CORPID'] %>
|
||||
corpsecret: <%= ENV['WECHAT_CORPSECRET'] %>
|
||||
agentid: <%= ENV['WECHAT_AGENTID'] %>
|
||||
access_token: <%= ENV['WECHAT_ACCESS_TOKEN'] %>
|
||||
token: <%= ENV['WECHAT_TOKEN'] %>
|
||||
timeout: 30,
|
||||
skip_verify_ssl: true
|
||||
encoding_aes_key: <%= ENV['WECHAT_ENCODING_AES_KEY'] %>
|
||||
jsapi_ticket: <%= ENV['WECHAT_JSAPI_TICKET'] %>
|
||||
|
||||
development:
|
||||
<<: *default
|
||||
|
||||
test:
|
||||
<<: *default
|
||||
```
|
||||
|
||||
##### 配置优先级
|
||||
|
||||
注意在Rails项目根目录下运行`wechat`命令行工具会优先使用`config/wechat.yml`中的`default`配置,如果失败则使用`~\.wechat.yml`中的配置,以便于在生产环境下管理多个微信账号应用。
|
||||
|
||||
##### 配置微信服务器超时
|
||||
|
||||
微信服务器有时请求会花很长时间,如果不配置,默认为20秒,可视情况配置。
|
||||
|
||||
##### 配置跳过SSL认证
|
||||
|
||||
Wechat服务器有报道曾出现[RestClient::SSLCertificateNotVerified](http://qydev.weixin.qq.com/qa/index.php?qa=11037)错误,此时可以选择关闭SSL验证。`skip_verify_ssl: true`
|
||||
|
||||
#### 为每个Responder配置不同的appid和secret
|
||||
|
||||
在个别情况下,单个Rails应用可能需要处理来自多个账号的消息,此时可以配置多个responder controller。
|
||||
|
||||
```ruby
|
||||
class WechatFirstController < ActionController::Base
|
||||
wechat_responder appid: "app1", secret: "secret1", token: "token1", access_token: Rails.root.join("tmp/access_token1")
|
||||
|
||||
on :text, with:"help", respond: "help content"
|
||||
end
|
||||
```
|
||||
|
||||
#### jssdk 支持
|
||||
|
||||
jssdk 使用前需通过config接口注入权限验证配置, 所需参数可以通过 signature 方法获取:
|
||||
|
||||
```ruby
|
||||
WechatsController.wechat.jsapi_ticket.signature(request.original_url)
|
||||
```
|
||||
|
||||
## 关于接口权限
|
||||
|
||||
wechat gems 内部不会检查权限。但因公众号类型不同,和微信服务器端通讯时,可能会被拒绝,详细权限控制可参考[官方文档](http://mp.weixin.qq.com/wiki/7/2d301d4b757dedc333b9a9854b457b47.html)。
|
||||
|
||||
## 使用命令行
|
||||
|
||||
根据企业号和公众号配置不同,wechat提供了的命令行命令。
|
||||
|
||||
#### 公众号命令行
|
||||
|
||||
```
|
||||
$ wechat
|
||||
Wechat commands:
|
||||
wechat callbackip # 获取微信服务器IP地址
|
||||
wechat custom_image [OPENID, IMAGE_PATH] # 发送图片客服消息
|
||||
wechat custom_music [OPENID, THUMBNAIL_PATH, MUSIC_URL] # 发送音乐客服消息
|
||||
wechat custom_news [OPENID, NEWS_YAML_PATH] # 发送图文客服消息
|
||||
wechat custom_text [OPENID, TEXT_MESSAGE] # 发送文字客服消息
|
||||
wechat custom_video [OPENID, VIDEO_PATH] # 发送视频客服消息
|
||||
wechat custom_voice [OPENID, VOICE_PATH] # 发送语音客服消息
|
||||
wechat group_create [GROUP_NAME] # 创建分组
|
||||
wechat group_delete [GROUP_ID] # 删除分组
|
||||
wechat group_update [GROUP_ID, NEW_GROUP_NAME] # 修改分组名
|
||||
wechat groups # 查询所有分组
|
||||
wechat material [MEDIA_ID, PATH] # 永久媒体下载
|
||||
wechat material_add [MEDIA_TYPE, PATH] # 永久媒体上传
|
||||
wechat material_count # 获取永久素材总数
|
||||
wechat material_delete [MEDIA_ID] # 删除永久素材
|
||||
wechat material_list [TYPE, OFFSET, COUNT] # 获取永久素材列表
|
||||
wechat media [MEDIA_ID, PATH] # 媒体下载
|
||||
wechat media_create [MEDIA_TYPE, PATH] # 媒体上传
|
||||
wechat menu # 当前菜单
|
||||
wechat menu_create [MENU_YAML_PATH] # 创建菜单
|
||||
wechat menu_delete # 删除菜单
|
||||
wechat oauth2_url [REDIRECT_URI] # 生成OAuth2.0验证URL
|
||||
wechat qrcode_create_limit_scene [SCENE_ID_OR_STR] # 请求永久二维码
|
||||
wechat qrcode_create_scene [SCENE_ID, EXPIRE_SECONDS] # 请求临时二维码
|
||||
wechat qrcode_download [TICKET, QR_CODE_PIC_PATH] # 通过ticket下载二维码
|
||||
wechat template_message [OPENID, TEMPLATE_YAML_PATH] # 模板消息接口
|
||||
wechat user [OPEN_ID] # 获取用户基本信息
|
||||
wechat user_change_group [OPEN_ID, TO_GROUP_ID] # 移动用户分组
|
||||
wechat user_group [OPEN_ID] # 查询用户所在分组
|
||||
wechat user_update_remark [OPEN_ID, REMARK] # 设置备注名
|
||||
wechat users # 关注者列表
|
||||
```
|
||||
|
||||
#### 企业号命令行
|
||||
```
|
||||
$ wechat
|
||||
Wechat commands:
|
||||
wechat agent [AGENT_ID] # 获取企业号应用详情
|
||||
wechat agent_list # 获取应用概况列表
|
||||
wechat batch_job_result [JOB_ID] # 获取异步任务结果
|
||||
wechat batch_replaceparty [BATCH_PARTY_CSV_MEDIA_ID] # 全量覆盖部门
|
||||
wechat batch_replaceuser [BATCH_USER_CSV_MEDIA_ID] # 全量覆盖成员
|
||||
wechat batch_syncuser [SYNC_USER_CSV_MEDIA_ID] # 增量更新成员
|
||||
wechat callbackip # 获取微信服务器IP地址
|
||||
wechat convert_to_openid [USER_ID] # userid转换成openid
|
||||
wechat custom_image [OPENID, IMAGE_PATH] # 发送图片客服消息
|
||||
wechat custom_music [OPENID, THUMBNAIL_PATH, MUSIC_URL] # 发送音乐客服消息
|
||||
wechat custom_news [OPENID, NEWS_YAML_PATH] # 发送图文客服消息
|
||||
wechat custom_text [OPENID, TEXT_MESSAGE] # 发送文字客服消息
|
||||
wechat custom_video [OPENID, VIDEO_PATH] # 发送视频客服消息
|
||||
wechat custom_voice [OPENID, VOICE_PATH] # 发送语音客服消息
|
||||
wechat department [DEPARTMENT_ID] # 获取部门列表
|
||||
wechat department_create [NAME, PARENT_ID] # 创建部门
|
||||
wechat department_delete [DEPARTMENT_ID] # 删除部门
|
||||
wechat department_update [DEPARTMENT_ID, NAME] # 更新部门
|
||||
wechat invite_user [USER_ID] # 邀请成员关注
|
||||
wechat material [MEDIA_ID, PATH] # 永久媒体下载
|
||||
wechat material_add [MEDIA_TYPE, PATH] # 永久媒体上传
|
||||
wechat material_count # 获取永久素材总数
|
||||
wechat material_delete [MEDIA_ID] # 删除永久素材
|
||||
wechat material_list [TYPE, OFFSET, COUNT] # 获取永久素材列表
|
||||
wechat media [MEDIA_ID, PATH] # 媒体下载
|
||||
wechat media_create [MEDIA_TYPE, PATH] # 媒体上传
|
||||
wechat menu # 当前菜单
|
||||
wechat menu_create [MENU_YAML_PATH] # 创建菜单
|
||||
wechat menu_delete # 删除菜单
|
||||
wechat message_send [OPENID, TEXT_MESSAGE] # 发送文字消息
|
||||
wechat oauth2_url [REDIRECT_URI] # 生成OAuth2.0验证URL
|
||||
wechat qrcode_download [TICKET, QR_CODE_PIC_PATH] # 通过ticket下载二维码
|
||||
wechat tag [TAG_ID] # 获取标签成员
|
||||
wechat tag_add_department [TAG_ID, PARTY_IDS] # 增加标签部门
|
||||
wechat tag_add_user [TAG_ID, USER_IDS] # 增加标签成员
|
||||
wechat tag_create [TAGNAME, TAG_ID] # 创建标签
|
||||
wechat tag_del_department [TAG_ID, PARTY_IDS] # 删除标签部门
|
||||
wechat tag_del_user [TAG_ID, USER_IDS] # 删除标签成员
|
||||
wechat tag_delete [TAG_ID] # 删除标签
|
||||
wechat tag_update [TAG_ID, TAGNAME] # 更新标签名字
|
||||
wechat tags # 获取标签列表
|
||||
wechat template_message [OPENID, TEMPLATE_YAML_PATH] # 模板消息接口
|
||||
wechat upload_replaceparty [BATCH_PARTY_CSV_PATH] # 上传文件方式全量覆盖部门
|
||||
wechat upload_replaceuser [BATCH_USER_CSV_PATH] # 上传文件方式全量覆盖成员
|
||||
wechat user [OPEN_ID] # 获取用户基本信息
|
||||
wechat user_batchdelete [USER_ID_LIST] # 批量删除成员
|
||||
wechat user_delete [USER_ID] # 删除成员
|
||||
wechat user_list [DEPARTMENT_ID] # 获取部门成员详情
|
||||
wechat user_simplelist [DEPARTMENT_ID] # 获取部门成员
|
||||
wechat user_update_remark [OPEN_ID, REMARK] # 设置备注名
|
||||
```
|
||||
|
||||
### 使用场景
|
||||
以下是几种典型场景的使用方法
|
||||
|
||||
#####获取所有用户的OPENID
|
||||
|
||||
```
|
||||
$ wechat users
|
||||
|
||||
{"total"=>4, "count"=>4, "data"=>{"openid"=>["oCfEht9***********", "oCfEhtwqa***********", "oCfEht9oMCqGo***********", "oCfEht_81H5o2***********"]}, "next_openid"=>"oCfEht_81H5o2***********"}
|
||||
|
||||
```
|
||||
|
||||
#####获取用户的信息
|
||||
|
||||
```
|
||||
$ wechat user "oCfEht9***********"
|
||||
|
||||
{"subscribe"=>1, "openid"=>"oCfEht9***********", "nickname"=>"Nickname", "sex"=>1, "language"=>"zh_CN", "city"=>"徐汇", "province"=>"上海", "country"=>"中国", "headimgurl"=>"http://wx.qlogo.cn/mmopen/ajNVdqHZLLBd0SG8NjV3UpXZuiaGGPDcaKHebTKiaTyof*********/0", "subscribe_time"=>1395715239}
|
||||
|
||||
```
|
||||
|
||||
##### 获取当前菜单
|
||||
```
|
||||
$ wechat menu
|
||||
|
||||
{"menu"=>{"button"=>[{"type"=>"view", "name"=>"保护的", "url"=>"http://***/protected", "sub_button"=>[]}, {"type"=>"view", "name"=>"公开的", "url"=>"http://***", "sub_button"=>[]}]}}
|
||||
|
||||
```
|
||||
|
||||
##### 创建菜单
|
||||
创建菜单需要一个定义菜单内容的yaml文件,比如
|
||||
menu.yaml
|
||||
|
||||
```
|
||||
button:
|
||||
-
|
||||
name: "我要"
|
||||
sub_button:
|
||||
-
|
||||
type: "scancode_waitmsg"
|
||||
name: "绑定用餐二维码"
|
||||
key: "BINDING_QR_CODE"
|
||||
-
|
||||
type: "click"
|
||||
name: "预订午餐"
|
||||
key: "BOOK_LUNCH"
|
||||
-
|
||||
type: "click"
|
||||
name: "预订晚餐"
|
||||
key: "BOOK_DINNER"
|
||||
-
|
||||
name: "查询"
|
||||
sub_button:
|
||||
-
|
||||
type: "click"
|
||||
name: "进出记录"
|
||||
key: "BADGE_IN_OUT"
|
||||
-
|
||||
type: "click"
|
||||
name: "年假余额"
|
||||
key: "ANNUAL_LEAVE"
|
||||
-
|
||||
type: "view"
|
||||
name: "关于"
|
||||
url: "http://blog.cloud-mes.com/"
|
||||
```
|
||||
|
||||
然后执行命令行,需确保设置,权限管理中有对此应用的管理权限,否则会报[60011](http://qydev.weixin.qq.com/wiki/index.php?title=%E5%85%A8%E5%B1%80%E8%BF%94%E5%9B%9E%E7%A0%81%E8%AF%B4%E6%98%8E)错。
|
||||
|
||||
```
|
||||
$ wechat menu_create menu.yaml
|
||||
|
||||
```
|
||||
|
||||
##### 发送客服图文消息
|
||||
需定义一个图文消息内容的yaml文件,比如
|
||||
articles.yaml
|
||||
|
||||
```
|
||||
articles:
|
||||
-
|
||||
title: "习近平在布鲁日欧洲学院演讲"
|
||||
description: "新华网比利时布鲁日4月1日电 国家主席习近平1日在比利时布鲁日欧洲学院发表重要演讲"
|
||||
url: "http://news.sina.com.cn/c/2014-04-01/232629843387.shtml"
|
||||
pic_url: "http://i3.sinaimg.cn/dy/c/2014-04-01/1396366518_bYays1.jpg"
|
||||
```
|
||||
|
||||
然后执行命令行
|
||||
|
||||
```
|
||||
$ wechat custom_news oCfEht9oM*********** articles.yml
|
||||
|
||||
```
|
||||
|
||||
##### 发送模板消息
|
||||
需定义一个模板消息内容的yaml文件,比如
|
||||
template.yml
|
||||
|
||||
```
|
||||
template:
|
||||
template_id: "o64KQ62_xxxxxxxxxxxxxxx-Qz-MlNcRKteq8"
|
||||
url: "http://weixin.qq.com/download"
|
||||
topcolor: "#FF0000"
|
||||
data:
|
||||
first:
|
||||
value: "你好,你已报名成功"
|
||||
color: "#0A0A0A"
|
||||
keynote1:
|
||||
value: "XX活动"
|
||||
color: "#CCCCCC"
|
||||
keynote2:
|
||||
value: "2014年9月16日"
|
||||
color: "#CCCCCC"
|
||||
keynote3:
|
||||
value: "上海徐家汇xxx城"
|
||||
color: "#CCCCCC"
|
||||
remark:
|
||||
value: "欢迎再次使用。"
|
||||
color: "#173177"
|
||||
|
||||
```
|
||||
|
||||
然后执行命令行
|
||||
|
||||
```
|
||||
$ wechat template_message oCfEht9oM*********** template.yml
|
||||
```
|
||||
|
||||
## Rails Responder Controller DSL
|
||||
|
||||
为了在Rails app中响应用户的消息,开发者需要创建一个wechat responder controller. 首先在router中定义
|
||||
|
||||
```ruby
|
||||
resource :wechat, only:[:show, :create]
|
||||
```
|
||||
|
||||
然后创建Controller class, 例如
|
||||
|
||||
```ruby
|
||||
class WechatsController < ActionController::Base
|
||||
wechat_responder
|
||||
|
||||
# 默认文字信息responder
|
||||
on :text do |request, content|
|
||||
request.reply.text "echo: #{content}" #Just echo
|
||||
end
|
||||
|
||||
# 当请求的文字信息内容为'help'时, 使用这个responder处理
|
||||
on :text, with: 'help' do |request|
|
||||
request.reply.text 'help content' #回复帮助信息
|
||||
end
|
||||
|
||||
# 当请求的文字信息内容为'<n>条新闻'时, 使用这个responder处理, 并将n作为第二个参数
|
||||
on :text, with: /^(\d+)条新闻$/ do |request, count|
|
||||
# 微信最多显示10条新闻,大于10条将只取前10条
|
||||
news = (1..count.to_i).each_with_object([]) { |n, memo| memo << { title: '新闻标题', content: "第#{n}条新闻的内容#{n.hash}" } }
|
||||
request.reply.news(news) do |article, n, index| # 回复"articles"
|
||||
article.item title: "#{index} #{n[:title]}", description: n[:content], pic_url: 'http://www.baidu.com/img/bdlogo.gif', url: 'http://www.baidu.com/'
|
||||
end
|
||||
end
|
||||
|
||||
# 当用户加关注
|
||||
on :event, with: 'subscribe' do |request|
|
||||
request.reply.text "User #{request[:FromUserName]} subscribe now"
|
||||
end
|
||||
|
||||
# 公众号收到未关注用户扫描qrscene_xxxxxx二维码时。注意此次扫描事件将不再引发上条的用户加关注事件
|
||||
on :scan, with: 'qrscene_xxxxxx' do |request, ticket|
|
||||
request.reply.text "Unsubscribe user #{request[:FromUserName]} Ticket #{ticket}"
|
||||
end
|
||||
|
||||
# 公众号收到已关注用户扫描创建二维码的scene_id事件时
|
||||
on :scan, with: 'scene_id' do |request, ticket|
|
||||
request.reply.text "Subscribe user #{request[:FromUserName]} Ticket #{ticket}"
|
||||
end
|
||||
|
||||
# 当没有任何on :scan事件处理已关注用户扫描的scene_id时
|
||||
on :event, with: 'scan' do |request|
|
||||
if request[:EventKey].present?
|
||||
request.reply.text "event scan got EventKey #{request[:EventKey]} Ticket #{request[:Ticket]}"
|
||||
end
|
||||
end
|
||||
|
||||
# 企业号收到EventKey 为二维码扫描结果事件时
|
||||
on :scan, with: 'BINDING_QR_CODE' do |request, scan_result, scan_type|
|
||||
request.reply.text "User #{request[:FromUserName]} ScanResult #{scan_result} ScanType #{scan_type}"
|
||||
end
|
||||
|
||||
# 企业号收到EventKey 为CODE 39码扫描结果事件时
|
||||
on :scan, with: 'BINDING_BARCODE' do |message, scan_result|
|
||||
if scan_result.start_with? 'CODE_39,'
|
||||
message.reply.text "User: #{message[:FromUserName]} scan barcode, result is #{scan_result.split(',')[1]}"
|
||||
end
|
||||
end
|
||||
|
||||
# 当用户点击菜单时
|
||||
on :click, with: 'BOOK_LUNCH' do |request, key|
|
||||
request.reply.text "User: #{request[:FromUserName]} click #{key}"
|
||||
end
|
||||
|
||||
# 当用户点击菜单时
|
||||
on :view, with: 'http://wechat.somewhere.com/view_url' do |request, view|
|
||||
request.reply.text "#{request[:FromUserName]} view #{view}"
|
||||
end
|
||||
|
||||
# 处理图片信息
|
||||
on :image do |request|
|
||||
request.reply.image(request[:MediaId]) #直接将图片返回给用户
|
||||
end
|
||||
|
||||
# 处理语音信息
|
||||
on :voice do |request|
|
||||
request.reply.voice(request[:MediaId]) #直接语音音返回给用户
|
||||
end
|
||||
|
||||
# 处理视频信息
|
||||
on :video do |request|
|
||||
nickname = wechat.user(request[:FromUserName])['nickname'] #调用 api 获得发送者的nickname
|
||||
request.reply.video(request[:MediaId], title: '回声', description: "#{nickname}发来的视频请求") #直接视频返回给用户
|
||||
end
|
||||
|
||||
# 处理上报地理位置事件
|
||||
on :location do |request|
|
||||
request.reply.text("Latitude: #{message[:Latitude]} Longitude: #{message[:Longitude]} Precision: #{message[:Precision]}")
|
||||
end
|
||||
|
||||
# 当用户取消关注订阅
|
||||
on :event, with: 'unsubscribe' do |request|
|
||||
request.reply.success # user can not receive this message
|
||||
end
|
||||
|
||||
# 成员进入应用的事件推送
|
||||
on :event, with: 'enter_agent' do |request|
|
||||
request.reply.text "#{request[:FromUserName]} enter agent app now"
|
||||
end
|
||||
|
||||
# 当异步任务增量更新成员完成时推送
|
||||
on :batch_job, with: 'sync_user' do |request, batch_job|
|
||||
request.reply.text "job #{batch_job[:JobId]} finished, return code #{batch_job[:ErrCode]}, return message #{batch_job[:ErrMsg]}"
|
||||
end
|
||||
|
||||
# 当异步任务全量覆盖成员完成时推送
|
||||
on :batch_job, with: 'replace_user' do |request, batch_job|
|
||||
request.reply.text "job #{batch_job[:JobId]} finished, return code #{batch_job[:ErrCode]}, return message #{batch_job[:ErrMsg]}"
|
||||
end
|
||||
|
||||
# 当异步任务邀请成员关注完成时推送
|
||||
on :batch_job, with: 'invite_user' do |request, batch_job|
|
||||
request.reply.text "job #{batch_job[:JobId]} finished, return code #{batch_job[:ErrCode]}, return message #{batch_job[:ErrMsg]}"
|
||||
end
|
||||
|
||||
# 当异步任务全量覆盖部门完成时推送
|
||||
on :batch_job, with: 'replace_party' do |request, batch_job|
|
||||
request.reply.text "job #{batch_job[:JobId]} finished, return code #{batch_job[:ErrCode]}, return message #{batch_job[:ErrMsg]}"
|
||||
end
|
||||
|
||||
# 当无任何responder处理用户信息时,使用这个responder处理
|
||||
on :fallback, respond: 'fallback message'
|
||||
|
||||
# 如果你要在微信回复后增加一些操作,可以用 after_wechat_response(req, res)
|
||||
# private
|
||||
#
|
||||
# def after_wechat_response(req, res)
|
||||
# WechatLog.create req: req, res: res
|
||||
# end
|
||||
end
|
||||
```
|
||||
|
||||
在controller中使用`wechat_responder`引入Responder DSL, 之后可以用
|
||||
|
||||
```
|
||||
on <message_type> do |message|
|
||||
message.reply.text "some text"
|
||||
end
|
||||
```
|
||||
|
||||
来响应用户信息。
|
||||
|
||||
目前支持的message_type有如下几种
|
||||
|
||||
- :text 响应文字消息,可以用`:with`参数来匹配文本内容 `on(:text, with:'help'){|message, content| ...}`
|
||||
- :image 响应图片消息
|
||||
- :voice 响应语音消息
|
||||
- :video 响应视频消息
|
||||
- :link 响应链接消息
|
||||
- :event 响应事件消息, 可以用`:with`参数来匹配事件类型
|
||||
- :click 虚拟响应事件消息, 微信传入:event,但gem内部会单独处理
|
||||
- :view 虚拟响应事件消息, 微信传入:event,但gem内部会单独处理
|
||||
- :scan 虚拟响应事件消息
|
||||
- :batch_job 虚拟响应事件消息
|
||||
- :location 虚拟响应上报地理位置事件消息
|
||||
- :fallback 默认响应,当收到的消息无法被其他responder响应时,会使用这个responder.
|
||||
|
||||
### 多客服消息转发
|
||||
|
||||
```ruby
|
||||
class WechatsController < ActionController::Base
|
||||
# 当无任何responder处理用户信息时,转发至客服处理。
|
||||
on :fallback do |message|
|
||||
message.reply.transfer_customer_service
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
注意设置了[多客服消息转发](http://dkf.qq.com/)后,不能再添加`默认文字信息responder`,否则文字消息将得不到转发。
|
||||
|
||||
## 已知问题
|
||||
|
||||
* 企业号接受菜单消息时,Wechat腾讯服务器无法解析部分域名,请使用IP绑定回调URL,用户的普通消息目前不受影响。
|
||||
* 企业号全量覆盖成员使用的csv通讯录格式,直接将下载的模板导入[是不工作的](http://qydev.weixin.qq.com/qa/index.php?qa=13978),必须使用Excel打开,然后另存为csv格式才会变成合法格式。
|
||||
|
||||
|
||||
[version-badge]: https://badge.fury.io/rb/wechat.svg
|
||||
[rubygems]: https://rubygems.org/gems/wechat
|
||||
[travis-badge]: https://travis-ci.org/Eric-Guo/wechat.svg
|
||||
[travis]: https://travis-ci.org/Eric-Guo/wechat
|
||||
[codeclimate-badge]: https://codeclimate.com/github/Eric-Guo/wechat.png
|
||||
[codeclimate]: https://codeclimate.com/github/Eric-Guo/wechat
|
||||
[codecoverage-badge]: https://codeclimate.com/github/Eric-Guo/wechat/coverage.png
|
||||
[codecoverage]: https://codeclimate.com/github/Eric-Guo/wechat/coverage
|
||||
[gitter-badge]: https://badges.gitter.im/Join%20Chat.svg
|
||||
[gitter]: https://gitter.im/Eric-Guo/wechat?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
[issue-badge]: http://issuestats.com/github/Eric-Guo/wechat/badge/issue
|
||||
[pr-badge]: http://issuestats.com/github/Eric-Guo/wechat/badge/pr
|
||||
[issuestats]: http://issuestats.com/github/Eric-Guo/wechat
|
|
@ -0,0 +1,605 @@
|
|||
WeChat [![Gem Version][version-badge]][rubygems] [![Build Status][travis-badge]][travis] [![Code Climate][codeclimate-badge]][codeclimate] [![Code Coverage][codecoverage-badge]][codecoverage]
|
||||
======
|
||||
|
||||
[![Join the chat][gitter-badge]][gitter] [![Issue Stats][issue-badge]][issuestats] [![PR Stats][pr-badge]][issuestats]
|
||||
|
||||
[中文文档 Chinese document](/README-CN.md)
|
||||
|
||||
[Wechat](http://www.wechat.com/) is a free messaging and calling app developed by [Tencent](http://tencent.com/en-us/index.shtml), after linked billion people, Wechat become a platform of application
|
||||
|
||||
WeChat gem trying to helping Rails developer to integrated [enterprise account](https://qy.weixin.qq.com) / [public account](https://mp.weixin.qq.com/) easily. Below feature is ready and no need writing adapter code talking to wechat server directly.
|
||||
|
||||
- [Sending message](http://qydev.weixin.qq.com/wiki/index.php?title=%E5%8F%91%E9%80%81%E6%B6%88%E6%81%AF) API(Can access via console or in rails)
|
||||
- [Receiving message](http://qydev.weixin.qq.com/wiki/index.php?title=%E6%8E%A5%E6%94%B6%E6%B6%88%E6%81%AF%E4%B8%8E%E4%BA%8B%E4%BB%B6)(You must running on rails server to receiving message)
|
||||
- [Wechat JS-SDK](http://qydev.weixin.qq.com/wiki/index.php?title=%E5%BE%AE%E4%BF%A1JS%E6%8E%A5%E5%8F%A3) config signature
|
||||
- OAuth 2.0 authentication
|
||||
|
||||
|
||||
`wechat` command share the same API in console, so you can interactive with wechat server quickly, without starting up web environment/code.
|
||||
|
||||
A responder DSL can used in Rails controller, so giving a event based interface to handler message sent by end user from wechat server.
|
||||
|
||||
Wechat provide OAuth2.0 as authentication service and possible to intergrated with devise/other authorization gems, [omniauth-wechat-oauth2](https://github.com/skinnyworm/omniauth-wechat-oauth2) is a good start
|
||||
|
||||
There is official [weui](https://github.com/weui/weui), which corresponding Rails gems called [weui-rails](https://github.com/Eric-Guo/weui-rails) available, if you prefer following the same UI design as wechat.
|
||||
|
||||
## Installation
|
||||
|
||||
Using `gem install`
|
||||
|
||||
```
|
||||
gem install "wechat"
|
||||
```
|
||||
|
||||
Or add to your app's `Gemfile`:
|
||||
|
||||
```
|
||||
gem 'wechat'
|
||||
```
|
||||
|
||||
Run the following command to install it:
|
||||
|
||||
```console
|
||||
bundle install
|
||||
```
|
||||
|
||||
Run the generator:
|
||||
|
||||
```console
|
||||
rails generate wechat:install
|
||||
```
|
||||
|
||||
`rails g wechat:install` will generated the initial `wechat.yml` configuration, example wechat controller and corresponding routes.
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
#### Configure for command line
|
||||
|
||||
You can using `wechat` command solely, you need created configure file `~/.wechat.yml` and including below content for public account. the access_token will be write as a file.
|
||||
|
||||
```
|
||||
appid: "my_appid"
|
||||
secret: "my_secret"
|
||||
access_token: "/var/tmp/wechat_access_token"
|
||||
```
|
||||
|
||||
For enterprise account, need using `corpid` instead of `appid` as enterprise account support multiply application (Tencent called agent) in one enterprise account. Obtain the `corpsecret` is a little tricky, must created at management mode->privilege setting and create any of management group to obtain. Due to Tencent currently only provide Chinese interface for they management console, it's highly recommend you find a college knowing Mandarin to help you to obtain.
|
||||
|
||||
Windows user need store `.wechat.yml` at `C:/Users/[user_name]/` (replace your user name), also be caution the direction of folder separator.
|
||||
|
||||
```
|
||||
corpid: "my_appid"
|
||||
corpsecret: "my_secret"
|
||||
agentid: 1 # Integer, which can be obtain from application, settings
|
||||
access_token: "C:/Users/[user_name]/wechat_access_token"
|
||||
```
|
||||
|
||||
#### Configure for Rails
|
||||
|
||||
Rails configuration files support different environment similar to database.yml, after running `rails generate wechat:install` you can find configuration files at `config/wechat.yml`
|
||||
|
||||
Public account congfigure example:
|
||||
|
||||
```
|
||||
default: &default
|
||||
appid: "app_id"
|
||||
secret: "app_secret"
|
||||
token: "app_token"
|
||||
access_token: "/var/tmp/wechat_access_token"
|
||||
|
||||
production:
|
||||
appid: <%= ENV['WECHAT_APPID'] %>
|
||||
secret: <%= ENV['WECHAT_APP_SECRET'] %>
|
||||
token: <%= ENV['WECHAT_TOKEN'] %>
|
||||
access_token: <%= ENV['WECHAT_ACCESS_TOKEN'] %>
|
||||
|
||||
development:
|
||||
<<: *default
|
||||
|
||||
test:
|
||||
<<: *default
|
||||
```
|
||||
|
||||
Although it's optional for public account, but highly recommend to enable encrypt mode by add below two items to `wechat.yml`
|
||||
|
||||
|
||||
```
|
||||
default: &default
|
||||
encrypt_mode: true
|
||||
encoding_aes_key: "my_encoding_aes_key"
|
||||
```
|
||||
|
||||
Enterprise account must using encrypt mode (`encrypt_mode: true` is default on, no need configure).
|
||||
|
||||
The `token` and `encoding_aes_key` can be obtain from management console -> one of the agent application -> Mode selection, select callback mode and get/set.
|
||||
|
||||
```
|
||||
default: &default
|
||||
corpid: "corpid"
|
||||
corpsecret: "corpsecret"
|
||||
agentid: 1
|
||||
access_token: "C:/Users/[user_name]/wechat_access_token"
|
||||
token: ""
|
||||
encoding_aes_key: ""
|
||||
jsapi_ticket: "C:/Users/[user_name]/wechat_jsapi_ticket"
|
||||
|
||||
production:
|
||||
corpid: <%= ENV['WECHAT_CORPID'] %>
|
||||
corpsecret: <%= ENV['WECHAT_CORPSECRET'] %>
|
||||
agentid: <%= ENV['WECHAT_AGENTID'] %>
|
||||
access_token: <%= ENV['WECHAT_ACCESS_TOKEN'] %>
|
||||
token: <%= ENV['WECHAT_TOKEN'] %>
|
||||
timeout: 30,
|
||||
skip_verify_ssl: true # not recommend
|
||||
encoding_aes_key: <%= ENV['WECHAT_ENCODING_AES_KEY'] %>
|
||||
jsapi_ticket: <%= ENV['WECHAT_JSAPI_TICKET'] %>
|
||||
|
||||
development:
|
||||
<<: *default
|
||||
|
||||
test:
|
||||
<<: *default
|
||||
```
|
||||
|
||||
##### Configure priority
|
||||
|
||||
Running `wechat` command in the root folder of Rails application will using the Rails configuration first (`default` section), if can not find it, will relay on `~\.wechat.yml`, such behavior enable manage more wechat public account and enterprise account without changing your home `~\.wechat.yml` file.
|
||||
|
||||
##### Wechat server timeout setting
|
||||
|
||||
Stability various even for Tencent wechat server, so setting a long timeout may needed, default is 20 seconds if not set.
|
||||
|
||||
##### Skip the SSL verification
|
||||
|
||||
SSL Certification can also be corrupted by some reason in China, [it's reported](http://qydev.weixin.qq.com/qa/index.php?qa=11037) and if it's happen to you, can setting `skip_verify_ssl: true`. (not recommend)
|
||||
|
||||
#### Configure individual responder with different appid
|
||||
|
||||
Rare case, you may want to hosting more than one wechat enterprise/public account in one Rails application, so you can given those configuration info when calling `wechat_responder`
|
||||
|
||||
```ruby
|
||||
class WechatFirstController < ActionController::Base
|
||||
wechat_responder appid: "app1", secret: "secret1", token: "token1", access_token: Rails.root.join("tmp/access_token1")
|
||||
|
||||
on :text, with:"help", respond: "help content"
|
||||
end
|
||||
```
|
||||
|
||||
#### JS-SDK helper
|
||||
|
||||
JS-SDK enable you control wechat behavior in your web page, but need inject a config with signature methods first, you can obtain those signature hash via below
|
||||
|
||||
```ruby
|
||||
WechatsController.wechat.jsapi_ticket.signature(request.original_url)
|
||||
```
|
||||
|
||||
## The API privilege
|
||||
|
||||
wechat gems won't handle any privilege exception. (except token time out, but it's not important to you as it's auto retry/recovery in gems internally), but Tencent will control a lot of privilege based on your public account type and certification, more info, please reference [official document](http://mp.weixin.qq.com/wiki/7/2d301d4b757dedc333b9a9854b457b47.html).
|
||||
|
||||
## Command line mode
|
||||
|
||||
The available API is different between public account and enterprise account, so wechat gems provide different set of command.
|
||||
|
||||
Feel safe if you can not read Chinese in the comments, it's keep there in order to copy & find in official document more easier.
|
||||
|
||||
#### Public account command line
|
||||
|
||||
```
|
||||
$ wechat
|
||||
Wechat commands:
|
||||
wechat callbackip # 获取微信服务器IP地址
|
||||
wechat custom_image [OPENID, IMAGE_PATH] # 发送图片客服消息
|
||||
wechat custom_music [OPENID, THUMBNAIL_PATH, MUSIC_URL] # 发送音乐客服消息
|
||||
wechat custom_news [OPENID, NEWS_YAML_PATH] # 发送图文客服消息
|
||||
wechat custom_text [OPENID, TEXT_MESSAGE] # 发送文字客服消息
|
||||
wechat custom_video [OPENID, VIDEO_PATH] # 发送视频客服消息
|
||||
wechat custom_voice [OPENID, VOICE_PATH] # 发送语音客服消息
|
||||
wechat group_create [GROUP_NAME] # 创建分组
|
||||
wechat group_delete [GROUP_ID] # 删除分组
|
||||
wechat group_update [GROUP_ID, NEW_GROUP_NAME] # 修改分组名
|
||||
wechat groups # 查询所有分组
|
||||
wechat material [MEDIA_ID, PATH] # 永久媒体下载
|
||||
wechat material_add [MEDIA_TYPE, PATH] # 永久媒体上传
|
||||
wechat material_count # 获取永久素材总数
|
||||
wechat material_delete [MEDIA_ID] # 删除永久素材
|
||||
wechat material_list [TYPE, OFFSET, COUNT] # 获取永久素材列表
|
||||
wechat media [MEDIA_ID, PATH] # 媒体下载
|
||||
wechat media_create [MEDIA_TYPE, PATH] # 媒体上传
|
||||
wechat menu # 当前菜单
|
||||
wechat menu_create [MENU_YAML_PATH] # 创建菜单
|
||||
wechat menu_delete # 删除菜单
|
||||
wechat oauth2_url [REDIRECT_URI] # 生成OAuth2.0验证URL
|
||||
wechat qrcode_create_limit_scene [SCENE_ID_OR_STR] # 请求永久二维码
|
||||
wechat qrcode_create_scene [SCENE_ID, EXPIRE_SECONDS] # 请求临时二维码
|
||||
wechat qrcode_download [TICKET, QR_CODE_PIC_PATH] # 通过ticket下载二维码
|
||||
wechat template_message [OPENID, TEMPLATE_YAML_PATH] # 模板消息接口
|
||||
wechat user [OPEN_ID] # 获取用户基本信息
|
||||
wechat user_change_group [OPEN_ID, TO_GROUP_ID] # 移动用户分组
|
||||
wechat user_group [OPEN_ID] # 查询用户所在分组
|
||||
wechat user_update_remark [OPEN_ID, REMARK] # 设置备注名
|
||||
wechat users # 关注者列表
|
||||
```
|
||||
|
||||
#### Enterprise account command line
|
||||
```
|
||||
$ wechat
|
||||
Wechat commands:
|
||||
wechat agent [AGENT_ID] # 获取企业号应用详情
|
||||
wechat agent_list # 获取应用概况列表
|
||||
wechat batch_job_result [JOB_ID] # 获取异步任务结果
|
||||
wechat batch_replaceparty [BATCH_PARTY_CSV_MEDIA_ID] # 全量覆盖部门
|
||||
wechat batch_replaceuser [BATCH_USER_CSV_MEDIA_ID] # 全量覆盖成员
|
||||
wechat batch_syncuser [SYNC_USER_CSV_MEDIA_ID] # 增量更新成员
|
||||
wechat callbackip # 获取微信服务器IP地址
|
||||
wechat convert_to_openid [USER_ID] # userid转换成openid
|
||||
wechat custom_image [OPENID, IMAGE_PATH] # 发送图片客服消息
|
||||
wechat custom_music [OPENID, THUMBNAIL_PATH, MUSIC_URL] # 发送音乐客服消息
|
||||
wechat custom_news [OPENID, NEWS_YAML_PATH] # 发送图文客服消息
|
||||
wechat custom_text [OPENID, TEXT_MESSAGE] # 发送文字客服消息
|
||||
wechat custom_video [OPENID, VIDEO_PATH] # 发送视频客服消息
|
||||
wechat custom_voice [OPENID, VOICE_PATH] # 发送语音客服消息
|
||||
wechat department [DEPARTMENT_ID] # 获取部门列表
|
||||
wechat department_create [NAME, PARENT_ID] # 创建部门
|
||||
wechat department_delete [DEPARTMENT_ID] # 删除部门
|
||||
wechat department_update [DEPARTMENT_ID, NAME] # 更新部门
|
||||
wechat invite_user [USER_ID] # 邀请成员关注
|
||||
wechat material [MEDIA_ID, PATH] # 永久媒体下载
|
||||
wechat material_add [MEDIA_TYPE, PATH] # 永久媒体上传
|
||||
wechat material_count # 获取永久素材总数
|
||||
wechat material_delete [MEDIA_ID] # 删除永久素材
|
||||
wechat material_list [TYPE, OFFSET, COUNT] # 获取永久素材列表
|
||||
wechat media [MEDIA_ID, PATH] # 媒体下载
|
||||
wechat media_create [MEDIA_TYPE, PATH] # 媒体上传
|
||||
wechat menu # 当前菜单
|
||||
wechat menu_create [MENU_YAML_PATH] # 创建菜单
|
||||
wechat menu_delete # 删除菜单
|
||||
wechat message_send [OPENID, TEXT_MESSAGE] # 发送文字消息
|
||||
wechat oauth2_url [REDIRECT_URI] # 生成OAuth2.0验证URL
|
||||
wechat qrcode_download [TICKET, QR_CODE_PIC_PATH] # 通过ticket下载二维码
|
||||
wechat tag [TAG_ID] # 获取标签成员
|
||||
wechat tag_add_department [TAG_ID, PARTY_IDS] # 增加标签部门
|
||||
wechat tag_add_user [TAG_ID, USER_IDS] # 增加标签成员
|
||||
wechat tag_create [TAGNAME, TAG_ID] # 创建标签
|
||||
wechat tag_del_department [TAG_ID, PARTY_IDS] # 删除标签部门
|
||||
wechat tag_del_user [TAG_ID, USER_IDS] # 删除标签成员
|
||||
wechat tag_delete [TAG_ID] # 删除标签
|
||||
wechat tag_update [TAG_ID, TAGNAME] # 更新标签名字
|
||||
wechat tags # 获取标签列表
|
||||
wechat template_message [OPENID, TEMPLATE_YAML_PATH] # 模板消息接口
|
||||
wechat upload_replaceparty [BATCH_PARTY_CSV_PATH] # 上传文件方式全量覆盖部门
|
||||
wechat upload_replaceuser [BATCH_USER_CSV_PATH] # 上传文件方式全量覆盖成员
|
||||
wechat user [OPEN_ID] # 获取用户基本信息
|
||||
wechat user_batchdelete [USER_ID_LIST] # 批量删除成员
|
||||
wechat user_delete [USER_ID] # 删除成员
|
||||
wechat user_list [DEPARTMENT_ID] # 获取部门成员详情
|
||||
wechat user_simplelist [DEPARTMENT_ID] # 获取部门成员
|
||||
wechat user_update_remark [OPEN_ID, REMARK] # 设置备注名
|
||||
```
|
||||
|
||||
### Command line usage demo (partially)
|
||||
|
||||
##### Fetch all users open id
|
||||
|
||||
```
|
||||
$ wechat users
|
||||
|
||||
{"total"=>4, "count"=>4, "data"=>{"openid"=>["oCfEht9***********", "oCfEhtwqa***********", "oCfEht9oMCqGo***********", "oCfEht_81H5o2***********"]}, "next_openid"=>"oCfEht_81H5o2***********"}
|
||||
|
||||
```
|
||||
|
||||
##### Fetch user info
|
||||
|
||||
```
|
||||
$ wechat user "oCfEht9***********"
|
||||
|
||||
{"subscribe"=>1, "openid"=>"oCfEht9***********", "nickname"=>"Nickname", "sex"=>1, "language"=>"zh_CN", "city"=>"徐汇", "province"=>"上海", "country"=>"中国", "headimgurl"=>"http://wx.qlogo.cn/mmopen/ajNVdqHZLLBd0SG8NjV3UpXZuiaGGPDcaKHebTKiaTyof*********/0", "subscribe_time"=>1395715239}
|
||||
|
||||
```
|
||||
|
||||
##### Fetch menu
|
||||
```
|
||||
$ wechat menu
|
||||
|
||||
{"menu"=>{"button"=>[{"type"=>"view", "name"=>"保护的", "url"=>"http://***/protected", "sub_button"=>[]}, {"type"=>"view", "name"=>"公开的", "url"=>"http://***", "sub_button"=>[]}]}}
|
||||
|
||||
```
|
||||
|
||||
##### Menu create
|
||||
|
||||
menu content for a wechat application can be defined as a yaml files, like `menu.yaml`
|
||||
|
||||
```
|
||||
button:
|
||||
-
|
||||
name: "Want"
|
||||
sub_button:
|
||||
-
|
||||
type: "scancode_waitmsg"
|
||||
name: "绑定用餐二维码"
|
||||
key: "BINDING_QR_CODE"
|
||||
-
|
||||
type: "click"
|
||||
name: "预订午餐"
|
||||
key: "BOOK_LUNCH"
|
||||
-
|
||||
type: "click"
|
||||
name: "预订晚餐"
|
||||
key: "BOOK_DINNER"
|
||||
-
|
||||
name: "Query"
|
||||
sub_button:
|
||||
-
|
||||
type: "click"
|
||||
name: "进出记录"
|
||||
key: "BADGE_IN_OUT"
|
||||
-
|
||||
type: "click"
|
||||
name: "年假余额"
|
||||
key: "ANNUAL_LEAVE"
|
||||
-
|
||||
type: "view"
|
||||
name: "About"
|
||||
url: "http://blog.cloud-mes.com/"
|
||||
```
|
||||
|
||||
Caution: make sure you having management privilege for those application below running below command, other will got [60011](http://qydev.weixin.qq.com/wiki/index.php?title=%E5%85%A8%E5%B1%80%E8%BF%94%E5%9B%9E%E7%A0%81%E8%AF%B4%E6%98%8E) error.
|
||||
|
||||
```
|
||||
$ wechat menu_create menu.yaml
|
||||
```
|
||||
|
||||
##### Sent custom news
|
||||
|
||||
|
||||
Sending custom_news should also defined as a yaml file, like `articles.yaml`
|
||||
|
||||
```
|
||||
articles:
|
||||
-
|
||||
title: "习近平在布鲁日欧洲学院演讲"
|
||||
description: "新华网比利时布鲁日4月1日电 国家主席习近平1日在比利时布鲁日欧洲学院发表重要演讲"
|
||||
url: "http://news.sina.com.cn/c/2014-04-01/232629843387.shtml"
|
||||
pic_url: "http://i3.sinaimg.cn/dy/c/2014-04-01/1396366518_bYays1.jpg"
|
||||
```
|
||||
|
||||
After that, can running command:
|
||||
|
||||
```
|
||||
$ wechat custom_news oCfEht9oM*********** articles.yml
|
||||
|
||||
```
|
||||
|
||||
##### Sent template message
|
||||
|
||||
Sending template message via yaml file similar too, defined `template.yml` and content is just the template content.
|
||||
|
||||
```
|
||||
template:
|
||||
template_id: "o64KQ62_xxxxxxxxxxxxxxx-Qz-MlNcRKteq8"
|
||||
url: "http://weixin.qq.com/download"
|
||||
topcolor: "#FF0000"
|
||||
data:
|
||||
first:
|
||||
value: "Hello, you successfully registed"
|
||||
color: "#0A0A0A"
|
||||
keynote1:
|
||||
value: "5km Health Running"
|
||||
color: "#CCCCCC"
|
||||
keynote2:
|
||||
value: "2014-09-16"
|
||||
color: "#CCCCCC"
|
||||
keynote3:
|
||||
value: "Centry Park, Pudong, Shanghai"
|
||||
color: "#CCCCCC"
|
||||
remark:
|
||||
value: "Welcome back"
|
||||
color: "#173177"
|
||||
|
||||
```
|
||||
|
||||
After that, can running command:
|
||||
|
||||
```
|
||||
$ wechat template_message oCfEht9oM*********** template.yml
|
||||
```
|
||||
|
||||
## Rails Responder Controller DSL
|
||||
|
||||
In order to responding the message user sent, Rails developer need create a wechat responder controller and defined the routing in `routes.rb`
|
||||
|
||||
```ruby
|
||||
resource :wechat, only:[:show, :create]
|
||||
```
|
||||
|
||||
So the ActionController should defined like below:
|
||||
|
||||
```ruby
|
||||
class WechatsController < ActionController::Base
|
||||
wechat_responder
|
||||
|
||||
# default text responder when no other match
|
||||
on :text do |request, content|
|
||||
request.reply.text "echo: #{content}" # Just echo
|
||||
end
|
||||
|
||||
# When receive 'help', will trigger this responder
|
||||
on :text, with: 'help' do |request|
|
||||
request.reply.text 'help content'
|
||||
end
|
||||
|
||||
# When receive '<n>news', will match and will got count as <n> as parameter
|
||||
on :text, with: /^(\d+) news$/ do |request, count|
|
||||
# Wechat article can only contain max 10 items, large than 10 will dropped.
|
||||
news = (1..count.to_i).each_with_object([]) { |n, memo| memo << { title: 'News title', content: "No. #{n} news content" } }
|
||||
request.reply.news(news) do |article, n, index| # article is return object
|
||||
article.item title: "#{index} #{n[:title]}", description: n[:content], pic_url: 'http://www.baidu.com/img/bdlogo.gif', url: 'http://www.baidu.com/'
|
||||
end
|
||||
end
|
||||
|
||||
on :event, with: 'subscribe' do |request|
|
||||
request.reply.text "#{request[:FromUserName]} subscribe now"
|
||||
end
|
||||
|
||||
# When unsubscribe user scan qrcode qrscene_xxxxxx to subscribe in public account
|
||||
# notice user will subscribe public account at same time, so wechat won't trigger subscribe event any more
|
||||
on :scan, with: 'qrscene_xxxxxx' do |request, ticket|
|
||||
request.reply.text "Unsubscribe user #{request[:FromUserName]} Ticket #{ticket}"
|
||||
end
|
||||
|
||||
# When subscribe user scan scene_id in public account
|
||||
on :scan, with: 'scene_id' do |request, ticket|
|
||||
request.reply.text "Subscribe user #{request[:FromUserName]} Ticket #{ticket}"
|
||||
end
|
||||
|
||||
# When no any on :scan responder can match subscribe user scaned scene_id
|
||||
on :event, with: 'scan' do |request|
|
||||
if request[:EventKey].present?
|
||||
request.reply.text "event scan got EventKey #{request[:EventKey]} Ticket #{request[:Ticket]}"
|
||||
end
|
||||
end
|
||||
|
||||
# When enterprise user press menu BINDING_QR_CODE and success to scan bar code
|
||||
on :scan, with: 'BINDING_QR_CODE' do |request, scan_result, scan_type|
|
||||
request.reply.text "User #{request[:FromUserName]} ScanResult #{scan_result} ScanType #{scan_type}"
|
||||
end
|
||||
|
||||
# Except QR code, wechat can also scan CODE_39 bar code in enterprise account
|
||||
on :scan, with: 'BINDING_BARCODE' do |message, scan_result|
|
||||
if scan_result.start_with? 'CODE_39,'
|
||||
message.reply.text "User: #{message[:FromUserName]} scan barcode, result is #{scan_result.split(',')[1]}"
|
||||
end
|
||||
end
|
||||
|
||||
# When user click the menu button
|
||||
on :click, with: 'BOOK_LUNCH' do |request, key|
|
||||
request.reply.text "User: #{request[:FromUserName]} click #{key}"
|
||||
end
|
||||
|
||||
# When user view URL in the menu button
|
||||
on :view, with: 'http://wechat.somewhere.com/view_url' do |request, view|
|
||||
request.reply.text "#{request[:FromUserName]} view #{view}"
|
||||
end
|
||||
|
||||
# When user sent the imsage
|
||||
on :image do |request|
|
||||
request.reply.image(request[:MediaId]) # Echo the sent image to user
|
||||
end
|
||||
|
||||
# When user sent the voice
|
||||
on :voice do |request|
|
||||
request.reply.voice(request[:MediaId]) # Echo the sent voice to user
|
||||
end
|
||||
|
||||
# When user sent the video
|
||||
on :video do |request|
|
||||
nickname = wechat.user(request[:FromUserName])['nickname'] # Call wechat api to get sender nickname
|
||||
request.reply.video(request[:MediaId], title: 'Echo', description: "Got #{nickname} sent video") # Echo the sent video to user
|
||||
end
|
||||
|
||||
# When user sent location
|
||||
on :location do |request|
|
||||
request.reply.text("Latitude: #{message[:Latitude]} Longitude: #{message[:Longitude]} Precision: #{message[:Precision]}")
|
||||
end
|
||||
|
||||
on :event, with: 'unsubscribe' do |request|
|
||||
request.reply.success # user can not receive this message
|
||||
end
|
||||
|
||||
# When user enter the app / agent app
|
||||
on :event, with: 'enter_agent' do |request|
|
||||
request.reply.text "#{request[:FromUserName]} enter agent app now"
|
||||
end
|
||||
|
||||
# When batch job create/update user (incremental) finished.
|
||||
on :batch_job, with: 'sync_user' do |request, batch_job|
|
||||
request.reply.text "sync_user job #{batch_job[:JobId]} finished, return code #{batch_job[:ErrCode]}, return message #{batch_job[:ErrMsg]}"
|
||||
end
|
||||
|
||||
# When batch job replace user (full sync) finished.
|
||||
on :batch_job, with: 'replace_user' do |request, batch_job|
|
||||
request.reply.text "replace_user job #{batch_job[:JobId]} finished, return code #{batch_job[:ErrCode]}, return message #{batch_job[:ErrMsg]}"
|
||||
end
|
||||
|
||||
# When batch job invent user finished.
|
||||
on :batch_job, with: 'invite_user' do |request, batch_job|
|
||||
request.reply.text "invite_user job #{batch_job[:JobId]} finished, return code #{batch_job[:ErrCode]}, return message #{batch_job[:ErrMsg]}"
|
||||
end
|
||||
|
||||
# When batch job replace department (full sync) finished.
|
||||
on :batch_job, with: 'replace_party' do |request, batch_job|
|
||||
request.reply.text "replace_party job #{batch_job[:JobId]} finished, return code #{batch_job[:ErrCode]}, return message #{batch_job[:ErrMsg]}"
|
||||
end
|
||||
|
||||
# Any not match above will fail to below
|
||||
on :fallback, respond: 'fallback message'
|
||||
|
||||
# If you need do something after response, you should add after_wechat_response(req, res)
|
||||
# private
|
||||
#
|
||||
# def after_wechat_response(req, res)
|
||||
# WechatLog.create req: req, res: res
|
||||
# end
|
||||
end
|
||||
```
|
||||
|
||||
So the importent statement is only `wechat_responder`, all other is just a DSL:
|
||||
|
||||
```
|
||||
on <message_type> do |message|
|
||||
message.reply.text "some text"
|
||||
end
|
||||
```
|
||||
|
||||
The block code will be running to responding user's message.
|
||||
|
||||
|
||||
Below is current supported message_type:
|
||||
|
||||
- :text text message, using `:with` to match text content like `on(:text, with:'help'){|message, content| ...}`
|
||||
- :image image message
|
||||
- :voice voice message
|
||||
- :video video message
|
||||
- :link link message
|
||||
- :event event message, using `:with` to match particular event
|
||||
- :click virtual event message, wechat still sent event message,but gems will mapping to menu click event.
|
||||
- :view virtual view message, wechat still sent event message,but gems will mapping to menu view page event.
|
||||
- :scan virtual scan message, wechat still sent event message, but gems will mapping on scan event
|
||||
- :batch_job virtual batch job message
|
||||
- :location virtual location message
|
||||
- :fallback default message, when no other responder can handle incoming messsage, will using fallback to handle
|
||||
|
||||
### Transfer to customer service
|
||||
|
||||
```ruby
|
||||
class WechatsController < ActionController::Base
|
||||
# When no other responder can handle incoming message, will transfer to human customer service.
|
||||
on :fallback do |message|
|
||||
message.reply.transfer_customer_service
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Caution: do not setting default text responder if you want to using [multiply human customer service](http://dkf.qq.com/), other will lead text message can not transfer.
|
||||
|
||||
|
||||
## Known Issue
|
||||
|
||||
* Sometime, enterprise account can not receive the menu message due to Tencent server can not resolved the DNS, so using IP as a callback URL more stable, but it's never happen for user sent text message.
|
||||
* Enterprise batch replace users using a CSV format file, but if you using the download template directly, it's [not working](http://qydev.weixin.qq.com/qa/index.php?qa=13978), must open the CSV file in excel first, then save as CSV format again, seems Tencent only support Excel save as CSV file format.
|
||||
|
||||
|
||||
[version-badge]: https://badge.fury.io/rb/wechat.svg
|
||||
[rubygems]: https://rubygems.org/gems/wechat
|
||||
[travis-badge]: https://travis-ci.org/Eric-Guo/wechat.svg
|
||||
[travis]: https://travis-ci.org/Eric-Guo/wechat
|
||||
[codeclimate-badge]: https://codeclimate.com/github/Eric-Guo/wechat.png
|
||||
[codeclimate]: https://codeclimate.com/github/Eric-Guo/wechat
|
||||
[codecoverage-badge]: https://codeclimate.com/github/Eric-Guo/wechat/coverage.png
|
||||
[codecoverage]: https://codeclimate.com/github/Eric-Guo/wechat/coverage
|
||||
[gitter-badge]: https://badges.gitter.im/Join%20Chat.svg
|
||||
[gitter]: https://gitter.im/Eric-Guo/wechat?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
[issue-badge]: http://issuestats.com/github/Eric-Guo/wechat/badge/issue
|
||||
[pr-badge]: http://issuestats.com/github/Eric-Guo/wechat/badge/pr
|
||||
[issuestats]: http://issuestats.com/github/Eric-Guo/wechat
|
|
@ -0,0 +1,27 @@
|
|||
#!/usr/bin/env rake
|
||||
begin
|
||||
require 'bundler/setup'
|
||||
rescue LoadError
|
||||
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
||||
end
|
||||
begin
|
||||
require 'rdoc/task'
|
||||
rescue LoadError
|
||||
require 'rdoc/rdoc'
|
||||
require 'rake/rdoctask'
|
||||
RDoc::Task = Rake::RDocTask
|
||||
end
|
||||
|
||||
RDoc::Task.new(:rdoc) do |rdoc|
|
||||
rdoc.rdoc_dir = 'rdoc'
|
||||
rdoc.title = 'Wechat'
|
||||
rdoc.options << '--line-numbers'
|
||||
rdoc.rdoc_files.include('README.rdoc')
|
||||
rdoc.rdoc_files.include('lib/**/*.rb')
|
||||
end
|
||||
|
||||
require File.join('bundler', 'gem_tasks')
|
||||
require File.join('rspec', 'core', 'rake_task')
|
||||
RSpec::Core::RakeTask.new(:spec)
|
||||
|
||||
task default: :spec
|
|
@ -0,0 +1 @@
|
|||
0.7.1
|
|
@ -0,0 +1,415 @@
|
|||
#!/usr/bin/env ruby
|
||||
|
||||
lib = File.expand_path(File.dirname(__FILE__) + '/../lib')
|
||||
$LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
|
||||
|
||||
require 'thor'
|
||||
require 'wechat'
|
||||
require 'json'
|
||||
require 'active_support' # To support Rails 4.2.1, see #17936
|
||||
require 'active_support/dependencies/autoload'
|
||||
require 'active_support/core_ext'
|
||||
require 'active_support/json'
|
||||
require 'fileutils'
|
||||
require 'yaml'
|
||||
require 'wechat/api_loader'
|
||||
require 'cgi'
|
||||
|
||||
class App < Thor
|
||||
package_name 'Wechat'
|
||||
option :token_file, aliases: '-t', desc: 'File to store access token'
|
||||
|
||||
attr_reader :wechat_api_client
|
||||
no_commands do
|
||||
def wechat_api
|
||||
@wechat_api_client ||= Wechat::ApiLoader.with(options)
|
||||
end
|
||||
end
|
||||
|
||||
desc 'callbackip', '获取微信服务器IP地址'
|
||||
def callbackip
|
||||
puts wechat_api.callbackip
|
||||
end
|
||||
|
||||
desc 'qrcode_download [TICKET, QR_CODE_PIC_PATH]', '通过ticket下载二维码'
|
||||
def qrcode_download(ticket, qr_code_pic_path)
|
||||
tmp_file = wechat_api.qrcode(ticket)
|
||||
FileUtils.mv(tmp_file.path, qr_code_pic_path)
|
||||
puts 'File downloaded'
|
||||
end
|
||||
|
||||
if Wechat::ApiLoader.with(options).is_a?(Wechat::CorpApi)
|
||||
desc 'department_create [NAME, PARENT_ID]', '创建部门'
|
||||
method_option :parentid, aliases: '-p', desc: '父亲部门id。根部门id为1'
|
||||
def department_create(name)
|
||||
api_opts = options.slice(:parentid)
|
||||
puts wechat_api.department_create(name, api_opts[:parentid] || '1')
|
||||
end
|
||||
|
||||
desc 'department_delete [DEPARTMENT_ID]', '删除部门'
|
||||
def department_delete(departmentid)
|
||||
puts wechat_api.department_delete(departmentid)
|
||||
end
|
||||
|
||||
desc 'department_update [DEPARTMENT_ID, NAME]', '更新部门'
|
||||
method_option :parentid, aliases: '-p', desc: '父亲部门id。根部门id为1', default: nil
|
||||
method_option :order, aliases: '-o', desc: '在父部门中的次序值。order值小的排序靠前。', default: nil
|
||||
def department_update(departmentid, name)
|
||||
api_opts = options.slice(:parentid, :order)
|
||||
puts wechat_api.department_update(departmentid, name, api_opts[:parentid], api_opts[:order])
|
||||
end
|
||||
|
||||
desc 'department [DEPARTMENT_ID]', '获取部门列表'
|
||||
def department(departmentid = 0)
|
||||
r = wechat_api.department(departmentid)
|
||||
puts "errcode: #{r['errcode']} errmsg: #{r['errmsg']}"
|
||||
puts 'Or# pid id name'
|
||||
r['department'].sort_by { |d| d['order'].to_i + d['parentid'].to_i * 1000 } .each do |i|
|
||||
puts format('%3d %3d %3d %s', i['order'], i['parentid'], i['id'], i['name'])
|
||||
end
|
||||
end
|
||||
|
||||
desc 'user_delete [USER_ID]', '删除成员'
|
||||
def user_delete(userid)
|
||||
puts wechat_api.user_delete(userid)
|
||||
end
|
||||
|
||||
desc 'user_batchdelete [USER_ID_LIST]', '批量删除成员'
|
||||
def user_batchdelete(useridlist)
|
||||
puts wechat_api.user_batchdelete(useridlist.split(','))
|
||||
end
|
||||
|
||||
desc 'user_simplelist [DEPARTMENT_ID]', '获取部门成员'
|
||||
method_option :fetch_child, aliases: '-c', desc: '是否递归获取子部门下面的成员', default: 1
|
||||
method_option :status, aliases: '-s', desc: '0 获取全部成员,1 获取已关注成员列表,2 获取禁用成员列表,4 获取未关注成员列表。status可叠加', default: 0
|
||||
def user_simplelist(departmentid = 0)
|
||||
api_opts = options.slice(:fetch_child, :status)
|
||||
|
||||
r = wechat_api.user_simplelist(departmentid, api_opts[:fetch_child], api_opts[:status])
|
||||
puts "errcode: #{r['errcode']} errmsg: #{r['errmsg']}"
|
||||
puts " userid Name #{' ' * 20} department_ids"
|
||||
r['userlist'].sort_by { |d| d['userid'] } .each do |i|
|
||||
puts format('%7s %-25s %-14s', i['userid'], i['name'], i['department'])
|
||||
end
|
||||
end
|
||||
|
||||
desc 'user_list [DEPARTMENT_ID]', '获取部门成员详情'
|
||||
method_option :fetch_child, aliases: '-c', desc: '是否递归获取子部门下面的成员', default: 0
|
||||
method_option :status, aliases: '-s', desc: '0 获取全部成员,1 获取已关注成员列表,2 获取禁用成员列表,4 获取未关注成员列表。status可叠加', default: 0
|
||||
def user_list(departmentid = 0)
|
||||
api_opts = options.slice(:fetch_child, :status)
|
||||
|
||||
r = wechat_api.user_list(departmentid, api_opts[:fetch_child], api_opts[:status])
|
||||
puts "errcode: #{r['errcode']} errmsg: #{r['errmsg']}"
|
||||
puts " userid Name #{' ' * 15} department_ids position mobile #{' ' * 5}gender email #{' ' * 10}weixinid status extattr"
|
||||
r['userlist'].sort_by { |d| d['userid'] } .each do |i|
|
||||
puts format('%7s %-20s %-14s %-8s %-11s %-6s %-15s %-15s %-6s %s',
|
||||
i['userid'], i['name'], i['department'], i['position'], i['mobile'],
|
||||
i['gender'], i['email'], i['weixinid'], i['status'], i['extattr'])
|
||||
end
|
||||
end
|
||||
|
||||
desc 'invite_user [USER_ID]', '邀请成员关注'
|
||||
def invite_user(userid)
|
||||
puts wechat_api.invite_user(userid)
|
||||
end
|
||||
|
||||
desc 'tag_create [TAGNAME, TAG_ID]', '创建标签'
|
||||
method_option :tagid, aliases: '-id', desc: '整型,指定此参数时新增的标签会生成对应的标签id,不指定时则以目前最大的id自增'
|
||||
def tag_create(name)
|
||||
api_opts = options.slice(:tagid)
|
||||
puts wechat_api.tag_create(name, api_opts[:tagid])
|
||||
end
|
||||
|
||||
desc 'tag_update [TAG_ID, TAGNAME]', '更新标签名字'
|
||||
def tag_update(tagid, tagname)
|
||||
puts wechat_api.tag_update(tagid, tagname)
|
||||
end
|
||||
|
||||
desc 'tag_delete [TAG_ID]', '删除标签'
|
||||
def tag_delete(tagid)
|
||||
puts wechat_api.tag_delete(tagid)
|
||||
end
|
||||
|
||||
desc 'tag [TAG_ID]', '获取标签成员'
|
||||
def tag(tagid)
|
||||
puts wechat_api.tag(tagid)
|
||||
end
|
||||
|
||||
desc 'tag_add_user [TAG_ID, USER_IDS]', '增加标签成员'
|
||||
def tag_add_user(tagid, userids)
|
||||
puts wechat_api.tag_add_user(tagid, userids.split(','))
|
||||
end
|
||||
|
||||
desc 'tag_add_department [TAG_ID, PARTY_IDS]', '增加标签部门'
|
||||
def tag_add_department(tagid, partyids)
|
||||
puts wechat_api.tag_add_user(tagid, nil, partyids.split(','))
|
||||
end
|
||||
|
||||
desc 'tag_del_user [TAG_ID, USER_IDS]', '删除标签成员'
|
||||
def tag_del_user(tagid, userids)
|
||||
puts wechat_api.tag_del_user(tagid, userids.split(','))
|
||||
end
|
||||
|
||||
desc 'tag_del_department [TAG_ID, PARTY_IDS]', '删除标签部门'
|
||||
def tag_del_department(tagid, partyids)
|
||||
puts wechat_api.tag_del_user(tagid, nil, partyids.split(','))
|
||||
end
|
||||
|
||||
desc 'tags', '获取标签列表'
|
||||
def tags
|
||||
puts wechat_api.tags
|
||||
end
|
||||
|
||||
desc 'batch_job_result [JOB_ID]', '获取异步任务结果'
|
||||
def batch_job_result(job_id)
|
||||
puts wechat_api.batch_job_result(job_id)
|
||||
end
|
||||
|
||||
desc 'batch_replaceparty [BATCH_PARTY_CSV_MEDIA_ID]', '全量覆盖部门'
|
||||
def batch_replaceparty(batch_party_csv_media_id)
|
||||
puts wechat_api.batch_replaceparty(batch_party_csv_media_id)
|
||||
end
|
||||
|
||||
desc 'upload_replaceparty [BATCH_PARTY_CSV_PATH]', '上传文件方式全量覆盖部门'
|
||||
def upload_replaceparty(batch_party_csv_path)
|
||||
media_id = wechat_api.media_create('file', batch_party_csv_path)['media_id']
|
||||
job_id = wechat_api.batch_replaceparty(media_id)['jobid']
|
||||
puts "running job_id: #{job_id}"
|
||||
puts wechat_api.batch_job_result(job_id)
|
||||
end
|
||||
|
||||
desc 'batch_syncuser [SYNC_USER_CSV_MEDIA_ID]', '增量更新成员'
|
||||
def batch_syncuser(sync_user_csv_media_id)
|
||||
puts wechat_api.batch_syncuser(sync_user_csv_media_id)
|
||||
end
|
||||
|
||||
desc 'batch_replaceuser [BATCH_USER_CSV_MEDIA_ID]', '全量覆盖成员'
|
||||
def batch_replaceuser(batch_user_csv_media_id)
|
||||
puts wechat_api.batch_replaceuser(batch_user_csv_media_id)
|
||||
end
|
||||
|
||||
desc 'upload_replaceuser [BATCH_USER_CSV_PATH]', '上传文件方式全量覆盖成员'
|
||||
def upload_replaceuser(batch_user_csv_path)
|
||||
media_id = wechat_api.media_create('file', batch_user_csv_path)['media_id']
|
||||
job_id = wechat_api.batch_replaceuser(media_id)['jobid']
|
||||
puts "running job_id: #{job_id}"
|
||||
puts wechat_api.batch_job_result(job_id)
|
||||
end
|
||||
|
||||
desc 'convert_to_openid [USER_ID]', 'userid转换成openid'
|
||||
def convert_to_openid(userid)
|
||||
puts wechat_api.convert_to_openid(userid)
|
||||
end
|
||||
|
||||
desc 'agent_list', '获取应用概况列表'
|
||||
def agent_list
|
||||
r = wechat_api.agent_list
|
||||
puts "errcode: #{r['errcode']} errmsg: #{r['errmsg']}"
|
||||
puts 'ag# name square_logo_url round_logo_url'
|
||||
r['agentlist'].sort_by { |d| d['agentid'] } .each do |i|
|
||||
puts format('%3d %s %s %s', i['agentid'], i['name'], i['square_logo_url'], i['round_logo_url'])
|
||||
end
|
||||
end
|
||||
|
||||
desc 'agent [AGENT_ID]', '获取企业号应用详情'
|
||||
def agent(agentid)
|
||||
r = wechat_api.agent(agentid)
|
||||
puts "agentid: #{r['agentid']} errcode: #{r['errcode']} errmsg: #{r['errmsg']}"
|
||||
puts "name: #{r['name']}"
|
||||
puts "description: #{r['description']}"
|
||||
puts " square_logo_url: #{r['square_logo_url']}"
|
||||
puts " round_logo_url: #{r['round_logo_url']}"
|
||||
puts "allow_userinfos: #{r['allow_userinfos']}"
|
||||
puts "allow_partys: #{r['allow_partys']}"
|
||||
puts "allow_tags: #{r['allow_tags']}"
|
||||
puts "close: #{r['close']} redirect_domain: #{r['redirect_domain']}"
|
||||
puts "report_location_flag: #{r['report_location_flag']} isreportuser: #{r['isreportuser']} isreportenter: #{r['isreportenter']}"
|
||||
end
|
||||
|
||||
desc 'message_send [OPENID, TEXT_MESSAGE]', '发送文字消息'
|
||||
def message_send(openid, text_message)
|
||||
puts wechat_api.message_send openid, text_message
|
||||
end
|
||||
else
|
||||
desc 'group_create [GROUP_NAME]', '创建分组'
|
||||
def group_create(group_name)
|
||||
puts wechat_api.group_create(group_name)
|
||||
end
|
||||
|
||||
desc 'groups', '查询所有分组'
|
||||
def groups
|
||||
puts wechat_api.groups
|
||||
end
|
||||
|
||||
desc 'user_group [OPEN_ID]', '查询用户所在分组'
|
||||
def user_group(openid)
|
||||
puts wechat_api.user_group(openid)
|
||||
end
|
||||
|
||||
desc 'group_update [GROUP_ID, NEW_GROUP_NAME]', '修改分组名'
|
||||
def group_update(groupid, new_group_name)
|
||||
puts wechat_api.group_update(groupid, new_group_name)
|
||||
end
|
||||
|
||||
desc 'user_change_group [OPEN_ID, TO_GROUP_ID]', '移动用户分组'
|
||||
def user_change_group(openid, to_groupid)
|
||||
puts wechat_api.user_change_group(openid, to_groupid)
|
||||
end
|
||||
|
||||
desc 'group_delete [GROUP_ID]', '删除分组'
|
||||
def group_delete(groupid)
|
||||
puts wechat_api.group_delete(groupid)
|
||||
end
|
||||
|
||||
desc 'users', '关注者列表'
|
||||
def users
|
||||
puts wechat_api.users
|
||||
end
|
||||
|
||||
desc 'qrcode_create_scene [SCENE_ID, EXPIRE_SECONDS]', '请求临时二维码'
|
||||
def qrcode_create_scene(scene_id, expire_seconds = 604800)
|
||||
puts wechat_api.qrcode_create_scene(scene_id, expire_seconds)
|
||||
end
|
||||
|
||||
desc 'qrcode_create_limit_scene [SCENE_ID_OR_STR]', '请求永久二维码'
|
||||
def qrcode_create_limit_scene(scene_id_or_str)
|
||||
puts wechat_api.qrcode_create_limit_scene(scene_id_or_str)
|
||||
end
|
||||
end
|
||||
|
||||
desc 'user [OPEN_ID]', '获取用户基本信息'
|
||||
def user(open_id)
|
||||
puts wechat_api.user(open_id)
|
||||
end
|
||||
|
||||
desc 'oauth2_url [REDIRECT_URI]', '生成OAuth2.0验证URL'
|
||||
def oauth2_url(redirect_uri)
|
||||
appid = Wechat.config.corpid || Wechat.config.appid
|
||||
puts wechat_api.oauth2_url(redirect_uri, appid)
|
||||
end
|
||||
|
||||
desc 'user_update_remark [OPEN_ID, REMARK]', '设置备注名'
|
||||
def user_update_remark(openid, remark)
|
||||
puts wechat_api.user_update_remark(openid, remark)
|
||||
end
|
||||
|
||||
desc 'menu', '当前菜单'
|
||||
def menu
|
||||
puts wechat_api.menu
|
||||
end
|
||||
|
||||
desc 'menu_delete', '删除菜单'
|
||||
def menu_delete
|
||||
puts 'Menu deleted' if wechat_api.menu_delete
|
||||
end
|
||||
|
||||
desc 'menu_create [MENU_YAML_PATH]', '创建菜单'
|
||||
def menu_create(menu_yaml_path)
|
||||
menu = YAML.load(File.read(menu_yaml_path))
|
||||
puts 'Menu created' if wechat_api.menu_create(menu)
|
||||
end
|
||||
|
||||
desc 'media [MEDIA_ID, PATH]', '媒体下载'
|
||||
def media(media_id, path)
|
||||
tmp_file = wechat_api.media(media_id)
|
||||
FileUtils.mv(tmp_file.path, path)
|
||||
puts 'File downloaded'
|
||||
end
|
||||
|
||||
desc 'media_create [MEDIA_TYPE, PATH]', '媒体上传'
|
||||
def media_create(type, path)
|
||||
puts wechat_api.media_create(type, path)
|
||||
end
|
||||
|
||||
desc 'material [MEDIA_ID, PATH]', '永久媒体下载'
|
||||
def material(media_id, path)
|
||||
tmp_file = wechat_api.material(media_id)
|
||||
FileUtils.mv(tmp_file.path, path)
|
||||
puts 'File downloaded'
|
||||
end
|
||||
|
||||
desc 'material_add [MEDIA_TYPE, PATH]', '永久媒体上传'
|
||||
def material_add(type, path)
|
||||
puts wechat_api.material_add(type, path)
|
||||
end
|
||||
|
||||
desc 'material_delete [MEDIA_ID]', '删除永久素材'
|
||||
def material_delete(media_id)
|
||||
puts wechat_api.material_delete(media_id)
|
||||
end
|
||||
|
||||
desc 'material_count', '获取永久素材总数'
|
||||
def material_count
|
||||
puts wechat_api.material_count
|
||||
end
|
||||
|
||||
desc 'material_list [TYPE, OFFSET, COUNT]', '获取永久素材列表'
|
||||
def material_list(type, offset, count)
|
||||
r = wechat_api.material_list(type, offset, count)
|
||||
if %w(image voice video file).include?(type)
|
||||
puts "errcode: #{r['errcode']} errmsg: #{r['errmsg']} total_count: #{r['total_count']} item_count: #{r['item_count']}"
|
||||
if wechat_api.is_a?(Wechat::CorpApi)
|
||||
r['itemlist'].each { |i| puts "#{i['media_id']} #{i['filename']} #{Time.at(i['update_time'].to_i)}" }
|
||||
else
|
||||
r['item'].each { |i| puts "#{i['media_id']} #{i['name']} #{Time.at(i['update_time'].to_i)}" }
|
||||
end
|
||||
else
|
||||
puts r
|
||||
end
|
||||
end
|
||||
|
||||
desc 'custom_text [OPENID, TEXT_MESSAGE]', '发送文字客服消息'
|
||||
def custom_text(openid, text_message)
|
||||
puts wechat_api.custom_message_send Wechat::Message.to(openid).text(text_message)
|
||||
end
|
||||
|
||||
desc 'custom_image [OPENID, IMAGE_PATH]', '发送图片客服消息'
|
||||
def custom_image(openid, image_path)
|
||||
api = wechat_api
|
||||
media_id = api.media_create('image', image_path)['media_id']
|
||||
puts api.custom_message_send Wechat::Message.to(openid).image(media_id)
|
||||
end
|
||||
|
||||
desc 'custom_voice [OPENID, VOICE_PATH]', '发送语音客服消息'
|
||||
def custom_voice(openid, voice_path)
|
||||
api = wechat_api
|
||||
media_id = api.media_create('voice', voice_path)['media_id']
|
||||
puts api.custom_message_send Wechat::Message.to(openid).voice(media_id)
|
||||
end
|
||||
|
||||
desc 'custom_video [OPENID, VIDEO_PATH]', '发送视频客服消息'
|
||||
method_option :title, aliases: '-h', desc: '视频标题'
|
||||
method_option :description, aliases: '-d', desc: '视频描述'
|
||||
def custom_video(openid, video_path)
|
||||
api = wechat_api
|
||||
api_opts = options.slice(:title, :description)
|
||||
media_id = api.media_create('video', video_path)['media_id']
|
||||
puts api.custom_message_send Wechat::Message.to(openid).video(media_id, api_opts)
|
||||
end
|
||||
|
||||
desc 'custom_music [OPENID, THUMBNAIL_PATH, MUSIC_URL]', '发送音乐客服消息'
|
||||
method_option :title, aliases: '-h', desc: '音乐标题'
|
||||
method_option :description, aliases: '-d', desc: '音乐描述'
|
||||
method_option :HQ_music_url, aliases: '-u', desc: '高质量音乐URL链接'
|
||||
def custom_music(openid, thumbnail_path, music_url)
|
||||
api = wechat_api
|
||||
api_opts = options.slice(:title, :description, :HQ_music_url)
|
||||
thumb_media_id = api.media_create('thumb', thumbnail_path)['thumb_media_id']
|
||||
puts api.custom_message_send Wechat::Message.to(openid).music(thumb_media_id, music_url, api_opts)
|
||||
end
|
||||
|
||||
desc 'custom_news [OPENID, NEWS_YAML_PATH]', '发送图文客服消息'
|
||||
def custom_news(openid, news_yaml_path)
|
||||
articles = YAML.load(File.read(news_yaml_path))
|
||||
puts wechat_api.custom_message_send Wechat::Message.to(openid).news(articles['articles'])
|
||||
end
|
||||
|
||||
desc 'template_message [OPENID, TEMPLATE_YAML_PATH]', '模板消息接口'
|
||||
def template_message(openid, template_yaml_path)
|
||||
template = YAML.load(File.read(template_yaml_path))
|
||||
puts wechat_api.template_message_send Wechat::Message.to(openid).template(template['template'])
|
||||
end
|
||||
end
|
||||
|
||||
App.start
|
|
@ -0,0 +1,37 @@
|
|||
module ActionController
|
||||
module WechatResponder
|
||||
def wechat_responder(opts = {})
|
||||
include Wechat::Responder
|
||||
|
||||
self.corpid = opts[:corpid] || Wechat.config.corpid
|
||||
self.agentid = opts[:agentid] || Wechat.config.agentid
|
||||
self.encrypt_mode = opts[:encrypt_mode] || Wechat.config.encrypt_mode || corpid.present?
|
||||
self.timeout = opts[:timeout] || 20
|
||||
self.skip_verify_ssl = opts[:skip_verify_ssl]
|
||||
self.token = opts[:token] || Wechat.config.token
|
||||
self.encoding_aes_key = opts[:encoding_aes_key] || Wechat.config.encoding_aes_key
|
||||
|
||||
if opts.empty?
|
||||
self.wechat = Wechat.api
|
||||
else
|
||||
if corpid.present?
|
||||
self.wechat = Wechat::CorpApi.new(corpid, opts[:corpsecret], opts[:access_token], agentid, timeout, skip_verify_ssl, opts[:jsapi_ticket])
|
||||
else
|
||||
self.wechat = Wechat::Api.new(opts[:appid], opts[:secret], opts[:access_token], timeout, skip_verify_ssl, opts[:jsapi_ticket])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if defined? Base
|
||||
class << Base
|
||||
include WechatResponder
|
||||
end
|
||||
end
|
||||
|
||||
if defined? API
|
||||
class << API
|
||||
include WechatResponder
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,32 @@
|
|||
require 'rails/generators/active_record'
|
||||
|
||||
module Wechat
|
||||
module Generators
|
||||
class InstallGenerator < Rails::Generators::Base
|
||||
include ::Rails::Generators::Migration
|
||||
|
||||
desc 'Install Wechat support files'
|
||||
source_root File.expand_path('../templates', __FILE__)
|
||||
|
||||
def copy_config
|
||||
template 'config/wechat.yml'
|
||||
end
|
||||
|
||||
def add_wechat_route
|
||||
route 'resource :wechat, only: [:show, :create]'
|
||||
end
|
||||
|
||||
def copy_wechat_controller
|
||||
template 'app/controllers/wechats_controller.rb'
|
||||
end
|
||||
|
||||
def copy_model_migration
|
||||
migration_template 'db/migration.rb', 'db/migrate/create_wechat_logs.rb'
|
||||
end
|
||||
|
||||
def self.next_migration_number(dirname)
|
||||
::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,123 @@
|
|||
<% if defined? ActionController::API -%>
|
||||
class WechatsController < ApplicationController
|
||||
<% else -%>
|
||||
class WechatsController < ActionController::Base
|
||||
<% end -%>
|
||||
wechat_responder
|
||||
|
||||
# default text responder when no other match
|
||||
on :text do |request, content|
|
||||
request.reply.text "echo: #{content}" # Just echo
|
||||
end
|
||||
|
||||
# When receive 'help', will trigger this responder
|
||||
on :text, with: 'help' do |request|
|
||||
request.reply.text 'help content'
|
||||
end
|
||||
|
||||
# When receive '<n>news', will match and will got count as <n> as parameter
|
||||
on :text, with: /^(\d+) news$/ do |request, count|
|
||||
# Wechat article can only contain max 10 items, large than 10 will dropped.
|
||||
news = (1..count.to_i).each_with_object([]) { |n, memo| memo << { title: 'News title', content: "No. #{n} news content" } }
|
||||
request.reply.news(news) do |article, n, index| # article is return object
|
||||
article.item title: "#{index} #{n[:title]}", description: n[:content], pic_url: 'http://www.baidu.com/img/bdlogo.gif', url: 'http://www.baidu.com/'
|
||||
end
|
||||
end
|
||||
|
||||
on :event, with: 'subscribe' do |request|
|
||||
request.reply.text "#{request[:FromUserName]} subscribe now"
|
||||
end
|
||||
|
||||
# When unsubscribe user scan qrcode qrscene_xxxxxx to subscribe in public account
|
||||
# notice user will subscribe public account at same time, so wechat won't trigger subscribe event any more
|
||||
on :scan, with: 'qrscene_xxxxxx' do |request, ticket|
|
||||
request.reply.text "Unsubscribe user #{request[:FromUserName]} Ticket #{ticket}"
|
||||
end
|
||||
|
||||
# When subscribe user scan scene_id in public account
|
||||
on :scan, with: 'scene_id' do |request, ticket|
|
||||
request.reply.text "Subscribe user #{request[:FromUserName]} Ticket #{ticket}"
|
||||
end
|
||||
|
||||
# When no any on :scan responder can match subscribe user scaned scene_id
|
||||
on :event, with: 'scan' do |request|
|
||||
if request[:EventKey].present?
|
||||
request.reply.text "event scan got EventKey #{request[:EventKey]} Ticket #{request[:Ticket]}"
|
||||
end
|
||||
end
|
||||
|
||||
# When enterprise user press menu BINDING_QR_CODE and success to scan bar code
|
||||
on :scan, with: 'BINDING_QR_CODE' do |request, scan_result, scan_type|
|
||||
request.reply.text "User #{request[:FromUserName]} ScanResult #{scan_result} ScanType #{scan_type}"
|
||||
end
|
||||
|
||||
# Except QR code, wechat can also scan CODE_39 bar code in enterprise account
|
||||
on :scan, with: 'BINDING_BARCODE' do |message, scan_result|
|
||||
if scan_result.start_with? 'CODE_39,'
|
||||
message.reply.text "User: #{message[:FromUserName]} scan barcode, result is #{scan_result.split(',')[1]}"
|
||||
end
|
||||
end
|
||||
|
||||
# When user click the menu button
|
||||
on :click, with: 'BOOK_LUNCH' do |request, key|
|
||||
request.reply.text "User: #{request[:FromUserName]} click #{key}"
|
||||
end
|
||||
|
||||
# When user view URL in the menu button
|
||||
on :view, with: 'http://wechat.somewhere.com/view_url' do |request, view|
|
||||
request.reply.text "#{request[:FromUserName]} view #{view}"
|
||||
end
|
||||
|
||||
# When user sent the imsage
|
||||
on :image do |request|
|
||||
request.reply.image(request[:MediaId]) # Echo the sent image to user
|
||||
end
|
||||
|
||||
# When user sent the voice
|
||||
on :voice do |request|
|
||||
request.reply.voice(request[:MediaId]) # Echo the sent voice to user
|
||||
end
|
||||
|
||||
# When user sent the video
|
||||
on :video do |request|
|
||||
nickname = wechat.user(request[:FromUserName])['nickname'] # Call wechat api to get sender nickname
|
||||
request.reply.video(request[:MediaId], title: 'Echo', description: "Got #{nickname} sent video") # Echo the sent video to user
|
||||
end
|
||||
|
||||
# When user sent location
|
||||
on :location do |request|
|
||||
request.reply.text("Latitude: #{message[:Latitude]} Longitude: #{message[:Longitude]} Precision: #{message[:Precision]}")
|
||||
end
|
||||
|
||||
on :event, with: 'unsubscribe' do |request|
|
||||
request.reply.success # user can not receive this message
|
||||
end
|
||||
|
||||
# When user enter the app / agent app
|
||||
on :event, with: 'enter_agent' do |request|
|
||||
request.reply.text "#{request[:FromUserName]} enter agent app now"
|
||||
end
|
||||
|
||||
# When batch job create/update user (incremental) finished.
|
||||
on :batch_job, with: 'sync_user' do |request, batch_job|
|
||||
request.reply.text "sync_user job #{batch_job[:JobId]} finished, return code #{batch_job[:ErrCode]}, return message #{batch_job[:ErrMsg]}"
|
||||
end
|
||||
|
||||
# When batch job replace user (full sync) finished.
|
||||
on :batch_job, with: 'replace_user' do |request, batch_job|
|
||||
request.reply.text "replace_user job #{batch_job[:JobId]} finished, return code #{batch_job[:ErrCode]}, return message #{batch_job[:ErrMsg]}"
|
||||
end
|
||||
|
||||
# When batch job invent user finished.
|
||||
on :batch_job, with: 'invite_user' do |request, batch_job|
|
||||
request.reply.text "invite_user job #{batch_job[:JobId]} finished, return code #{batch_job[:ErrCode]}, return message #{batch_job[:ErrMsg]}"
|
||||
end
|
||||
|
||||
# When batch job replace department (full sync) finished.
|
||||
on :batch_job, with: 'replace_party' do |request, batch_job|
|
||||
request.reply.text "replace_party job #{batch_job[:JobId]} finished, return code #{batch_job[:ErrCode]}, return message #{batch_job[:ErrMsg]}"
|
||||
end
|
||||
|
||||
# Any not match above will fail to below
|
||||
on :fallback, respond: 'fallback message'
|
||||
end
|
|
@ -0,0 +1,33 @@
|
|||
default: &default
|
||||
corpid: "corpid"
|
||||
corpsecret: "corpsecret"
|
||||
agentid: 1
|
||||
# Or if using public account, only need above two line
|
||||
# appid: "my_appid"
|
||||
# secret: "my_secret"
|
||||
token: "my_token"
|
||||
access_token: "C:/Users/[username]/wechat_access_token"
|
||||
encrypt_mode: false # if true must fill encoding_aes_key
|
||||
encoding_aes_key: "my_encoding_aes_key"
|
||||
jsapi_ticket: "C:/Users/[user_name]/wechat_jsapi_ticket"
|
||||
|
||||
production:
|
||||
corpid: <%%= ENV['WECHAT_CORPID'] %>
|
||||
corpsecret: <%%= ENV['WECHAT_CORPSECRET'] %>
|
||||
agentid: <%%= ENV['WECHAT_AGENTID'] %>
|
||||
# Or if using public account, only need above two line
|
||||
# appid: <%= ENV['WECHAT_APPID'] %>
|
||||
# secret: <%= ENV['WECHAT_APP_SECRET'] %>
|
||||
token: <%%= ENV['WECHAT_TOKEN'] %>
|
||||
timeout: 30,
|
||||
skip_verify_ssl: true
|
||||
access_token: <%%= ENV['WECHAT_ACCESS_TOKEN'] %>
|
||||
encrypt_mode: false # if true must fill encoding_aes_key
|
||||
encoding_aes_key: <%%= ENV['WECHAT_ENCODING_AES_KEY'] %>
|
||||
jsapi_ticket: <%= ENV['WECHAT_JSAPI_TICKET'] %>
|
||||
|
||||
development:
|
||||
<<: *default
|
||||
|
||||
test:
|
||||
<<: *default
|
|
@ -0,0 +1,11 @@
|
|||
class CreateWechatLogs < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :wechat_logs do |t|
|
||||
t.string :openid, null: false, index: true
|
||||
t.text :request_raw
|
||||
t.text :response_raw
|
||||
t.text :session_raw
|
||||
t.datetime :created_at, null: false
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,28 @@
|
|||
require 'wechat/api_loader'
|
||||
require 'wechat/api'
|
||||
require 'wechat/corp_api'
|
||||
require 'action_controller/wechat_responder'
|
||||
|
||||
module Wechat
|
||||
autoload :Message, 'wechat/message'
|
||||
autoload :Responder, 'wechat/responder'
|
||||
autoload :Cipher, 'wechat/cipher'
|
||||
autoload :WechatLog, 'wechat/wechat_log'
|
||||
|
||||
class AccessTokenExpiredError < StandardError; end
|
||||
class ResponseError < StandardError
|
||||
attr_reader :error_code
|
||||
def initialize(errcode, errmsg)
|
||||
@error_code = errcode
|
||||
super "#{errmsg}(#{error_code})"
|
||||
end
|
||||
end
|
||||
|
||||
def self.config
|
||||
ApiLoader.config
|
||||
end
|
||||
|
||||
def self.api
|
||||
@wechat_api ||= ApiLoader.with({})
|
||||
end
|
||||
end
|
|
@ -0,0 +1,124 @@
|
|||
require 'wechat/api_base'
|
||||
require 'wechat/client'
|
||||
require 'wechat/token/public_access_token'
|
||||
require 'wechat/ticket/public_jsapi_ticket'
|
||||
|
||||
module Wechat
|
||||
class Api < ApiBase
|
||||
API_BASE = 'https://api.weixin.qq.com/cgi-bin/'
|
||||
OAUTH2_BASE = 'https://api.weixin.qq.com/sns/oauth2/'
|
||||
|
||||
def initialize(appid, secret, token_file, timeout, skip_verify_ssl, jsapi_ticket_file)
|
||||
@client = Client.new(API_BASE, timeout, skip_verify_ssl)
|
||||
@access_token = Token::PublicAccessToken.new(@client, appid, secret, token_file)
|
||||
@jsapi_ticket = Ticket::PublicJsapiTicket.new(@client, @access_token, jsapi_ticket_file)
|
||||
end
|
||||
|
||||
def groups
|
||||
get 'groups/get'
|
||||
end
|
||||
|
||||
def group_create(group_name)
|
||||
post 'groups/create', JSON.generate(group: { name: group_name })
|
||||
end
|
||||
|
||||
def group_update(groupid, new_group_name)
|
||||
post 'groups/update', JSON.generate(group: { id: groupid, name: new_group_name })
|
||||
end
|
||||
|
||||
def group_delete(groupid)
|
||||
post 'groups/delete', JSON.generate(group: { id: groupid })
|
||||
end
|
||||
|
||||
def users(nextid = nil)
|
||||
params = { params: { next_openid: nextid } } if nextid.present?
|
||||
get('user/get', params || {})
|
||||
end
|
||||
|
||||
def user(openid)
|
||||
get 'user/info', params: { openid: openid }
|
||||
end
|
||||
|
||||
def user_group(openid)
|
||||
post 'groups/getid', JSON.generate(openid: openid)
|
||||
end
|
||||
|
||||
def user_change_group(openid, to_groupid)
|
||||
post 'groups/members/update', JSON.generate(openid: openid, to_groupid: to_groupid)
|
||||
end
|
||||
|
||||
def user_update_remark(openid, remark)
|
||||
post 'user/info/updateremark', JSON.generate(openid: openid, remark: remark)
|
||||
end
|
||||
|
||||
def qrcode_create_scene(scene_id, expire_seconds = 604800)
|
||||
post 'qrcode/create', JSON.generate(expire_seconds: expire_seconds,
|
||||
action_name: 'QR_SCENE',
|
||||
action_info: { scene: { scene_id: scene_id } })
|
||||
end
|
||||
|
||||
def qrcode_create_limit_scene(scene_id_or_str)
|
||||
case scene_id_or_str
|
||||
when Fixnum
|
||||
post 'qrcode/create', JSON.generate(action_name: 'QR_LIMIT_SCENE',
|
||||
action_info: { scene: { scene_id: scene_id_or_str } })
|
||||
else
|
||||
post 'qrcode/create', JSON.generate(action_name: 'QR_LIMIT_STR_SCENE',
|
||||
action_info: { scene: { scene_str: scene_id_or_str } })
|
||||
end
|
||||
end
|
||||
|
||||
def menu
|
||||
get 'menu/get'
|
||||
end
|
||||
|
||||
def menu_delete
|
||||
get 'menu/delete'
|
||||
end
|
||||
|
||||
def menu_create(menu)
|
||||
# 微信不接受7bit escaped json(eg \uxxxx), 中文必须UTF-8编码, 这可能是个安全漏洞
|
||||
post 'menu/create', JSON.generate(menu)
|
||||
end
|
||||
|
||||
def material(media_id)
|
||||
get 'material/get', params: { media_id: media_id }, as: :file
|
||||
end
|
||||
|
||||
def material_count
|
||||
get 'material/get_materialcount'
|
||||
end
|
||||
|
||||
def material_list(type, offset, count)
|
||||
post 'material/batchget_material', JSON.generate(type: type, offset: offset, count: count)
|
||||
end
|
||||
|
||||
def material_add(type, file)
|
||||
post_file 'material/add_material', file, params: { type: type }
|
||||
end
|
||||
|
||||
def material_delete(media_id)
|
||||
post 'material/del_material', media_id: media_id
|
||||
end
|
||||
|
||||
def custom_message_send(message)
|
||||
post 'message/custom/send', message.to_json, content_type: :json
|
||||
end
|
||||
|
||||
def template_message_send(message)
|
||||
post 'message/template/send', message.to_json, content_type: :json
|
||||
end
|
||||
|
||||
# http://mp.weixin.qq.com/wiki/17/c0f37d5704f0b64713d5d2c37b468d75.html
|
||||
# 第二步:通过code换取网页授权access_token
|
||||
def web_access_token(code)
|
||||
params = {
|
||||
appid: access_token.appid,
|
||||
secret: access_token.secret,
|
||||
code: code,
|
||||
grant_type: 'authorization_code'
|
||||
}
|
||||
get 'access_token', params: params, base: OAUTH2_BASE
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,51 @@
|
|||
module Wechat
|
||||
class ApiBase
|
||||
attr_reader :access_token, :client, :jsapi_ticket
|
||||
|
||||
MP_BASE = 'https://mp.weixin.qq.com/cgi-bin/'
|
||||
|
||||
def callbackip
|
||||
get 'getcallbackip'
|
||||
end
|
||||
|
||||
def qrcode(ticket)
|
||||
client.get 'showqrcode', params: { ticket: ticket }, base: MP_BASE, as: :file
|
||||
end
|
||||
|
||||
def media(media_id)
|
||||
get 'media/get', params: { media_id: media_id }, as: :file
|
||||
end
|
||||
|
||||
def media_create(type, file)
|
||||
post_file 'media/upload', file, params: { type: type }
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def get(path, headers = {})
|
||||
with_access_token(headers[:params]) do |params|
|
||||
client.get path, headers.merge(params: params)
|
||||
end
|
||||
end
|
||||
|
||||
def post(path, payload, headers = {})
|
||||
with_access_token(headers[:params]) do |params|
|
||||
client.post path, payload, headers.merge(params: params)
|
||||
end
|
||||
end
|
||||
|
||||
def post_file(path, file, headers = {})
|
||||
with_access_token(headers[:params]) do |params|
|
||||
client.post_file path, file, headers.merge(params: params)
|
||||
end
|
||||
end
|
||||
|
||||
def with_access_token(params = {}, tries = 2)
|
||||
params ||= {}
|
||||
yield(params.merge(access_token: access_token.token))
|
||||
rescue AccessTokenExpiredError
|
||||
access_token.refresh
|
||||
retry unless (tries -= 1).zero?
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,79 @@
|
|||
module Wechat
|
||||
module ApiLoader
|
||||
def self.with(options)
|
||||
c = ApiLoader.config
|
||||
|
||||
token_file = options[:token_file] || c.access_token || '/var/tmp/wechat_access_token'
|
||||
js_token_file = options[:js_token_file] || c.jsapi_ticket || '/var/tmp/wechat_jsapi_ticket'
|
||||
|
||||
if c.appid && c.secret && token_file.present?
|
||||
Wechat::Api.new(c.appid, c.secret, token_file, c.timeout, c.skip_verify_ssl, js_token_file)
|
||||
elsif c.corpid && c.corpsecret && token_file.present?
|
||||
Wechat::CorpApi.new(c.corpid, c.corpsecret, token_file, c.agentid, c.timeout, c.skip_verify_ssl, js_token_file)
|
||||
else
|
||||
puts <<-HELP
|
||||
Need create ~/.wechat.yml with wechat appid and secret
|
||||
or running at rails root folder so wechat can read config/wechat.yml
|
||||
HELP
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
@config = nil
|
||||
|
||||
def self.config
|
||||
return @config unless @config.nil?
|
||||
@config ||= loading_config!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.loading_config!
|
||||
config ||= config_from_file || config_from_environment
|
||||
|
||||
if defined?(::Rails)
|
||||
config[:access_token] ||= Rails.root.join('tmp/access_token').to_s
|
||||
config[:jsapi_ticket] ||= Rails.root.join('tmp/jsapi_ticket').to_s
|
||||
end
|
||||
config[:timeout] ||= 20
|
||||
config.symbolize_keys!
|
||||
@config = OpenStruct.new(config)
|
||||
end
|
||||
|
||||
def self.config_from_file
|
||||
if defined?(::Rails)
|
||||
config_file = Rails.root.join('config/wechat.yml')
|
||||
return YAML.load(ERB.new(File.read(config_file)).result)[Rails.env] if File.exist?(config_file)
|
||||
else
|
||||
rails_config_file = File.join(Dir.getwd, 'config/wechat.yml')
|
||||
home_config_file = File.join(Dir.home, '.wechat.yml')
|
||||
if File.exist?(rails_config_file)
|
||||
rails_env = ENV['RAILS_ENV'] || 'default'
|
||||
config = YAML.load(ERB.new(File.read(rails_config_file)).result)[rails_env]
|
||||
if config.present? && (config['appid'] || config['corpid'])
|
||||
puts "Using rails project config/wechat.yml #{rails_env} setting..."
|
||||
return config
|
||||
end
|
||||
end
|
||||
if File.exist?(home_config_file)
|
||||
return YAML.load ERB.new(File.read(home_config_file)).result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.config_from_environment
|
||||
{ appid: ENV['WECHAT_APPID'],
|
||||
secret: ENV['WECHAT_SECRET'],
|
||||
corpid: ENV['WECHAT_CORPID'],
|
||||
corpsecret: ENV['WECHAT_CORPSECRET'],
|
||||
agentid: ENV['WECHAT_AGENTID'],
|
||||
token: ENV['WECHAT_TOKEN'],
|
||||
access_token: ENV['WECHAT_ACCESS_TOKEN'],
|
||||
encrypt_mode: ENV['WECHAT_ENCRYPT_MODE'],
|
||||
timeout: ENV['WECHAT_TIMEOUT'],
|
||||
skip_verify_ssl: ENV['WECHAT_SKIP_VERIFY_SSL'],
|
||||
encoding_aes_key: ENV['WECHAT_ENCODING_AES_KEY'],
|
||||
jsapi_ticket: ENV['WECHAT_JSAPI_TICKET'] }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,72 @@
|
|||
require 'openssl/cipher'
|
||||
require 'securerandom'
|
||||
require 'base64'
|
||||
|
||||
module Wechat
|
||||
module Cipher
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
BLOCK_SIZE = 32
|
||||
CIPHER = 'AES-256-CBC'
|
||||
|
||||
def encrypt(plain, encoding_aes_key)
|
||||
cipher = OpenSSL::Cipher.new(CIPHER)
|
||||
cipher.encrypt
|
||||
|
||||
cipher.padding = 0
|
||||
key_data = Base64.decode64(encoding_aes_key + '=')
|
||||
cipher.key = key_data
|
||||
cipher.iv = key_data[0..16]
|
||||
|
||||
cipher.update(plain) + cipher.final
|
||||
end
|
||||
|
||||
def decrypt(msg, encoding_aes_key)
|
||||
cipher = OpenSSL::Cipher.new(CIPHER)
|
||||
cipher.decrypt
|
||||
|
||||
cipher.padding = 0
|
||||
key_data = Base64.decode64(encoding_aes_key + '=')
|
||||
cipher.key = key_data
|
||||
cipher.iv = key_data[0..16]
|
||||
|
||||
plain = cipher.update(msg) + cipher.final
|
||||
decode_padding(plain)
|
||||
end
|
||||
|
||||
# app_id or corp_id
|
||||
def pack(content, app_id)
|
||||
random = SecureRandom.hex(8)
|
||||
text = content.force_encoding('ASCII-8BIT')
|
||||
msg_len = [text.length].pack('N')
|
||||
|
||||
encode_padding("#{random}#{msg_len}#{text}#{app_id}")
|
||||
end
|
||||
|
||||
def unpack(msg)
|
||||
msg = decode_padding(msg)
|
||||
msg_len = msg[16, 4].reverse.unpack('V')[0]
|
||||
content = msg[20, msg_len]
|
||||
app_id = msg[(20 + msg_len)..-1]
|
||||
|
||||
[content, app_id]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def encode_padding(data)
|
||||
length = data.bytes.length
|
||||
amount_to_pad = BLOCK_SIZE - (length % BLOCK_SIZE)
|
||||
amount_to_pad = BLOCK_SIZE if amount_to_pad == 0
|
||||
padding = ([amount_to_pad].pack('c') * amount_to_pad)
|
||||
data + padding
|
||||
end
|
||||
|
||||
def decode_padding(plain)
|
||||
pad = plain.bytes[-1]
|
||||
# no padding
|
||||
pad = 0 if pad < 1 || pad > BLOCK_SIZE
|
||||
plain[0...(plain.length - pad)]
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,90 @@
|
|||
require 'http'
|
||||
|
||||
module Wechat
|
||||
class Client
|
||||
attr_reader :base, :ssl_context
|
||||
|
||||
def initialize(base, timeout, skip_verify_ssl)
|
||||
@base = base
|
||||
HTTP.timeout(:global, write: timeout, connect: timeout, read: timeout)
|
||||
@ssl_context = OpenSSL::SSL::SSLContext.new
|
||||
@ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE if skip_verify_ssl
|
||||
end
|
||||
|
||||
def get(path, get_header = {})
|
||||
request(path, get_header) do |url, header|
|
||||
params = header.delete(:params)
|
||||
HTTP.headers(header).get(url, params: params, ssl_context: ssl_context)
|
||||
end
|
||||
end
|
||||
|
||||
def post(path, payload, post_header = {})
|
||||
request(path, post_header) do |url, header|
|
||||
params = header.delete(:params)
|
||||
HTTP.headers(header).post(url, params: params, body: payload, ssl_context: ssl_context)
|
||||
end
|
||||
end
|
||||
|
||||
def post_file(path, file, post_header = {})
|
||||
request(path, post_header) do |url, header|
|
||||
params = header.delete(:params)
|
||||
HTTP.headers(header)
|
||||
.post(url, params: params,
|
||||
form: { media: HTTP::FormData::File.new(file),
|
||||
hack: 'X' }, # Existing here for http-form_data 1.0.1 handle single param improperly
|
||||
ssl_context: ssl_context)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def request(path, header = {}, &_block)
|
||||
url = "#{header.delete(:base) || base}#{path}"
|
||||
as = header.delete(:as)
|
||||
header.merge!('Accept' => 'application/json')
|
||||
response = yield(url, header)
|
||||
|
||||
fail "Request not OK, response status #{response.status}" if response.status != 200
|
||||
parse_response(response, as || :json) do |parse_as, data|
|
||||
break data unless parse_as == :json && data['errcode'].present?
|
||||
|
||||
case data['errcode']
|
||||
when 0 # for request didn't expect results
|
||||
data
|
||||
# 42001: access_token timeout
|
||||
# 40014: invalid access_token
|
||||
# 40001, invalid credential, access_token is invalid or not latest hint
|
||||
# 48001, api unauthorized hint, for qrcode creation # 71
|
||||
when 42001, 40014, 40001, 48001
|
||||
fail AccessTokenExpiredError
|
||||
else
|
||||
fail ResponseError.new(data['errcode'], data['errmsg'])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def parse_response(response, as)
|
||||
content_type = response.headers[:content_type]
|
||||
parse_as = {
|
||||
%r{^application\/json} => :json,
|
||||
%r{^image\/.*} => :file
|
||||
}.each_with_object([]) { |match, memo| memo << match[1] if content_type =~ match[0] }.first || as || :text
|
||||
|
||||
case parse_as
|
||||
when :file
|
||||
file = Tempfile.new('tmp')
|
||||
file.binmode
|
||||
file.write(response.body)
|
||||
file.close
|
||||
data = file
|
||||
|
||||
when :json
|
||||
data = JSON.parse response.body.to_s.gsub(/[\u0000-\u001f]+/, '')
|
||||
else
|
||||
data = response.body
|
||||
end
|
||||
|
||||
yield(parse_as, data)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,166 @@
|
|||
require 'wechat/api_base'
|
||||
require 'wechat/client'
|
||||
require 'wechat/token/corp_access_token'
|
||||
require 'wechat/ticket/corp_jsapi_ticket'
|
||||
require 'cgi'
|
||||
|
||||
module Wechat
|
||||
class CorpApi < ApiBase
|
||||
attr_reader :agentid
|
||||
|
||||
API_BASE = 'https://qyapi.weixin.qq.com/cgi-bin/'
|
||||
|
||||
def initialize(appid, secret, token_file, agentid, timeout, skip_verify_ssl, jsapi_ticket_file)
|
||||
@client = Client.new(API_BASE, timeout, skip_verify_ssl)
|
||||
@access_token = Token::CorpAccessToken.new(@client, appid, secret, token_file)
|
||||
@agentid = agentid
|
||||
@jsapi_ticket = Ticket::CorpJsapiTicket.new(@client, @access_token, jsapi_ticket_file)
|
||||
end
|
||||
|
||||
def agent_list
|
||||
get 'agent/list'
|
||||
end
|
||||
|
||||
def agent(agentid)
|
||||
get 'agent/get', params: { agentid: agentid }
|
||||
end
|
||||
|
||||
def user(userid)
|
||||
get 'user/get', params: { userid: userid }
|
||||
end
|
||||
|
||||
def getuserinfo(code)
|
||||
get 'user/getuserinfo', params: { code: code }
|
||||
end
|
||||
|
||||
def oauth2_url(redirect_uri, appid)
|
||||
redirect_uri = CGI.escape(redirect_uri)
|
||||
"https://open.weixin.qq.com/connect/oauth2/authorize?appid=#{appid}&redirect_uri=#{redirect_uri}&response_type=code&scope=snsapi_base#wechat_redirect"
|
||||
end
|
||||
|
||||
def convert_to_openid(userid)
|
||||
post 'user/convert_to_openid', JSON.generate(userid: userid, agentid: agentid)
|
||||
end
|
||||
|
||||
def invite_user(userid)
|
||||
post 'invite/send', JSON.generate(userid: userid)
|
||||
end
|
||||
|
||||
def user_auth_success(userid)
|
||||
get 'user/authsucc', params: { userid: userid }
|
||||
end
|
||||
|
||||
def user_delete(userid)
|
||||
get 'user/delete', params: { userid: userid }
|
||||
end
|
||||
|
||||
def user_batchdelete(useridlist)
|
||||
post 'user/batchdelete', JSON.generate(useridlist: useridlist)
|
||||
end
|
||||
|
||||
def batch_job_result(jobid)
|
||||
get 'batch/getresult', params: { jobid: jobid }
|
||||
end
|
||||
|
||||
def batch_replaceparty(media_id)
|
||||
post 'batch/replaceparty', JSON.generate(media_id: media_id)
|
||||
end
|
||||
|
||||
def batch_syncuser(media_id)
|
||||
post 'batch/syncuser', JSON.generate(media_id: media_id)
|
||||
end
|
||||
|
||||
def batch_replaceuser(media_id)
|
||||
post 'batch/replaceuser', JSON.generate(media_id: media_id)
|
||||
end
|
||||
|
||||
def department_create(name, parentid)
|
||||
post 'department/create', JSON.generate(name: name, parentid: parentid)
|
||||
end
|
||||
|
||||
def department_delete(departmentid)
|
||||
get 'department/delete', params: { id: departmentid }
|
||||
end
|
||||
|
||||
def department_update(departmentid, name = nil, parentid = nil, order = nil)
|
||||
post 'department/update', JSON.generate({ id: departmentid, name: name, parentid: parentid, order: order }.reject { |_k, v| v.nil? })
|
||||
end
|
||||
|
||||
def department(departmentid = 1)
|
||||
get 'department/list', params: { id: departmentid }
|
||||
end
|
||||
|
||||
def user_simplelist(department_id, fetch_child = 0, status = 0)
|
||||
get 'user/simplelist', params: { department_id: department_id, fetch_child: fetch_child, status: status }
|
||||
end
|
||||
|
||||
def user_list(department_id, fetch_child = 0, status = 0)
|
||||
get 'user/list', params: { department_id: department_id, fetch_child: fetch_child, status: status }
|
||||
end
|
||||
|
||||
def tag_create(tagname, tagid = nil)
|
||||
post 'tag/create', JSON.generate(tagname: tagname, tagid: tagid)
|
||||
end
|
||||
|
||||
def tag_update(tagid, tagname)
|
||||
post 'tag/update', JSON.generate(tagid: tagid, tagname: tagname)
|
||||
end
|
||||
|
||||
def tag_delete(tagid)
|
||||
get 'tag/delete', params: { tagid: tagid }
|
||||
end
|
||||
|
||||
def tags
|
||||
get 'tag/list'
|
||||
end
|
||||
|
||||
def tag(tagid)
|
||||
get 'tag/get', params: { tagid: tagid }
|
||||
end
|
||||
|
||||
def tag_add_user(tagid, userids = nil, departmentids = nil)
|
||||
post 'tag/addtagusers', JSON.generate(tagid: tagid, userlist: userids, partylist: departmentids)
|
||||
end
|
||||
|
||||
def tag_del_user(tagid, userids = nil, departmentids = nil)
|
||||
post 'tag/deltagusers', JSON.generate(tagid: tagid, userlist: userids, partylist: departmentids)
|
||||
end
|
||||
|
||||
def menu
|
||||
get 'menu/get', params: { agentid: agentid }
|
||||
end
|
||||
|
||||
def menu_delete
|
||||
get 'menu/delete', params: { agentid: agentid }
|
||||
end
|
||||
|
||||
def menu_create(menu)
|
||||
# 微信不接受7bit escaped json(eg \uxxxx), 中文必须UTF-8编码, 这可能是个安全漏洞
|
||||
post 'menu/create', JSON.generate(menu), params: { agentid: agentid }
|
||||
end
|
||||
|
||||
def material_count
|
||||
get 'material/get_count', params: { agentid: agentid }
|
||||
end
|
||||
|
||||
def material_list(type, offset, count)
|
||||
post 'material/batchget', JSON.generate(type: type, agentid: agentid, offset: offset, count: count)
|
||||
end
|
||||
|
||||
def material(media_id)
|
||||
get 'material/get', params: { media_id: media_id, agentid: agentid }, as: :file
|
||||
end
|
||||
|
||||
def material_add(type, file)
|
||||
post_file 'material/add_material', file, params: { type: type, agentid: agentid }
|
||||
end
|
||||
|
||||
def material_delete(media_id)
|
||||
get 'material/del', params: { media_id: media_id, agentid: agentid }
|
||||
end
|
||||
|
||||
def message_send(openid, message)
|
||||
post 'message/send', Message.to(openid).text(message).agent_id(agentid).to_json, content_type: :json
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,195 @@
|
|||
module Wechat
|
||||
class Message
|
||||
class << self
|
||||
def from_hash(message_hash)
|
||||
new(message_hash)
|
||||
end
|
||||
|
||||
def to(to_user)
|
||||
new(ToUserName: to_user, CreateTime: Time.now.to_i)
|
||||
end
|
||||
end
|
||||
|
||||
class ArticleBuilder
|
||||
attr_reader :items
|
||||
delegate :count, to: :items
|
||||
def initialize
|
||||
@items = []
|
||||
end
|
||||
|
||||
def item(options={})
|
||||
title = options[:title]
|
||||
description = options[:description]
|
||||
pic_url = options[:pic_url]
|
||||
url = options[:url]
|
||||
items << { Title: title, Description: description, PicUrl: pic_url, Url: url }.reject { |_k, v| v.nil? }
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :message_hash
|
||||
|
||||
def initialize(message_hash)
|
||||
@message_hash = message_hash || {}
|
||||
session
|
||||
end
|
||||
|
||||
def [](key)
|
||||
message_hash[key]
|
||||
end
|
||||
|
||||
def reply
|
||||
Message.new(
|
||||
ToUserName: message_hash[:FromUserName],
|
||||
FromUserName: message_hash[:ToUserName],
|
||||
CreateTime: Time.now.to_i,
|
||||
session: session
|
||||
)
|
||||
end
|
||||
|
||||
def session
|
||||
@message_hash[:session] ||= Wechat::WechatLog.find_session(message_hash[:FromUserName]) || {}
|
||||
end
|
||||
|
||||
def as(type)
|
||||
case type
|
||||
when :text
|
||||
message_hash[:Content]
|
||||
|
||||
when :image, :voice, :video
|
||||
Wechat.api.media(message_hash[:MediaId])
|
||||
|
||||
when :location
|
||||
message_hash.slice(:Location_X, :Location_Y, :Scale, :Label).each_with_object({}) do |value, results|
|
||||
results[value[0].to_s.underscore.to_sym] = value[1]
|
||||
end
|
||||
else
|
||||
fail "Don't know how to parse message as #{type}"
|
||||
end
|
||||
end
|
||||
|
||||
def to(openid)
|
||||
update(ToUserName: openid)
|
||||
end
|
||||
|
||||
def agent_id(agentid)
|
||||
update(AgentId: agentid)
|
||||
end
|
||||
|
||||
def text(content)
|
||||
update(MsgType: 'text', Content: content)
|
||||
end
|
||||
|
||||
def transfer_customer_service
|
||||
update(MsgType: 'transfer_customer_service')
|
||||
end
|
||||
|
||||
def success
|
||||
update(MsgType: 'success')
|
||||
end
|
||||
|
||||
def image(media_id)
|
||||
update(MsgType: 'image', Image: { MediaId: media_id })
|
||||
end
|
||||
|
||||
def voice(media_id)
|
||||
update(MsgType: 'voice', Voice: { MediaId: media_id })
|
||||
end
|
||||
|
||||
def video(media_id, opts = {})
|
||||
video_fields = camelize_hash_keys({ media_id: media_id }.merge(opts.slice(:title, :description)))
|
||||
update(MsgType: 'video', Video: video_fields)
|
||||
end
|
||||
|
||||
def music(thumb_media_id, music_url, opts = {})
|
||||
music_fields = camelize_hash_keys(opts.slice(:title, :description, :HQ_music_url).merge(music_url: music_url, thumb_media_id: thumb_media_id))
|
||||
update(MsgType: 'music', Music: music_fields)
|
||||
end
|
||||
|
||||
def news(collection, &block)
|
||||
if block_given?
|
||||
article = ArticleBuilder.new
|
||||
collection.take(10).each_with_index { |item, index| yield(article, item, index) }
|
||||
items = article.items
|
||||
else
|
||||
items = collection.collect do |item|
|
||||
camelize_hash_keys(item.symbolize_keys.slice(:title, :description, :pic_url, :url).reject { |_k, v| v.nil? })
|
||||
end
|
||||
end
|
||||
|
||||
update(MsgType: 'news', ArticleCount: items.count,
|
||||
Articles: items.collect { |item| camelize_hash_keys(item) })
|
||||
end
|
||||
|
||||
def template(opts = {})
|
||||
template_fields = camelize_hash_keys(opts.symbolize_keys.slice(:template_id, :topcolor, :url, :data))
|
||||
update(MsgType: 'template', Template: template_fields)
|
||||
end
|
||||
|
||||
def to_xml
|
||||
message_hash.to_xml(root: 'xml', children: 'item', skip_instruct: true, skip_types: true)
|
||||
end
|
||||
|
||||
TO_JSON_KEY_MAP = {
|
||||
'ToUserName' => 'touser',
|
||||
'MediaId' => 'media_id',
|
||||
'ThumbMediaId' => 'thumb_media_id',
|
||||
'TemplateId' => 'template_id'
|
||||
}
|
||||
|
||||
TO_JSON_ALLOWED = %w(touser msgtype content image voice video music news articles template agentid)
|
||||
|
||||
def to_json
|
||||
json_hash = deep_recursive(message_hash) do |key, value|
|
||||
key = key.to_s
|
||||
[(TO_JSON_KEY_MAP[key] || key.downcase), value]
|
||||
end
|
||||
|
||||
json_hash = json_hash.select { |k, _v| TO_JSON_ALLOWED.include? k }
|
||||
case json_hash['msgtype']
|
||||
when 'text'
|
||||
json_hash['text'] = { 'content' => json_hash.delete('content') }
|
||||
when 'news'
|
||||
json_hash['news'] = { 'articles' => json_hash.delete('articles') }
|
||||
when 'template'
|
||||
json_hash.merge! json_hash['template']
|
||||
end
|
||||
JSON.generate(json_hash)
|
||||
end
|
||||
|
||||
def save_to!(model_class)
|
||||
model = model_class.new(underscore_hash_keys(message_hash.tap { |hs| hs.delete(:session) }))
|
||||
model.save!
|
||||
self
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def camelize_hash_keys(hash)
|
||||
deep_recursive(hash) { |key, value| [key.to_s.camelize.to_sym, value] }
|
||||
end
|
||||
|
||||
def underscore_hash_keys(hash)
|
||||
deep_recursive(hash) { |key, value| [key.to_s.underscore.to_sym, value] }
|
||||
end
|
||||
|
||||
def update(fields = {})
|
||||
message_hash.merge!(fields)
|
||||
self
|
||||
end
|
||||
|
||||
def deep_recursive(hash, &block)
|
||||
hash.inject({}) do |memo, val|
|
||||
key, value = *val
|
||||
case value.class.name
|
||||
when 'Hash'
|
||||
value = deep_recursive(value, &block)
|
||||
when 'Array'
|
||||
value = value.collect { |item| item.is_a?(Hash) ? deep_recursive(item, &block) : item }
|
||||
end
|
||||
|
||||
key, value = yield(key, value)
|
||||
memo.merge!(key => value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,275 @@
|
|||
require 'English'
|
||||
require 'wechat/signature'
|
||||
|
||||
module Wechat
|
||||
module Responder
|
||||
extend ActiveSupport::Concern
|
||||
include Cipher
|
||||
|
||||
included do
|
||||
# Rails 5 remove before_filter and skip_before_filter
|
||||
if respond_to?(:skip_before_action)
|
||||
# Rails 5 API mode won't define verify_authenticity_token
|
||||
skip_before_action :verify_authenticity_token unless respond_to?(:verify_authenticity_token)
|
||||
before_action :verify_signature, only: [:show, :create]
|
||||
else
|
||||
skip_before_filter :verify_authenticity_token
|
||||
before_filter :verify_signature, only: [:show, :create]
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
attr_accessor :wechat, :token, :corpid, :agentid, :encrypt_mode, :timeout, :skip_verify_ssl, :encoding_aes_key
|
||||
|
||||
def on(message_type, options={}, &block)
|
||||
fail 'Unknow message type' unless [:text, :image, :voice, :video, :link, :event, :click, :view, :scan, :batch_job, :location, :fallback].include?(message_type)
|
||||
respond = options[:respond]
|
||||
with = options[:with]
|
||||
|
||||
config = respond.nil? ? {} : { respond: respond }
|
||||
config.merge!(proc: block) if block_given?
|
||||
|
||||
if with.present?
|
||||
fail 'Only text, event, click, view, scan and batch_job can having :with parameters' unless [:text, :event, :click, :view, :scan, :batch_job].include?(message_type)
|
||||
config.merge!(with: with)
|
||||
if message_type == :scan
|
||||
if with.is_a?(String)
|
||||
self.known_scan_key_lists = with
|
||||
else
|
||||
fail 'on :scan only support string in parameter with, detail see https://github.com/Eric-Guo/wechat/issues/84'
|
||||
end
|
||||
end
|
||||
else
|
||||
fail 'Message type click, view, scan and batch_job must specify :with parameters' if [:click, :view, :scan, :batch_job].include?(message_type)
|
||||
end
|
||||
|
||||
case message_type
|
||||
when :click
|
||||
user_defined_click_responders(with) << config
|
||||
when :view
|
||||
user_defined_view_responders(with) << config
|
||||
when :batch_job
|
||||
user_defined_batch_job_responders(with) << config
|
||||
when :scan
|
||||
user_defined_scan_responders << config
|
||||
when :location
|
||||
user_defined_location_responders << config
|
||||
else
|
||||
user_defined_responders(message_type) << config
|
||||
end
|
||||
|
||||
config
|
||||
end
|
||||
|
||||
def user_defined_click_responders(with)
|
||||
@click_responders ||= {}
|
||||
@click_responders[with] ||= []
|
||||
end
|
||||
|
||||
def user_defined_view_responders(with)
|
||||
@view_responders ||= {}
|
||||
@view_responders[with] ||= []
|
||||
end
|
||||
|
||||
def user_defined_batch_job_responders(with)
|
||||
@batch_job_responders ||= {}
|
||||
@batch_job_responders[with] ||= []
|
||||
end
|
||||
|
||||
def user_defined_scan_responders
|
||||
@scan_responders ||= []
|
||||
end
|
||||
|
||||
def user_defined_location_responders
|
||||
@location_responders ||= []
|
||||
end
|
||||
|
||||
def user_defined_responders(type)
|
||||
@responders ||= {}
|
||||
@responders[type] ||= []
|
||||
end
|
||||
|
||||
def responder_for(message)
|
||||
message_type = message[:MsgType].to_sym
|
||||
responders = user_defined_responders(message_type)
|
||||
|
||||
case message_type
|
||||
when :text
|
||||
yield(* match_responders(responders, message[:Content]))
|
||||
when :event
|
||||
if 'click' == message[:Event] && !user_defined_click_responders(message[:EventKey]).empty?
|
||||
yield(* user_defined_click_responders(message[:EventKey]), message[:EventKey])
|
||||
elsif 'view' == message[:Event] && !user_defined_view_responders(message[:EventKey]).empty?
|
||||
yield(* user_defined_view_responders(message[:EventKey]), message[:EventKey])
|
||||
elsif 'click' == message[:Event]
|
||||
yield(* match_responders(responders, message[:EventKey]))
|
||||
elsif known_scan_key_lists.include?(message[:EventKey])
|
||||
yield(* known_scan_with_match_responders(user_defined_scan_responders, message))
|
||||
elsif 'batch_job_result' == message[:Event]
|
||||
yield(* user_defined_batch_job_responders(message[:BatchJob][:JobType]), message[:BatchJob])
|
||||
elsif 'location' == message[:Event]
|
||||
yield(* user_defined_location_responders, message)
|
||||
else
|
||||
yield(* match_responders(responders, message[:Event]))
|
||||
end
|
||||
else
|
||||
yield(responders.first)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def match_responders(responders, value)
|
||||
matched = responders.each_with_object({}) do |responder, memo|
|
||||
condition = responder[:with]
|
||||
|
||||
if condition.nil?
|
||||
memo[:general] ||= [responder, value]
|
||||
next
|
||||
end
|
||||
|
||||
if condition.is_a? Regexp
|
||||
memo[:scoped] ||= [responder] + $LAST_MATCH_INFO.captures if value =~ condition
|
||||
else
|
||||
memo[:scoped] ||= [responder, value] if value == condition
|
||||
end
|
||||
end
|
||||
matched[:scoped] || matched[:general]
|
||||
end
|
||||
|
||||
def known_scan_with_match_responders(responders, message)
|
||||
matched = responders.each_with_object({}) do |responder, memo|
|
||||
if %w(scan subscribe).include?(message[:Event]) && message[:EventKey] == responder[:with]
|
||||
memo[:scaned] ||= [responder, message[:Ticket]]
|
||||
elsif %w(scancode_push scancode_waitmsg).include?(message[:Event]) && message[:EventKey] == responder[:with]
|
||||
memo[:scaned] ||= [responder, message[:ScanCodeInfo][:ScanResult], message[:ScanCodeInfo][:ScanType]]
|
||||
end
|
||||
end
|
||||
matched[:scaned]
|
||||
end
|
||||
|
||||
def known_scan_key_lists
|
||||
@known_scan_key_lists ||= []
|
||||
end
|
||||
|
||||
def known_scan_key_lists=(qrscene_value)
|
||||
@known_scan_key_lists ||= []
|
||||
@known_scan_key_lists << qrscene_value
|
||||
end
|
||||
end
|
||||
|
||||
def wechat
|
||||
self.class.wechat # Make sure user can continue access wechat at instance level similar to class level
|
||||
end
|
||||
|
||||
def show
|
||||
if self.class.corpid.present?
|
||||
echostr, _corp_id = unpack(decrypt(Base64.decode64(params[:echostr]), self.class.encoding_aes_key))
|
||||
render text: echostr
|
||||
else
|
||||
render text: params[:echostr]
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
request = Wechat::Message.from_hash(post_xml)
|
||||
response = run_responder(request)
|
||||
|
||||
if response.respond_to? :to_xml
|
||||
body = process_response(response)
|
||||
if Rails.version.start_with?("3.")
|
||||
render text: body
|
||||
else
|
||||
render plain: body
|
||||
end
|
||||
else
|
||||
render nothing: true, status: 200, content_type: 'text/html'
|
||||
end
|
||||
|
||||
Wechat::WechatLog.create_by_responder post_xml, response, response.session if response.is_a?(Wechat::Message)
|
||||
after_wechat_response(request, response) if respond_to?(:after_wechat_response)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def verify_signature
|
||||
if self.class.encrypt_mode
|
||||
signature = params[:signature] || params[:msg_signature]
|
||||
msg_encrypt = params[:echostr] || request_encrypt_content
|
||||
else
|
||||
signature = params[:signature]
|
||||
end
|
||||
|
||||
msg_encrypt = nil unless self.class.corpid.present?
|
||||
|
||||
render text: 'Forbidden', status: 403 if signature != Signature.hexdigest(self.class.token,
|
||||
params[:timestamp],
|
||||
params[:nonce],
|
||||
msg_encrypt)
|
||||
end
|
||||
|
||||
def post_xml
|
||||
data = request_content
|
||||
|
||||
if self.class.encrypt_mode && request_encrypt_content.present?
|
||||
content, @app_id = unpack(decrypt(Base64.decode64(request_encrypt_content), self.class.encoding_aes_key))
|
||||
data = Hash.from_xml(content)
|
||||
end
|
||||
|
||||
HashWithIndifferentAccess.new_from_hash_copying_default(data.fetch('xml', {})).tap do |msg|
|
||||
msg[:Event].downcase! if msg[:Event]
|
||||
end
|
||||
end
|
||||
|
||||
def run_responder(request)
|
||||
self.class.responder_for(request) do |responder, *args|
|
||||
responder ||= self.class.user_defined_responders(:fallback).first
|
||||
|
||||
next if responder.nil?
|
||||
case
|
||||
when responder[:respond]
|
||||
request.reply.text responder[:respond]
|
||||
when responder[:proc]
|
||||
define_singleton_method :process, responder[:proc]
|
||||
number_of_block_parameter = responder[:proc].arity
|
||||
send(:process, *args.unshift(request).take(number_of_block_parameter))
|
||||
else
|
||||
next
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def process_response(response)
|
||||
if response[:MsgType] == 'success'
|
||||
msg = 'success'
|
||||
else
|
||||
msg = response.to_xml
|
||||
end
|
||||
|
||||
if self.class.encrypt_mode
|
||||
encrypt = Base64.strict_encode64(encrypt(pack(msg, @app_id), self.class.encoding_aes_key))
|
||||
msg = gen_msg(encrypt, params[:timestamp], params[:nonce])
|
||||
end
|
||||
|
||||
msg
|
||||
end
|
||||
|
||||
def gen_msg(encrypt, timestamp, nonce)
|
||||
msg_sign = Signature.hexdigest(self.class.token, timestamp, nonce, encrypt)
|
||||
|
||||
{ Encrypt: encrypt,
|
||||
MsgSignature: msg_sign,
|
||||
TimeStamp: timestamp,
|
||||
Nonce: nonce
|
||||
}.to_xml(root: 'xml', children: 'item', skip_instruct: true, skip_types: true)
|
||||
end
|
||||
|
||||
def request_encrypt_content
|
||||
request_content['xml']['Encrypt']
|
||||
end
|
||||
|
||||
def request_content
|
||||
params[:xml].nil? ? Hash.from_xml(request.raw_post) : { 'xml' => params[:xml] }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,10 @@
|
|||
module Wechat
|
||||
module Signature
|
||||
def self.hexdigest(token, timestamp, nonce, msg_encrypt)
|
||||
array = [token, timestamp, nonce]
|
||||
array << msg_encrypt unless msg_encrypt.nil?
|
||||
dev_msg_signature = array.compact.collect(&:to_s).sort.join
|
||||
Digest::SHA1.hexdigest(dev_msg_signature)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,13 @@
|
|||
require 'wechat/ticket/jsapi_base'
|
||||
|
||||
module Wechat
|
||||
module Ticket
|
||||
class CorpJsapiTicket < JsapiBase
|
||||
def refresh
|
||||
data = client.get('get_jsapi_ticket', params: { access_token: access_token.token })
|
||||
write_ticket_to_file(data)
|
||||
read_ticket_from_file
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,65 @@
|
|||
require 'digest/sha1'
|
||||
|
||||
module Wechat
|
||||
module Ticket
|
||||
class JsapiBase
|
||||
attr_reader :client, :access_token, :jsapi_ticket_file, :access_ticket, :ticket_life_in_seconds, :got_ticket_at
|
||||
|
||||
def initialize(client, access_token, jsapi_ticket_file)
|
||||
@client = client
|
||||
@access_token = access_token
|
||||
@jsapi_ticket_file = jsapi_ticket_file
|
||||
@random_generator = Random.new
|
||||
end
|
||||
|
||||
def ticket
|
||||
# Possible two worker running, one worker refresh ticket, other unaware, so must read every time
|
||||
read_ticket_from_file
|
||||
refresh if remain_life_seconds < @random_generator.rand(30..3 * 60)
|
||||
access_ticket
|
||||
end
|
||||
|
||||
# Obtain the wechat jssdk config signature parameter and return below hash
|
||||
# params = {
|
||||
# noncestr: noncestr,
|
||||
# timestamp: timestamp,
|
||||
# jsapi_ticket: ticket,
|
||||
# url: url,
|
||||
# signature: signature
|
||||
# }
|
||||
def signature(url)
|
||||
params = {
|
||||
noncestr: SecureRandom.base64(16),
|
||||
timestamp: Time.now.to_i,
|
||||
jsapi_ticket: ticket,
|
||||
url: url
|
||||
}
|
||||
pairs = params.keys.sort.map do |key|
|
||||
"#{key}=#{params[key]}"
|
||||
end
|
||||
result = Digest::SHA1.hexdigest pairs.join('&')
|
||||
params.merge(signature: result)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def read_ticket_from_file
|
||||
td = JSON.parse(File.read(jsapi_ticket_file))
|
||||
@got_ticket_at = td.fetch('got_ticket_at').to_i
|
||||
@ticket_life_in_seconds = td.fetch('expires_in').to_i
|
||||
@access_ticket = td.fetch('ticket')
|
||||
rescue JSON::ParserError, Errno::ENOENT, KeyError
|
||||
refresh
|
||||
end
|
||||
|
||||
def write_ticket_to_file(ticket_hash)
|
||||
ticket_hash.merge!('got_ticket_at'.freeze => Time.now.to_i)
|
||||
File.write(jsapi_ticket_file, ticket_hash.to_json)
|
||||
end
|
||||
|
||||
def remain_life_seconds
|
||||
ticket_life_in_seconds - (Time.now.to_i - got_ticket_at)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,13 @@
|
|||
require 'wechat/ticket/jsapi_base'
|
||||
|
||||
module Wechat
|
||||
module Ticket
|
||||
class PublicJsapiTicket < JsapiBase
|
||||
def refresh
|
||||
data = client.get('ticket/getticket', params: { access_token: access_token.token, type: 'jsapi' })
|
||||
write_ticket_to_file(data)
|
||||
read_ticket_from_file
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,42 @@
|
|||
module Wechat
|
||||
module Token
|
||||
class AccessTokenBase
|
||||
attr_reader :client, :appid, :secret, :token_file, :access_token, :token_life_in_seconds, :got_token_at
|
||||
|
||||
def initialize(client, appid, secret, token_file)
|
||||
@appid = appid
|
||||
@secret = secret
|
||||
@client = client
|
||||
@token_file = token_file
|
||||
@random_generator = Random.new
|
||||
end
|
||||
|
||||
def token
|
||||
# Possible two worker running, one worker refresh token, other unaware, so must read every time
|
||||
read_token_from_file
|
||||
refresh if remain_life_seconds < @random_generator.rand(30..3 * 60)
|
||||
access_token
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def read_token_from_file
|
||||
td = JSON.parse(File.read(token_file))
|
||||
@got_token_at = td.fetch('got_token_at').to_i
|
||||
@token_life_in_seconds = td.fetch('expires_in').to_i
|
||||
@access_token = td.fetch('access_token')
|
||||
rescue JSON::ParserError, Errno::ENOENT, KeyError
|
||||
refresh
|
||||
end
|
||||
|
||||
def write_token_to_file(token_hash)
|
||||
token_hash.merge!('got_token_at'.freeze => Time.now.to_i)
|
||||
File.write(token_file, token_hash.to_json)
|
||||
end
|
||||
|
||||
def remain_life_seconds
|
||||
token_life_in_seconds - (Time.now.to_i - got_token_at)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,13 @@
|
|||
require 'wechat/token/access_token_base'
|
||||
|
||||
module Wechat
|
||||
module Token
|
||||
class CorpAccessToken < AccessTokenBase
|
||||
def refresh
|
||||
data = client.get('gettoken', params: { corpid: appid, corpsecret: secret })
|
||||
write_token_to_file(data)
|
||||
read_token_from_file
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,13 @@
|
|||
require 'wechat/token/access_token_base'
|
||||
|
||||
module Wechat
|
||||
module Token
|
||||
class PublicAccessToken < AccessTokenBase
|
||||
def refresh
|
||||
data = client.get('token', params: { grant_type: 'client_credential', appid: appid, secret: secret })
|
||||
write_token_to_file(data)
|
||||
read_token_from_file
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,24 @@
|
|||
require 'active_record'
|
||||
|
||||
module Wechat
|
||||
class WechatLog < ::ActiveRecord::Base
|
||||
def self.create_by_responder(req, res, session)
|
||||
create openid: req[:FromUserName], request: req, response: res, session: session
|
||||
end
|
||||
|
||||
def self.find_session(openid)
|
||||
select(:session_raw).where(openid: openid).last.try :session
|
||||
end
|
||||
|
||||
[:request, :response, :session].each do |name|
|
||||
define_method name do
|
||||
raw = send(:"#{name}_raw")
|
||||
raw.blank? ? {} : JSON.parse(raw).symbolize_keys
|
||||
end
|
||||
|
||||
define_method :"#{name}=" do |obj|
|
||||
self[:"#{name}_raw"] = obj.try(:to_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,4 @@
|
|||
# This file is used by Rack-based servers to start the application.
|
||||
|
||||
require ::File.expand_path('../config/environment', __FILE__)
|
||||
run Rails.application
|
|
@ -0,0 +1,35 @@
|
|||
require File.expand_path('../boot', __FILE__)
|
||||
|
||||
require "rails"
|
||||
# Pick the frameworks you want:
|
||||
# require "active_model/railtie"
|
||||
#require "active_job/railtie"
|
||||
require "active_record/railtie"
|
||||
require "action_controller/railtie"
|
||||
#require "action_mailer/railtie"
|
||||
#require "action_view/railtie"
|
||||
# require "sprockets/railtie"
|
||||
#require "rails/test_unit/railtie"
|
||||
|
||||
# Require the gems listed in Gemfile, including any gems
|
||||
# you've limited to :test, :development, or :production.
|
||||
Bundler.require(*Rails.groups)
|
||||
|
||||
module Dummy
|
||||
class Application < Rails::Application
|
||||
# Settings in config/environments/* take precedence over those specified here.
|
||||
# Application configuration should go into files in config/initializers
|
||||
# -- all .rb files in that directory are automatically loaded.
|
||||
|
||||
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
|
||||
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
|
||||
# config.time_zone = 'Central Time (US & Canada)'
|
||||
|
||||
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
|
||||
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
|
||||
# config.i18n.default_locale = :de
|
||||
|
||||
# Do not swallow errors in after_commit/after_rollback callbacks.
|
||||
#config.active_record.raise_in_transactional_callbacks = true
|
||||
end
|
||||
end
|
|
@ -0,0 +1,3 @@
|
|||
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__)
|
||||
|
||||
require 'bundler/setup' # Set up gems listed in the Gemfile.
|
|
@ -0,0 +1,5 @@
|
|||
test:
|
||||
adapter: sqlite3
|
||||
pool: 5
|
||||
timeout: 5000
|
||||
database: ":memory:"
|
|
@ -0,0 +1,5 @@
|
|||
# Load the Rails application.
|
||||
require File.expand_path('../application', __FILE__)
|
||||
|
||||
# Initialize the Rails application.
|
||||
Rails.application.initialize!
|
|
@ -0,0 +1,42 @@
|
|||
Rails.application.configure do
|
||||
# Settings specified here will take precedence over those in config/application.rb.
|
||||
|
||||
# The test environment is used exclusively to run your application's
|
||||
# test suite. You never need to work with it otherwise. Remember that
|
||||
# your test database is "scratch space" for the test suite and is wiped
|
||||
# and recreated between test runs. Don't rely on the data there!
|
||||
config.cache_classes = true
|
||||
|
||||
# Do not eager load code on boot. This avoids loading your whole application
|
||||
# just for the purpose of running a single test. If you are using a tool that
|
||||
# preloads Rails for running tests, you may have to set it to true.
|
||||
config.eager_load = false
|
||||
|
||||
# Configure static file server for tests with Cache-Control for performance.
|
||||
config.serve_static_files = true
|
||||
config.static_cache_control = 'public, max-age=3600'
|
||||
|
||||
# Show full error reports and disable caching.
|
||||
config.consider_all_requests_local = true
|
||||
config.action_controller.perform_caching = false
|
||||
|
||||
# Raise exceptions instead of rendering exception templates.
|
||||
config.action_dispatch.show_exceptions = false
|
||||
|
||||
# Disable request forgery protection in test environment.
|
||||
config.action_controller.allow_forgery_protection = false
|
||||
|
||||
# Tell Action Mailer not to deliver emails to the real world.
|
||||
# The :test delivery method accumulates sent emails in the
|
||||
# ActionMailer::Base.deliveries array.
|
||||
#config.action_mailer.delivery_method = :test
|
||||
|
||||
# Randomize the order test cases are executed.
|
||||
config.active_support.test_order = :random
|
||||
|
||||
# Print deprecation notices to the stderr.
|
||||
config.active_support.deprecation = :stderr
|
||||
|
||||
# Raises error for missing translations
|
||||
# config.action_view.raise_on_missing_translations = true
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
Rails.application.routes.draw do
|
||||
get 'wechat', to: 'wechat#show'
|
||||
post 'wechat', to: 'wechat#create'
|
||||
|
||||
get 'wechat_corp', to: 'wechat_corp#show'
|
||||
post 'wechat_corp', to: 'wechat_corp#create'
|
||||
end
|
|
@ -0,0 +1,22 @@
|
|||
# Be sure to restart your server when you modify this file.
|
||||
|
||||
# Your secret key is used for verifying the integrity of signed cookies.
|
||||
# If you change this key, all old signed cookies will become invalid!
|
||||
|
||||
# Make sure the secret is at least 30 characters and all random,
|
||||
# no regular words or you'll be exposed to dictionary attacks.
|
||||
# You can use `rake secret` to generate a secure secret key.
|
||||
|
||||
# Make sure the secrets in this file are kept private
|
||||
# if you're sharing your code publicly.
|
||||
|
||||
development:
|
||||
secret_key_base: f95d476dd39f698ea78be4a3c0fbba94cabb08b38aab9deab2e50f4fb7290786a722cfc4c174e30b5b761a31efd3b40133a3d01586eb2745f659fc7d6b17a552
|
||||
|
||||
test:
|
||||
secret_key_base: ebd51f2bd20540c6005cdc743ce71cc4cac49bc918a1207ac6107945da585aad330b07866f16f5ad643defcd50289aa4069c3aeb7e319389b4eb5adf99c27554
|
||||
|
||||
# Do not keep production secrets in the repository,
|
||||
# instead read values from the environment.
|
||||
production:
|
||||
secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
|
|
@ -0,0 +1,299 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Wechat::Api do
|
||||
let(:token_file) { Rails.root.join('tmp/access_token') }
|
||||
let(:jsapi_ticket_file) { Rails.root.join('tmp/jsapi_ticket') }
|
||||
|
||||
subject do
|
||||
Wechat::Api.new('appid', 'secret', token_file, 20, false, jsapi_ticket_file)
|
||||
end
|
||||
|
||||
before :each do
|
||||
allow(subject.access_token).to receive(:token).and_return('access_token')
|
||||
allow(subject.jsapi_ticket).to receive(:jsapi_ticket).and_return('jsapi_ticket')
|
||||
end
|
||||
|
||||
describe '#API_BASE' do
|
||||
specify 'will get correct API_BASE' do
|
||||
expect(subject.client.base).to eq Wechat::Api::API_BASE
|
||||
end
|
||||
end
|
||||
|
||||
describe '#callbackip' do
|
||||
specify 'will get callbackip with access_token' do
|
||||
server_ip_result = 'server_ip_result'
|
||||
expect(subject.client).to receive(:get).with('getcallbackip', params: { access_token: 'access_token' }).and_return(server_ip_result)
|
||||
expect(subject.callbackip).to eq server_ip_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#qrcode' do
|
||||
specify 'will get showqrcode with ticket at file based api endpoint as file' do
|
||||
ticket_result = 'ticket_result'
|
||||
|
||||
expect(subject.client).to receive(:get)
|
||||
.with('showqrcode', params: { ticket: 'ticket' },
|
||||
base: Wechat::ApiBase::MP_BASE,
|
||||
as: :file).and_return(ticket_result)
|
||||
expect(subject.qrcode('ticket')).to eq(ticket_result)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#groups' do
|
||||
specify 'will get groups with access_token' do
|
||||
groups_result = 'groups_result'
|
||||
expect(subject.client).to receive(:get).with('groups/get', params: { access_token: 'access_token' }).and_return(groups_result)
|
||||
expect(subject.groups).to eq groups_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#group_create' do
|
||||
specify 'will post groups/create with access_token and new group json_data' do
|
||||
new_group = { group: { name: 'new_group_name' } }
|
||||
expect(subject.client).to receive(:post).with('groups/create', new_group.to_json, params: { access_token: 'access_token' }).and_return(true)
|
||||
expect(subject.group_create('new_group_name')).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#group_update' do
|
||||
specify 'will post groups/update with access_token and json_data' do
|
||||
update_group = { group: { id: 108, name: 'test2_modify2' } }
|
||||
expect(subject.client).to receive(:post).with('groups/update', update_group.to_json, params: { access_token: 'access_token' }).and_return(true)
|
||||
expect(subject.group_update(108, 'test2_modify2')).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#group_delete' do
|
||||
specify 'will post groups/delete with access_token' do
|
||||
delete_group = { group: { id: 108 } }
|
||||
expect(subject.client).to receive(:post).with('groups/delete', delete_group.to_json, params: { access_token: 'access_token' }).and_return(true)
|
||||
expect(subject.group_delete(108)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#users' do
|
||||
specify 'will get user/get with access_token' do
|
||||
users_result = 'users_result'
|
||||
expect(subject.client).to receive(:get).with('user/get', params: { access_token: 'access_token' }).and_return(users_result)
|
||||
expect(subject.users).to eq(users_result)
|
||||
end
|
||||
|
||||
specify 'will get user/get with access_token and next_openid' do
|
||||
users_result = 'users_result'
|
||||
expect(subject.client).to receive(:get)
|
||||
.with('user/get', params: { access_token: 'access_token',
|
||||
next_openid: 'next_openid' }).and_return(users_result)
|
||||
expect(subject.users('next_openid')).to eq(users_result)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#user' do
|
||||
specify 'will get user/info with access_token and openid' do
|
||||
user_result = 'user_result'
|
||||
expect(subject.client).to receive(:get).with('user/info', params: { access_token: 'access_token', openid: 'openid' }).and_return(user_result)
|
||||
expect(subject.user 'openid').to eq(user_result)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#user_group' do
|
||||
specify 'will post groups/getid with access_token and openid to get user groups info' do
|
||||
user_request = { openid: 'openid' }
|
||||
user_response = { groupid: 102 }
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('groups/getid', user_request.to_json, params: { access_token: 'access_token' }).and_return(user_response)
|
||||
expect(subject.user_group 'openid').to eq(user_response)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#user_change_group' do
|
||||
specify 'will post groups/getid with access_token and openid to get user groups info' do
|
||||
user_request = { openid: 'openid', to_groupid: 108 }
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('groups/members/update', user_request.to_json, params: { access_token: 'access_token' }).and_return(true)
|
||||
expect(subject.user_change_group 'openid', 108).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#user_update_remark' do
|
||||
specify 'will post groups/getid with access_token and openid to get user groups info' do
|
||||
user_update_remark_request = { openid: 'openid', remark: 'remark' }
|
||||
user_update_remark_result = { errcode: 0, errmsg: 'ok' }
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('user/info/updateremark', user_update_remark_request.to_json, params: { access_token: 'access_token' }).and_return(user_update_remark_result)
|
||||
expect(subject.user_update_remark 'openid', 'remark').to eq user_update_remark_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#qrcode_create_scene' do
|
||||
specify 'will post qrcode/create with access_token, scene_id and expire_seconds' do
|
||||
scene_id = 101
|
||||
qrcode_scene_req = { expire_seconds: 60,
|
||||
action_name: 'QR_SCENE',
|
||||
action_info: { scene: { scene_id: scene_id } } }
|
||||
qrcode_scene_result = { ticket: 'qr_code_ticket',
|
||||
expire_seconds: 60, url: 'qr_code_ticket_pic_url' }
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('qrcode/create', qrcode_scene_req.to_json, params: { access_token: 'access_token' }).and_return(qrcode_scene_result)
|
||||
expect(subject.qrcode_create_scene(scene_id, 60)).to eq qrcode_scene_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#qrcode_create_limit_scene' do
|
||||
qrcode_limit_scene_result = { ticket: 'qr_code_ticket',
|
||||
url: 'qr_code_ticket_pic_url' }
|
||||
|
||||
specify 'will post qrcode/create with access_token and scene_id' do
|
||||
scene_id = 101
|
||||
qrcode_limit_scene_req = { action_name: 'QR_LIMIT_SCENE',
|
||||
action_info: { scene: { scene_id: scene_id } } }
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('qrcode/create', qrcode_limit_scene_req.to_json, params: { access_token: 'access_token' }).and_return(qrcode_limit_scene_result)
|
||||
expect(subject.qrcode_create_limit_scene(scene_id)).to eq qrcode_limit_scene_result
|
||||
end
|
||||
|
||||
specify 'will post qrcode/create with access_token and scene_str' do
|
||||
scene_str = 'scene_str'
|
||||
qrcode_limit_str_scene_req = { action_name: 'QR_LIMIT_STR_SCENE',
|
||||
action_info: { scene: { scene_str: scene_str } } }
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('qrcode/create', qrcode_limit_str_scene_req.to_json, params: { access_token: 'access_token' }).and_return(qrcode_limit_scene_result)
|
||||
expect(subject.qrcode_create_limit_scene(scene_str)).to eq qrcode_limit_scene_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#menu' do
|
||||
specify 'will get menu/get with access_token' do
|
||||
menu_result = 'menu_result'
|
||||
expect(subject.client).to receive(:get).with('menu/get', params: { access_token: 'access_token' }).and_return(menu_result)
|
||||
expect(subject.menu).to eq(menu_result)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#menu_delete' do
|
||||
specify 'will get menu/delete with access_token' do
|
||||
expect(subject.client).to receive(:get).with('menu/delete', params: { access_token: 'access_token' }).and_return(true)
|
||||
expect(subject.menu_delete).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#menu_create' do
|
||||
specify 'will post menu/create with access_token and json_data' do
|
||||
menu = { buttons: ['a_button'] }
|
||||
expect(subject.client).to receive(:post).with('menu/create', menu.to_json, params: { access_token: 'access_token' }).and_return(true)
|
||||
expect(subject.menu_create(menu)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#media' do
|
||||
specify 'will get media/get with access_token and media_id at file based api endpoint as file' do
|
||||
media_result = 'media_result'
|
||||
|
||||
expect(subject.client).to receive(:get)
|
||||
.with('media/get', params: { access_token: 'access_token', media_id: 'media_id' },
|
||||
as: :file).and_return(media_result)
|
||||
expect(subject.media('media_id')).to eq(media_result)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#media_create' do
|
||||
specify 'will post media/upload with access_token, type and media payload at file based api endpoint' do
|
||||
file = 'README.md'
|
||||
expect(subject.client).to receive(:post_file)
|
||||
.with('media/upload', file,
|
||||
params: { type: 'image', access_token: 'access_token' }).and_return(true)
|
||||
expect(subject.media_create('image', file)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#material' do
|
||||
specify 'will get material/get with access_token and media_id at file based api endpoint as file' do
|
||||
material_result = 'material_result'
|
||||
|
||||
expect(subject.client).to receive(:get)
|
||||
.with('material/get', params: { access_token: 'access_token', media_id: 'media_id' },
|
||||
as: :file).and_return(material_result)
|
||||
expect(subject.material('media_id')).to eq(material_result)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#material_count' do
|
||||
specify 'will get material_count with access_token' do
|
||||
material_count_result = { voice_count: 1,
|
||||
video_count: 2,
|
||||
image_count: 3,
|
||||
news_count: 4 }
|
||||
expect(subject.client).to receive(:get)
|
||||
.with('material/get_materialcount', params: { access_token: 'access_token' }).and_return(material_count_result)
|
||||
expect(subject.material_count).to eq material_count_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#material_list' do
|
||||
specify 'will get material list with access_token' do
|
||||
material_list_request = { type: 'image', offset: 0, count: 20 }
|
||||
material_list_result = { total_count: 1, item_count: 1,
|
||||
item: [{ media_id: 'media_id', name: 'name', update_time: 12345, url: 'url' }] }
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('material/batchget_material', material_list_request.to_json, params: { access_token: 'access_token' }).and_return(material_list_result)
|
||||
expect(subject.material_list('image', 0, 20)).to eq material_list_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#material_add' do
|
||||
specify 'will post material/add_material with access_token, type and media payload at file based api endpoint' do
|
||||
file = 'README.md'
|
||||
expect(subject.client).to receive(:post_file)
|
||||
.with('material/add_material', file,
|
||||
params: { type: 'image', access_token: 'access_token' }).and_return(true)
|
||||
expect(subject.material_add('image', file)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#material_delete' do
|
||||
specify 'will post material/del_material with access_token and media_id in payload' do
|
||||
media_id = 'media_id'
|
||||
material_delete_result = { errcode: 0, errmsg: 'deleted' }
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('material/del_material', { media_id: media_id },
|
||||
params: { access_token: 'access_token' }).and_return(material_delete_result)
|
||||
expect(subject.material_delete(media_id)).to eq material_delete_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#custom_message_send' do
|
||||
specify 'will post message/custom/send with access_token, and json payload' do
|
||||
payload = {
|
||||
touser: 'openid',
|
||||
msgtype: 'text',
|
||||
text: { content: 'message content' }
|
||||
}
|
||||
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('message/custom/send', payload.to_json,
|
||||
params: { access_token: 'access_token' }, content_type: :json).and_return(true)
|
||||
|
||||
expect(subject.custom_message_send Wechat::Message.to('openid').text('message content')).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#template_message' do
|
||||
specify 'will post message/custom/send with access_token, and json payload' do
|
||||
payload = { touser: 'OPENID',
|
||||
template_id: 'ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY',
|
||||
url: 'http://weixin.qq.com/download',
|
||||
topcolor: '#FF0000',
|
||||
data: { first: { value: '恭喜你购买成功!', color: '#173177' },
|
||||
keynote1: { value: '巧克力', color: '#173177' },
|
||||
keynote2: { value: '39.8元', color: '#173177' },
|
||||
keynote3: { value: '2014年9月16日', color: '#173177' },
|
||||
remark: { value: '欢迎再次购买!', color: '#173177' } } }
|
||||
response_result = { errcode: 0, errmsg: 'ok', msgid: 332 }
|
||||
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('message/template/send', payload.to_json,
|
||||
params: { access_token: 'access_token' }, content_type: :json).and_return(response_result)
|
||||
|
||||
expect(subject.template_message_send payload).to eq response_result
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,62 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Wechat::Cipher do
|
||||
subject { Class.new.send(:include, Wechat::Cipher) }
|
||||
|
||||
it '#encode_padding' do
|
||||
result = subject.new.instance_eval { encode_padding('abcd') }
|
||||
expect(result.length).to eq Wechat::Cipher::BLOCK_SIZE
|
||||
expect(result.bytes[-1]).to eq Wechat::Cipher::BLOCK_SIZE - 4
|
||||
end
|
||||
|
||||
it '#decode_padding' do
|
||||
result = subject.new.instance_eval { decode_padding("abcd\x3\x3\x3") }
|
||||
expect(result).to eq 'abcd'
|
||||
end
|
||||
|
||||
it '#encrypt & #decrypt short' do
|
||||
key = SecureRandom.base64(32)
|
||||
plain_text = 'hello world'
|
||||
encrypt_text = subject.new.instance_eval { encrypt(encode_padding(plain_text), key) }
|
||||
decrypt_text = subject.new.instance_eval { decrypt(encrypt_text, key) }
|
||||
|
||||
expect(decrypt_text).to eq plain_text
|
||||
end
|
||||
|
||||
it '#encrypt & #decrypt long' do
|
||||
key = SecureRandom.base64(32)
|
||||
plain_text = <<SHAKESPEARE
|
||||
Shall I compare thee to a summer's day?
|
||||
Thou art more lovely and more temperate:
|
||||
Rough winds do shake the darling buds of May,
|
||||
And summer's lease hath all too short a date:
|
||||
Sometime too hot the eye of heaven shines,
|
||||
And often is his gold complexion dimm'd;
|
||||
And every fair from fair sometime declines,
|
||||
By chance, or nature's changing course, untrimm'd;
|
||||
But thy eternal summer shall not fade
|
||||
Nor lose possession of that fair thou ow'st;
|
||||
Nor shall Death brag thou wander'st in his shade,
|
||||
When in eternal lines to time thou grow'st;
|
||||
So long as men can breathe or eyes can see,
|
||||
So long lives this, and this gives life to thee.
|
||||
SHAKESPEARE
|
||||
|
||||
encrypt_text = subject.new.instance_eval { encrypt(encode_padding(plain_text), key) }
|
||||
decrypt_text = subject.new.instance_eval { decrypt(encrypt_text, key) }
|
||||
|
||||
expect(decrypt_text).to eq plain_text
|
||||
end
|
||||
|
||||
it '#pack & #unpack' do
|
||||
content = '<xml>text</xml>'
|
||||
app_id = 'bravo_app'
|
||||
|
||||
packed_text = subject.new.instance_eval { pack(content, app_id) }
|
||||
|
||||
content2, app_id2 = subject.new.instance_eval { unpack(packed_text) }
|
||||
|
||||
expect(content2).to eq content
|
||||
expect(app_id2).to eq app_id
|
||||
end
|
||||
end
|
|
@ -0,0 +1,94 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Wechat::Client do
|
||||
subject do
|
||||
Wechat::Client.new('http://host/', 20, false)
|
||||
end
|
||||
|
||||
let(:response_params) do
|
||||
{
|
||||
headers: { content_type: 'text/plain' },
|
||||
status: 200
|
||||
}
|
||||
end
|
||||
let(:response_404) { double '404', response_params.merge(status: 404) }
|
||||
let(:response_text) { double 'text', response_params.merge(body: 'some text') }
|
||||
let(:response_json) do
|
||||
double 'json', response_params.merge(body: { result: 'success' }.to_json,
|
||||
headers: { content_type: 'application/json' })
|
||||
end
|
||||
let(:response_image) { double 'image', response_params.merge(body: 'image data', headers: { content_type: 'image/gif' }) }
|
||||
|
||||
describe '#get' do
|
||||
specify 'Will use http get method to request data' do
|
||||
allow(HTTP).to receive_message_chain('headers.get') { response_json }
|
||||
subject.get('token')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#post' do
|
||||
specify 'Will use http post method to request data' do
|
||||
allow(HTTP).to receive_message_chain('headers.post') { response_json }
|
||||
subject.post('token', 'some_data')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#request' do
|
||||
specify 'will add accept=>:json for request' do
|
||||
block = lambda do |url, headers|
|
||||
expect(url).to eq('http://host/token')
|
||||
expect(headers).to eq(params: { access_token: '1234' }, 'Accept' => 'application/json')
|
||||
response_json
|
||||
end
|
||||
|
||||
subject.send(:request, 'token', params: { access_token: '1234' }, &block)
|
||||
end
|
||||
|
||||
specify 'will use base option to construct url' do
|
||||
block = lambda do |url, _headers|
|
||||
expect(url).to eq('http://override/token')
|
||||
response_json
|
||||
end
|
||||
subject.send(:request, 'token', base: 'http://override/', &block)
|
||||
end
|
||||
|
||||
specify 'will not pass as option for request' do
|
||||
block = lambda do |_url, headers|
|
||||
expect(headers[:as]).to be_nil
|
||||
response_json
|
||||
end
|
||||
subject.send(:request, 'token', as: :text, &block)
|
||||
end
|
||||
|
||||
specify 'will raise error if response code is not 200' do
|
||||
expect { subject.send(:request, 'token') { response_404 } }.to raise_error
|
||||
end
|
||||
|
||||
context 'parse response body' do
|
||||
specify 'will return response body for text response' do
|
||||
expect(subject.send(:request, 'text', as: :text) { response_text }).to eq(response_text.body)
|
||||
end
|
||||
|
||||
specify 'will return response body as file for image' do
|
||||
expect(subject.send(:request, 'image') { response_image }).to be_a(Tempfile)
|
||||
end
|
||||
|
||||
specify 'will return response body as file for unknown content_type' do
|
||||
response_stream = double 'image', response_params.merge(body: 'stream', headers: { content_type: 'stream' })
|
||||
expect(subject.send(:request, 'image', as: :file) { response_stream }).to be_a(Tempfile)
|
||||
end
|
||||
end
|
||||
|
||||
context 'json error' do
|
||||
specify 'raise ResponseError given response has error json' do
|
||||
allow(response_json).to receive(:body).and_return({ errcode: 1106, errmsg: 'error message' }.to_json)
|
||||
expect { subject.send(:request, 'image', as: :file) { response_json } }.to raise_error(Wechat::ResponseError)
|
||||
end
|
||||
|
||||
specify 'raise AccessTokenExpiredError given response has error json with errorcode 40014' do
|
||||
allow(response_json).to receive(:body).and_return({ errcode: 40014, errmsg: 'error message' }.to_json)
|
||||
expect { subject.send(:request, 'image', as: :file) { response_json } }.to raise_error(Wechat::AccessTokenExpiredError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,42 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Wechat::Token::CorpAccessToken do
|
||||
let(:token_file) { Rails.root.join('access_token') }
|
||||
let(:token) { '12345' }
|
||||
let(:client) { double(:client) }
|
||||
|
||||
subject do
|
||||
Wechat::Token::CorpAccessToken.new(client, 'corpid', 'corpsecret', token_file)
|
||||
end
|
||||
|
||||
before :each do
|
||||
allow(client).to receive(:get)
|
||||
.with('gettoken', params: { corpid: 'corpid',
|
||||
corpsecret: 'corpsecret' }).and_return('access_token' => '12345', 'expires_in' => 7200)
|
||||
end
|
||||
|
||||
after :each do
|
||||
File.delete(token_file) if File.exist?(token_file)
|
||||
end
|
||||
|
||||
describe '#refresh' do
|
||||
specify 'will set access_token' do
|
||||
expect(subject.refresh).to eq(token)
|
||||
expect(subject.access_token).to eq('12345')
|
||||
end
|
||||
|
||||
specify "won't set access_token if request failed" do
|
||||
allow(client).to receive(:get).and_raise('error')
|
||||
|
||||
expect { subject.refresh }.to raise_error('error')
|
||||
expect(subject.access_token).to be_nil
|
||||
end
|
||||
|
||||
specify "won't set access_token if response value invalid" do
|
||||
allow(client).to receive(:get).and_return('rubbish')
|
||||
|
||||
expect { subject.refresh }.to raise_error
|
||||
expect(subject.access_token).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,463 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Wechat::CorpApi do
|
||||
let(:token_file) { Rails.root.join('tmp/access_token') }
|
||||
let(:jsapi_ticket_file) { Rails.root.join('tmp/jsapi_ticket') }
|
||||
|
||||
subject do
|
||||
Wechat::CorpApi.new('corpid', 'corpsecret', token_file, '1', 20, false, jsapi_ticket_file)
|
||||
end
|
||||
|
||||
before :each do
|
||||
allow(subject.access_token).to receive(:token).and_return('access_token')
|
||||
end
|
||||
|
||||
describe '#API_BASE' do
|
||||
specify 'will get correct API_BASE' do
|
||||
expect(subject.client.base).to eq Wechat::CorpApi::API_BASE
|
||||
end
|
||||
end
|
||||
|
||||
describe '#agent_list' do
|
||||
specify 'will get user/get with access_token and userid' do
|
||||
agent_list_result = { errcode: 0, errmsg: 'ok',
|
||||
agentlist: [{ agentid: '5', name: '企业小助手',
|
||||
square_logo_url: 'url', round_logo_url: 'url' },
|
||||
{ agentid: '8', name: 'HR小助手',
|
||||
square_logo_url: 'url', round_logo_url: 'url' }] }
|
||||
expect(subject.client).to receive(:get)
|
||||
.with('agent/list', params: { access_token: 'access_token' }).and_return(agent_list_result)
|
||||
expect(subject.agent_list).to eq agent_list_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#agent' do
|
||||
specify 'will get user/get with access_token and userid' do
|
||||
agentid = '1'
|
||||
agent_result = { errcode: 0, errmsg: 'ok', agentid: '1',
|
||||
name: 'NAME', square_logo_url: 'xxxxxxxx', round_logo_url: 'yyyyyyyy', description: 'desc',
|
||||
allow_userinfos: { user: [{ userid: 'id1', status: 1 },
|
||||
{ userid: 'id2', status: 1 }] },
|
||||
allow_partys: { partyid: [1] },
|
||||
allow_tags: { tagid: [1, 2, 3] },
|
||||
close: 0, redirect_domain: 'www.qq.com', report_location_flag: 0,
|
||||
isreportuser: 0, isreportenter: 0 }
|
||||
expect(subject.client).to receive(:get)
|
||||
.with('agent/get', params: { agentid: agentid, access_token: 'access_token' }).and_return(agent_result)
|
||||
expect(subject.agent(agentid)).to eq agent_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#user' do
|
||||
specify 'will get user/get with access_token and userid' do
|
||||
userid = 'userid'
|
||||
user_result = { errcode: 0, errmsg: 'ok',
|
||||
userid: 'zhangsan',
|
||||
name: '李四',
|
||||
department: [1, 2],
|
||||
position: '后台工程师',
|
||||
mobile: '15913215421',
|
||||
gender: '1',
|
||||
email: 'zhangsan@gzdev.com',
|
||||
weixinid: 'lisifordev',
|
||||
avatar: 'http://wx.qlogo.cn/mmopen/ajNVdqHZLLA3WJ6DSZUfiakYe37PKnQhBIeOQBO4czqrnZDS79FH5Wm5m4X69TBicnHFlhiafvDwklOpZeXYQQ2icg/0',
|
||||
status: 1,
|
||||
extattr: { attrs: [{ name: '爱好', value: '旅游' }, { name: '卡号', value: '1234567234' }] } }
|
||||
expect(subject.client).to receive(:get)
|
||||
.with('user/get', params: { userid: userid, access_token: 'access_token' }).and_return(user_result)
|
||||
expect(subject.user(userid)).to eq user_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#getuserinfo' do
|
||||
specify 'will get user/getuserinfo with access_token and code' do
|
||||
code = 'code'
|
||||
getuserinfo_result = { UserId: 'USERID', DeviceId: 'DEVICEID' }
|
||||
expect(subject.client).to receive(:get)
|
||||
.with('user/getuserinfo', params: { code: code, access_token: 'access_token' }).and_return(getuserinfo_result)
|
||||
expect(subject.getuserinfo(code)).to eq getuserinfo_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#convert_to_openid' do
|
||||
specify 'will get invite/send with access_token and json userid' do
|
||||
userid = 'userid'
|
||||
agentid = '1'
|
||||
convert_to_openid_request = { userid: userid, agentid: agentid }
|
||||
convert_to_openid_result = { errcode: 0, errmsg: 'ok', openid: 'oDOGms-6yCnGrRovBj2yHij5JL6E', appid: 'wxf874e15f78cc84a7' }
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('user/convert_to_openid', convert_to_openid_request.to_json, params: { access_token: 'access_token' }).and_return(convert_to_openid_result)
|
||||
expect(subject.convert_to_openid(userid)).to eq convert_to_openid_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#invite_user' do
|
||||
specify 'will get invite/send with access_token and json userid' do
|
||||
userid = 'userid'
|
||||
invite_request = { userid: userid }
|
||||
invite_result = { errcode: 0, errmsg: 'ok', type: 1 }
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('invite/send', invite_request.to_json, params: { access_token: 'access_token' }).and_return(invite_result)
|
||||
expect(subject.invite_user(userid)).to eq invite_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#user_auth_success' do
|
||||
specify 'will get user/authsucc with access_token and userid' do
|
||||
userid = 'userid'
|
||||
user_auth_result = { errcode: 0, errmsg: 'ok' }
|
||||
expect(subject.client).to receive(:get)
|
||||
.with('user/authsucc', params: { userid: userid, access_token: 'access_token' }).and_return(user_auth_result)
|
||||
expect(subject.user_auth_success(userid)).to eq user_auth_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#user_delete' do
|
||||
specify 'will get user/delete with access_token and userid' do
|
||||
userid = 'userid'
|
||||
user_delete_result = { errcode: 0, errmsg: 'deleted' }
|
||||
expect(subject.client).to receive(:get)
|
||||
.with('user/delete', params: { userid: userid, access_token: 'access_token' }).and_return(user_delete_result)
|
||||
expect(subject.user_delete(userid)).to eq user_delete_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#user_batchdelete' do
|
||||
specify 'will get user/delete with access_token and userid' do
|
||||
batchdelete_request = { useridlist: %w(6749 6110) }
|
||||
user_delete_result = { errcode: 0, errmsg: 'deleted' }
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('user/batchdelete', batchdelete_request.to_json, params: { access_token: 'access_token' }).and_return(user_delete_result)
|
||||
expect(subject.user_batchdelete(%w(6749 6110))).to eq user_delete_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#batch_job_result' do
|
||||
specify 'will get batch/getresult with access_token and userid' do
|
||||
batch_result = { errcode: 0, errmsg: 'ok', status: 1,
|
||||
type: 'replace_user', total: 3, percentage: 33, remaintime: 1,
|
||||
result: [{}, {}] }
|
||||
expect(subject.client).to receive(:get)
|
||||
.with('batch/getresult', params: { jobid: 'jobid', access_token: 'access_token' }).and_return(batch_result)
|
||||
expect(subject.batch_job_result('jobid')).to eq batch_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#batch_replaceparty' do
|
||||
specify 'will post batch/replaceparty with access_token and new department payload' do
|
||||
batch_replaceparty_request = { media_id: 'media_id' }
|
||||
batch_replaceparty_result = { errcode: 0, errmsg: 'ok', jobid: 'jobid' }
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('batch/replaceparty', batch_replaceparty_request.to_json, params: { access_token: 'access_token' }).and_return(batch_replaceparty_result)
|
||||
expect(subject.batch_replaceparty('media_id')).to eq batch_replaceparty_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#batch_replaceuser' do
|
||||
specify 'will post batch/replaceuser with access_token and new department payload' do
|
||||
batch_replaceuser_request = { media_id: 'media_id' }
|
||||
batch_replaceuser_result = { errcode: 0, errmsg: 'ok', jobid: 'jobid' }
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('batch/replaceuser', batch_replaceuser_request.to_json, params: { access_token: 'access_token' }).and_return(batch_replaceuser_result)
|
||||
expect(subject.batch_replaceuser('media_id')).to eq batch_replaceuser_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#batch_syncuser' do
|
||||
specify 'will post batch/syncuser with access_token and new department payload' do
|
||||
batch_syncuser_request = { media_id: 'media_id' }
|
||||
batch_syncuser_result = { errcode: 0, errmsg: 'ok', jobid: 'jobid' }
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('batch/syncuser', batch_syncuser_request.to_json, params: { access_token: 'access_token' }).and_return(batch_syncuser_result)
|
||||
expect(subject.batch_syncuser('media_id')).to eq batch_syncuser_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#department_create' do
|
||||
specify 'will post department/create with access_token and new department payload' do
|
||||
department_create_request = { name: '广州研发中心', parentid: '1' }
|
||||
department_create_result = { errcode: 0, errmsg: 'created', id: 2 }
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('department/create', department_create_request.to_json, params: { access_token: 'access_token' }).and_return(department_create_result)
|
||||
expect(subject.department_create('广州研发中心', '1')).to eq department_create_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#department_delete' do
|
||||
specify 'will get department/delete with access_token and id' do
|
||||
departmentid = 'departmentid'
|
||||
department_delete_result = { errcode: 0, errmsg: 'deleted' }
|
||||
expect(subject.client).to receive(:get)
|
||||
.with('department/delete', params: { access_token: 'access_token', id: departmentid }).and_return(department_delete_result)
|
||||
expect(subject.department_delete('departmentid')).to eq department_delete_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#department_update' do
|
||||
specify 'will post department/update with access_token and id' do
|
||||
departmentid = 'departmentid'
|
||||
department_update_request = { id: departmentid, name: '广研' }
|
||||
department_update_result = { errcode: 0, errmsg: 'updated' }
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('department/update', department_update_request.to_json, params: { access_token: 'access_token' }).and_return(department_update_result)
|
||||
expect(subject.department_update('departmentid', '广研')).to eq department_update_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#department' do
|
||||
specify 'will get user/get with access_token and userid' do
|
||||
departmentid = 'departmentid'
|
||||
department_result = { errcode: 0, errmsg: 'ok',
|
||||
department: [
|
||||
{ id: 2,
|
||||
name: '广州研发中心',
|
||||
parentid: 1,
|
||||
order: 10 },
|
||||
{ id: 3,
|
||||
name: '邮箱产品部',
|
||||
parentid: 2,
|
||||
order: 40 }] }
|
||||
expect(subject.client).to receive(:get)
|
||||
.with('department/list', params: { id: departmentid, access_token: 'access_token' }).and_return(department_result)
|
||||
expect(subject.department(departmentid)).to eq department_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#user_simplelist' do
|
||||
specify 'will get user/simplelist with access_token and departmentid' do
|
||||
department_id = 'department_id'
|
||||
simplelist_result = { errcode: 0, errmsg: 'ok',
|
||||
userlist: [{ userid: 'zhangsan', name: '李四', department: [1, 2] }] }
|
||||
expect(subject.client).to receive(:get)
|
||||
.with('user/simplelist', params: { department_id: department_id, fetch_child: 0, status: 0, access_token: 'access_token' })
|
||||
.and_return(simplelist_result)
|
||||
expect(subject.user_simplelist(department_id)).to eq simplelist_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#user_list' do
|
||||
specify 'will get user/list with access_token and departmentid' do
|
||||
user_list_result = { errcode: 0, errmsg: 'ok',
|
||||
userlist: [{ userid: 'zhangsan',
|
||||
name: '李四',
|
||||
department: [1, 2],
|
||||
position: '后台工程师',
|
||||
mobile: '15913215421',
|
||||
gender: '1',
|
||||
email: 'zhangsan@gzdev.com',
|
||||
weixinid: 'lisifordev',
|
||||
avatar: 'http://wx.qlogo.cn/mmopen/ajNVdqHZLLA3WJ6DSZUfiakYe37PKnQhBIeOQBO4czqrnZDS79FH5Wm5m4X69TBicnHFlhiafvDwklOpZeXYQQ2icg/0',
|
||||
status: 1,
|
||||
extattr: { attrs: [{ name: '爱好', value: '旅游' }, { name: '卡号', value: '1234567234' }] } }] }
|
||||
expect(subject.client).to receive(:get)
|
||||
.with('user/list', params: { department_id: 1, fetch_child: 0, status: 0, access_token: 'access_token' }).and_return(user_list_result)
|
||||
expect(subject.user_list(1)).to eq user_list_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#tag_create' do
|
||||
specify 'will post tag/create with access_token and new department payload' do
|
||||
tag_create_request = { tagname: 'UI', tagid: 1 }
|
||||
tag_create_result = { errcode: 0, errmsg: 'created', tagid: 1 }
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('tag/create', tag_create_request.to_json, params: { access_token: 'access_token' }).and_return(tag_create_result)
|
||||
expect(subject.tag_create('UI', 1)).to eq tag_create_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#tag_update' do
|
||||
specify 'will post tag/update with access_token and new department payload' do
|
||||
tag_update_request = { tagid: 1, tagname: 'UI Design' }
|
||||
tag_update_result = { errcode: 0, errmsg: 'updated' }
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('tag/update', tag_update_request.to_json, params: { access_token: 'access_token' }).and_return(tag_update_result)
|
||||
expect(subject.tag_update(1, 'UI Design')).to eq tag_update_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#tag_delete' do
|
||||
specify 'will get tag/delete with access_token and tagid' do
|
||||
tag_delete_result = { errcode: 0, errmsg: 'deleted' }
|
||||
expect(subject.client).to receive(:get)
|
||||
.with('tag/delete', params: { tagid: 1, access_token: 'access_token' }).and_return(tag_delete_result)
|
||||
expect(subject.tag_delete(1)).to eq tag_delete_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#tags' do
|
||||
specify 'will get tag/list with access_token' do
|
||||
tags_result = { errcode: 0, errmsg: 'ok',
|
||||
taglist: [{ tagid: 1, tagname: 'a' },
|
||||
{ tagid: 2, tagname: 'b' }] }
|
||||
expect(subject.client).to receive(:get)
|
||||
.with('tag/list', params: { access_token: 'access_token' }).and_return(tags_result)
|
||||
expect(subject.tags).to eq tags_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#tag' do
|
||||
specify 'will get user/get with access_token and tagid' do
|
||||
tag_result = { errcode: 0, errmsg: 'ok',
|
||||
userlist: [{ userid: 'zhangsan', name: '李四' }],
|
||||
partylist: [2] }
|
||||
expect(subject.client).to receive(:get)
|
||||
.with('tag/get', params: { tagid: 1, access_token: 'access_token' }).and_return(tag_result)
|
||||
expect(subject.tag(1)).to eq tag_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#tag_add_user' do
|
||||
specify 'will post tag/addtagusers with tagid, userlist(userids) and access_token' do
|
||||
tag_add_user_request = { tagid: 1, userlist: %w(6749 6110), partylist: nil }
|
||||
tag_add_user_result = { errcode: 0, errmsg: 'ok' }
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('tag/addtagusers', tag_add_user_request.to_json, params: { access_token: 'access_token' }).and_return(tag_add_user_result)
|
||||
expect(subject.tag_add_user(1, %w(6749 6110))).to eq tag_add_user_result
|
||||
end
|
||||
|
||||
specify 'will post tag/addtagusers with tagid, partylist(departmentids) and access_token' do
|
||||
tag_add_party_request = { tagid: 1, userlist: nil, partylist: [1, 2] }
|
||||
tag_add_party_result = { errcode: 0, errmsg: 'ok' }
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('tag/addtagusers', tag_add_party_request.to_json, params: { access_token: 'access_token' }).and_return(tag_add_party_result)
|
||||
expect(subject.tag_add_user(1, nil, [1, 2])).to eq tag_add_party_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#tag_del_user' do
|
||||
specify 'will post tag/deltagusers with tagid, userlist(userids) and access_token' do
|
||||
tag_del_user_request = { tagid: 1, userlist: %w(6749 6110), partylist: nil }
|
||||
tag_del_user_result = { errcode: 0, errmsg: 'deleted' }
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('tag/deltagusers', tag_del_user_request.to_json, params: { access_token: 'access_token' }).and_return(tag_del_user_result)
|
||||
expect(subject.tag_del_user(1, %w(6749 6110))).to eq tag_del_user_result
|
||||
end
|
||||
|
||||
specify 'will post tag/deltagusers with tagid, partylist(departmentids) and access_token' do
|
||||
tag_del_party_request = { tagid: 1, userlist: nil, partylist: [1, 2] }
|
||||
tag_del_party_result = { errcode: 0, errmsg: 'deleted' }
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('tag/deltagusers', tag_del_party_request.to_json, params: { access_token: 'access_token' }).and_return(tag_del_party_result)
|
||||
expect(subject.tag_del_user(1, nil, [1, 2])).to eq tag_del_party_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#menu' do
|
||||
specify 'will get menu/get with access_token and agentid' do
|
||||
menu_result = 'menu_result'
|
||||
expect(subject.client).to receive(:get).with('menu/get', params: { access_token: 'access_token', agentid: '1' }).and_return(menu_result)
|
||||
expect(subject.menu).to eq(menu_result)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#menu_delete' do
|
||||
specify 'will get menu/delete with access_token and agentid' do
|
||||
expect(subject.client).to receive(:get).with('menu/delete', params: { access_token: 'access_token', agentid: '1' }).and_return(true)
|
||||
expect(subject.menu_delete).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#menu_create' do
|
||||
specify 'will post menu/create with access_token, agentid and json_data' do
|
||||
menu = { buttons: ['a_button'] }
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('menu/create', menu.to_json, params: { access_token: 'access_token', agentid: '1' }).and_return(true)
|
||||
expect(subject.menu_create(menu)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#media' do
|
||||
specify 'will get media/get with access_token and media_id at file based api endpoint as file' do
|
||||
media_result = 'media_result'
|
||||
|
||||
expect(subject.client).to receive(:get)
|
||||
.with('media/get', params: { access_token: 'access_token', media_id: 'media_id' },
|
||||
as: :file).and_return(media_result)
|
||||
expect(subject.media('media_id')).to eq(media_result)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#media_create' do
|
||||
specify 'will post media/upload with access_token, type and media payload at file based api endpoint' do
|
||||
file = 'README.md'
|
||||
expect(subject.client).to receive(:post_file)
|
||||
.with('media/upload', file,
|
||||
params: { type: 'image', access_token: 'access_token' }).and_return(true)
|
||||
expect(subject.media_create('image', file)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#material' do
|
||||
specify 'will get material/get with access_token, media_id and agentid at file based api endpoint as file' do
|
||||
material_result = 'material_result'
|
||||
|
||||
expect(subject.client).to receive(:get)
|
||||
.with('material/get', params: { access_token: 'access_token', media_id: 'media_id', agentid: '1' },
|
||||
as: :file).and_return(material_result)
|
||||
expect(subject.material('media_id')).to eq(material_result)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#material_count' do
|
||||
specify 'will get material_count with access_token' do
|
||||
material_count_result = { errcode: 0, errmsg: 'ok',
|
||||
total_count: 37,
|
||||
image_count: 12,
|
||||
voice_count: 10,
|
||||
video_count: 3,
|
||||
file_count: 3,
|
||||
mpnews_count: 6 }
|
||||
expect(subject.client).to receive(:get)
|
||||
.with('material/get_count', params: { access_token: 'access_token', agentid: '1' }).and_return(material_count_result)
|
||||
expect(subject.material_count).to eq material_count_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#material_list' do
|
||||
specify 'will get material list with access_token' do
|
||||
material_list_request = { type: 'image', agentid: '1', offset: 0, count: 50 }
|
||||
material_list_result = { total_count: 1, item_count: 1,
|
||||
item: [{ media_id: 'media_id', name: 'name', update_time: 12345, url: 'url' }] }
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('material/batchget', material_list_request.to_json, params: { access_token: 'access_token' }).and_return(material_list_result)
|
||||
expect(subject.material_list('image', 0, 50)).to eq material_list_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#material_add' do
|
||||
specify 'will post material/add_material with access_token, type and media payload at file based api endpoint' do
|
||||
file = 'README.md'
|
||||
expect(subject.client).to receive(:post_file)
|
||||
.with('material/add_material', file,
|
||||
params: { type: 'image', access_token: 'access_token', agentid: '1' }).and_return(true)
|
||||
expect(subject.material_add('image', file)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#material_delete' do
|
||||
specify 'will post material/del_material with access_token and media_id' do
|
||||
media_id = 'media_id'
|
||||
material_delete_result = { errcode: 0, errmsg: 'deleted' }
|
||||
expect(subject.client).to receive(:get)
|
||||
.with('material/del', params: { media_id: media_id, access_token: 'access_token', agentid: '1' }).and_return(material_delete_result)
|
||||
expect(subject.material_delete(media_id)).to eq material_delete_result
|
||||
end
|
||||
end
|
||||
|
||||
describe '#message_send' do
|
||||
specify 'will post message with access_token, and json payload' do
|
||||
payload = {
|
||||
touser: 'openid',
|
||||
msgtype: 'text',
|
||||
agentid: '1',
|
||||
text: { content: 'message content' }
|
||||
}
|
||||
|
||||
expect(subject.client).to receive(:post)
|
||||
.with('message/send', payload.to_json,
|
||||
content_type: :json, params: { access_token: 'access_token' }).and_return(true)
|
||||
|
||||
expect(subject.message_send 'openid', 'message content').to be true
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,47 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Wechat::Ticket::CorpJsapiTicket do
|
||||
let(:jsapi_ticket_file) { Rails.root.join('tmp/jsapi_ticket_file') }
|
||||
let(:ticket) { 'bxLdikRXVbTPdHSM05e5u5sUoXNKd9' }
|
||||
let(:client) { double(:client) }
|
||||
let(:access_token) { double(:access_token) }
|
||||
let(:token_content) { { access_token: '12345', expires_in: 7200 } }
|
||||
|
||||
subject do
|
||||
Wechat::Ticket::CorpJsapiTicket.new(client, access_token, jsapi_ticket_file)
|
||||
end
|
||||
|
||||
before :each do
|
||||
allow(client).to receive(:get)
|
||||
.with('get_jsapi_ticket',
|
||||
params: { access_token: token_content[:access_token] })
|
||||
.and_return(errcode: 0, errmsg: 'ok', ticket: ticket, expires_in: 7200)
|
||||
|
||||
allow(access_token).to receive(:token).and_return(token_content[:access_token])
|
||||
end
|
||||
|
||||
after :each do
|
||||
File.delete(jsapi_ticket_file) if File.exist?(jsapi_ticket_file)
|
||||
end
|
||||
|
||||
describe '#ticket' do
|
||||
specify 'read from file if jsapi_ticket_file is not initialized' do
|
||||
File.open(jsapi_ticket_file, 'w') { |f| f.write({ errcode: 0, errmsg: 'ok', ticket: ticket, expires_in: 7200 }.to_json) }
|
||||
expect(subject.ticket).to eq ticket
|
||||
end
|
||||
end
|
||||
|
||||
describe '#refresh' do
|
||||
specify 'will set ticket' do
|
||||
expect(subject.refresh).to eq ticket
|
||||
expect(subject.ticket).to eq ticket
|
||||
end
|
||||
end
|
||||
|
||||
describe '#signature' do
|
||||
specify 'will get signature' do
|
||||
url = 'http://www.baidu.com?q=ming'
|
||||
expect(subject.signature(url)[:url]).to eq url
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,305 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Wechat::Message do
|
||||
let(:text_request) { request_base.merge(MsgType: 'text', Content: 'text message') }
|
||||
|
||||
let(:request_base) do
|
||||
{
|
||||
ToUserName: 'toUser',
|
||||
FromUserName: 'fromUser',
|
||||
CreateTime: '1348831860',
|
||||
MsgId: '1234567890123456'
|
||||
}
|
||||
end
|
||||
|
||||
let(:response_base) do
|
||||
{
|
||||
ToUserName: 'sender',
|
||||
FromUserName: 'receiver',
|
||||
CreateTime: 1_348_831_860,
|
||||
MsgId: '1234567890123456'
|
||||
}
|
||||
end
|
||||
|
||||
describe 'fromHash' do
|
||||
specify 'will create message' do
|
||||
message = Wechat::Message.from_hash(text_request)
|
||||
expect(message).to be_a(Wechat::Message)
|
||||
expect(message.message_hash.size).to eq(7)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'to' do
|
||||
let(:message) { Wechat::Message.from_hash(text_request) }
|
||||
specify 'will create base message' do
|
||||
reply = Wechat::Message.to('toUser')
|
||||
expect(reply).to be_a(Wechat::Message)
|
||||
expect(reply.message_hash).to include(ToUserName: 'toUser')
|
||||
expect(reply.message_hash[:CreateTime]).to be_a(Integer)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#reply' do
|
||||
let(:message) { Wechat::Message.from_hash(text_request) }
|
||||
specify 'will create base response message' do
|
||||
reply = message.reply
|
||||
expect(reply).to be_a(Wechat::Message)
|
||||
expect(reply.message_hash).to include(FromUserName: 'toUser', ToUserName: 'fromUser')
|
||||
expect(reply.message_hash[:CreateTime]).to be_a(Integer)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'parse message using as' do
|
||||
let(:image_request) { request_base.merge(MsgType: 'image', MediaId: 'media_id', PicUrl: 'pic_url') }
|
||||
let(:voice_request) { request_base.merge(MsgType: 'voice', MediaId: 'media_id', Format: 'format') }
|
||||
let(:video_request) { request_base.merge(MsgType: 'video', MediaId: 'media_id', ThumbMediaId: 'thumb_media_id') }
|
||||
let(:location_request) do
|
||||
request_base.merge(MsgType: 'location', Location_X: 'location_x', Location_Y: 'location_y',
|
||||
Scale: 'scale', Label: 'label')
|
||||
end
|
||||
|
||||
specify 'will raise error when parse message as an unkonwn type' do
|
||||
message = Wechat::Message.from_hash(text_request)
|
||||
expect { message.as(:unkown) }.to raise_error
|
||||
end
|
||||
|
||||
specify 'will get text content' do
|
||||
message = Wechat::Message.from_hash(text_request)
|
||||
expect(message.as(:text)).to eq 'text message'
|
||||
end
|
||||
|
||||
specify 'will get image file' do
|
||||
message = Wechat::Message.from_hash(image_request)
|
||||
expect(Wechat.api).to receive(:media).with('media_id')
|
||||
message.as(:image)
|
||||
end
|
||||
|
||||
specify 'will get voice file' do
|
||||
message = Wechat::Message.from_hash(voice_request)
|
||||
expect(Wechat.api).to receive(:media).with('media_id')
|
||||
message.as(:voice)
|
||||
end
|
||||
|
||||
specify 'will get video file' do
|
||||
message = Wechat::Message.from_hash(video_request)
|
||||
expect(Wechat.api).to receive(:media).with('media_id')
|
||||
message.as(:video)
|
||||
end
|
||||
|
||||
specify 'will get location information' do
|
||||
message = Wechat::Message.from_hash(location_request)
|
||||
expect(message.as :location).to eq(location_x: 'location_x', location_y: 'location_y', scale: 'scale', label: 'label')
|
||||
end
|
||||
end
|
||||
|
||||
context 'altering message fields' do
|
||||
let(:message) { Wechat::Message.from_hash(response_base) }
|
||||
describe '#to' do
|
||||
specify 'will update ToUserName field and return self' do
|
||||
expect(message.to('a user')).to eq(message)
|
||||
expect(message[:ToUserName]).to eq 'a user'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#text' do
|
||||
specify 'will update MsgType and Content field and return self' do
|
||||
expect(message.text('content')).to eq(message)
|
||||
expect(message[:MsgType]).to eq 'text'
|
||||
expect(message[:Content]).to eq 'content'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#transfer_customer_service' do
|
||||
specify 'will update MsgType and return self' do
|
||||
expect(message.transfer_customer_service).to eq(message)
|
||||
expect(message[:MsgType]).to eq 'transfer_customer_service'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#image' do
|
||||
specify 'will update MsgType and MediaId field and return self' do
|
||||
expect(message.image('media_id')).to eq(message)
|
||||
expect(message[:MsgType]).to eq 'image'
|
||||
expect(message[:Image][:MediaId]).to eq 'media_id'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#voice' do
|
||||
specify 'will update MsgType and MediaId field and return self' do
|
||||
expect(message.voice('media_id')).to eq(message)
|
||||
|
||||
expect(message[:MsgType]).to eq 'voice'
|
||||
expect(message[:Voice][:MediaId]).to eq 'media_id'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#video' do
|
||||
specify 'will update MsgType and MediaId, Title, Description field and return self' do
|
||||
expect(message.video('media_id', title: 'title', description: 'description')).to eq(message)
|
||||
|
||||
expect(message[:MsgType]).to eq 'video'
|
||||
expect(message[:Video][:MediaId]).to eq 'media_id'
|
||||
expect(message[:Video][:Title]).to eq 'title'
|
||||
expect(message[:Video][:Description]).to eq 'description'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#music' do
|
||||
specify 'will update MsgType and ThumbMediaId, Title, Description field and return self' do
|
||||
expect(message.music('thumb_media_id', 'music_url', title: 'title', description: 'description', HQ_music_url: 'hq_music_url')).to eq(message)
|
||||
|
||||
expect(message[:MsgType]).to eq 'music'
|
||||
expect(message[:Music][:Title]).to eq 'title'
|
||||
expect(message[:Music][:Description]).to eq 'description'
|
||||
expect(message[:Music][:MusicUrl]).to eq 'music_url'
|
||||
expect(message[:Music][:HQMusicUrl]).to eq 'hq_music_url'
|
||||
expect(message[:Music][:ThumbMediaId]).to eq 'thumb_media_id'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#news' do
|
||||
let(:items) do
|
||||
[
|
||||
{ title: 'title', description: 'description', url: 'url', pic_url: 'pic_url' },
|
||||
{ title: 'title', description: 'description', url: nil, pic_url: 'pic_url' }
|
||||
]
|
||||
end
|
||||
|
||||
after :each do
|
||||
expect(message[:MsgType]).to eq('news')
|
||||
expect(message[:ArticleCount]).to eq(2)
|
||||
expect(message[:Articles][0][:Title]).to eq 'title'
|
||||
expect(message[:Articles][0][:Description]).to eq 'description'
|
||||
expect(message[:Articles][0][:Url]).to eq 'url'
|
||||
expect(message[:Articles][0][:PicUrl]).to eq 'pic_url'
|
||||
expect(message[:Articles][1].key?(:Url)).to eq false
|
||||
end
|
||||
|
||||
specify 'when no block is given, whill take the items argument as an array articals hash' do
|
||||
message.news(items)
|
||||
end
|
||||
|
||||
specify 'will update MesageType, ArticleCount, Articles field and return self' do
|
||||
message.news(items) { |articals, item| articals.item item }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#to_xml' do
|
||||
let(:response) { Wechat::Message.from_hash(response_base) }
|
||||
|
||||
specify 'root is xml tag' do
|
||||
hash = Hash.from_xml(response.text('text content').to_xml)
|
||||
expect(hash.keys).to eq(['xml'])
|
||||
end
|
||||
|
||||
specify 'collection key is item' do
|
||||
xml = response.news([
|
||||
{ title: 'title1', description: 'description', url: 'url', pic_url: 'pic_url' },
|
||||
{ title: 'title2', description: 'description', url: 'url', pic_url: 'pic_url' }
|
||||
]).to_xml
|
||||
|
||||
hash = Hash.from_xml(xml)
|
||||
expect(hash['xml']['Articles']['item']).to be_a(Array)
|
||||
expect(hash['xml']['Articles']['item'].size).to eq 2
|
||||
end
|
||||
end
|
||||
|
||||
describe '#to_json' do
|
||||
specify 'can convert text message' do
|
||||
request = Wechat::Message.to('toUser').text('text content')
|
||||
expect(request.to_json).to eq({
|
||||
touser: 'toUser',
|
||||
msgtype: 'text',
|
||||
text: { content: 'text content' }
|
||||
}.to_json)
|
||||
end
|
||||
|
||||
specify 'can convert image message' do
|
||||
request = Wechat::Message.to('toUser').image('media_id')
|
||||
expect(request.to_json).to eq({
|
||||
touser: 'toUser',
|
||||
msgtype: 'image',
|
||||
image: { media_id: 'media_id' }
|
||||
}.to_json)
|
||||
end
|
||||
|
||||
specify 'can convert voice message' do
|
||||
request = Wechat::Message.to('toUser').voice('media_id')
|
||||
|
||||
expect(request.to_json).to eq({
|
||||
touser: 'toUser',
|
||||
msgtype: 'voice',
|
||||
voice: { media_id: 'media_id' }
|
||||
}.to_json)
|
||||
end
|
||||
|
||||
specify 'can convert video message' do
|
||||
request = Wechat::Message.to('toUser').video('media_id', title: 'title', description: 'description')
|
||||
|
||||
expect(request.to_json).to eq({
|
||||
touser: 'toUser',
|
||||
msgtype: 'video',
|
||||
video: {
|
||||
media_id: 'media_id',
|
||||
title: 'title',
|
||||
description: 'description'
|
||||
}
|
||||
}.to_json)
|
||||
end
|
||||
|
||||
specify 'can convert music message' do
|
||||
request = Wechat::Message.to('toUser')
|
||||
.music('thumb_media_id', 'music_url', title: 'title', description: 'description', HQ_music_url: 'hq_music_url')
|
||||
|
||||
expect(request.to_json).to eq({
|
||||
touser: 'toUser',
|
||||
msgtype: 'music',
|
||||
music: {
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
hqmusicurl: 'hq_music_url',
|
||||
musicurl: 'music_url',
|
||||
thumb_media_id: 'thumb_media_id'
|
||||
}
|
||||
}.to_json)
|
||||
end
|
||||
|
||||
specify 'can convert news message' do
|
||||
request = Wechat::Message.to('toUser')
|
||||
.news([{ title: 'title', description: 'description', url: 'url', pic_url: 'pic_url' }])
|
||||
|
||||
expect(request.to_json).to eq({
|
||||
touser: 'toUser',
|
||||
msgtype: 'news',
|
||||
news: {
|
||||
articles: [
|
||||
{
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
picurl: 'pic_url',
|
||||
url: 'url'
|
||||
}
|
||||
]
|
||||
}
|
||||
}.to_json)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#save_to!' do
|
||||
specify 'when given a model class, it will create a new model instance with json_hash and save it.' do
|
||||
model_class = double('Model Class')
|
||||
model = double('Model Instance')
|
||||
|
||||
message = Wechat::Message.to('toUser')
|
||||
expect(model_class).to receive(:new)
|
||||
.with(to_user_name: 'toUser',
|
||||
msg_type: 'text',
|
||||
content: 'text message',
|
||||
create_time: message[:CreateTime]).and_return(model)
|
||||
expect(model).to receive(:save!).and_return(true)
|
||||
|
||||
expect(message.text('text message').save_to!(model_class)).to eq(message)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,66 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Wechat::Token::PublicAccessToken do
|
||||
let(:token_file) { Rails.root.join('access_token') }
|
||||
let(:token) { '12345' }
|
||||
let(:client) { double(:client) }
|
||||
|
||||
subject do
|
||||
Wechat::Token::PublicAccessToken.new(client, 'appid', 'secret', token_file)
|
||||
end
|
||||
|
||||
before :each do
|
||||
allow(client).to receive(:get)
|
||||
.with('token', params: { grant_type: 'client_credential',
|
||||
appid: 'appid',
|
||||
secret: 'secret' }).and_return('access_token' => '12345', 'expires_in' => 7200)
|
||||
end
|
||||
|
||||
after :each do
|
||||
File.delete(token_file) if File.exist?(token_file)
|
||||
end
|
||||
|
||||
describe '#token' do
|
||||
specify 'read from file if access_token is not initialized' do
|
||||
File.open(token_file, 'w') { |f| f.write({ 'access_token' => '12345', 'expires_in' => 7200 }.to_json) }
|
||||
expect(subject.token).to eq('12345')
|
||||
end
|
||||
|
||||
specify "refresh access_token if token file didn't exist" do
|
||||
expect(File.exist? token_file).to be false
|
||||
expect(subject.token).to eq('12345')
|
||||
expect(File.exist? token_file).to be true
|
||||
end
|
||||
|
||||
specify 'refresh access_token if token file is invalid' do
|
||||
File.open(token_file, 'w') { |f| f.write('rubbish') }
|
||||
expect(subject.token).to eq('12345')
|
||||
end
|
||||
|
||||
specify 'raise exception if refresh failed' do
|
||||
allow(client).to receive(:get).and_raise('error')
|
||||
expect { subject.token }.to raise_error('error')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#refresh' do
|
||||
specify 'will set access_token' do
|
||||
expect(subject.refresh).to eq(token)
|
||||
expect(subject.access_token).to eq('12345')
|
||||
end
|
||||
|
||||
specify "won't set access_token if request failed" do
|
||||
allow(client).to receive(:get).and_raise('error')
|
||||
|
||||
expect { subject.refresh }.to raise_error('error')
|
||||
expect(subject.access_token).to be_nil
|
||||
end
|
||||
|
||||
specify "won't set access_token if response value invalid" do
|
||||
allow(client).to receive(:get).and_return('rubbish')
|
||||
|
||||
expect { subject.refresh }.to raise_error
|
||||
expect(subject.access_token).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,47 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Wechat::Ticket::PublicJsapiTicket do
|
||||
let(:jsapi_ticket_file) { Rails.root.join('tmp/jsapi_ticket_file') }
|
||||
let(:ticket) { 'bxLdikRXVbTPdHSM05e5u5sUoXNKd8' }
|
||||
let(:client) { double(:client) }
|
||||
let(:access_token) { double(:access_token) }
|
||||
let(:token_content) { { access_token: '12345', expires_in: 7200 } }
|
||||
|
||||
subject do
|
||||
Wechat::Ticket::PublicJsapiTicket.new(client, access_token, jsapi_ticket_file)
|
||||
end
|
||||
|
||||
before :each do
|
||||
allow(client).to receive(:get)
|
||||
.with('ticket/getticket',
|
||||
params: { type: 'jsapi', access_token: token_content[:access_token] })
|
||||
.and_return(errcode: 0, errmsg: 'ok', ticket: ticket, expires_in: 7200)
|
||||
|
||||
allow(access_token).to receive(:token).and_return(token_content[:access_token])
|
||||
end
|
||||
|
||||
after :each do
|
||||
File.delete(jsapi_ticket_file) if File.exist?(jsapi_ticket_file)
|
||||
end
|
||||
|
||||
describe '#ticket' do
|
||||
specify 'read from file if jsapi_ticket_file is not initialized' do
|
||||
File.open(jsapi_ticket_file, 'w') { |f| f.write({ ticket: ticket, expires_in: 7200 }.to_json) }
|
||||
expect(subject.ticket).to eq ticket
|
||||
end
|
||||
end
|
||||
|
||||
describe '#refresh' do
|
||||
specify 'will set ticket' do
|
||||
expect(subject.refresh).to eq ticket
|
||||
expect(subject.ticket).to eq ticket
|
||||
end
|
||||
end
|
||||
|
||||
describe '#signature' do
|
||||
specify 'will get signature' do
|
||||
url = 'http://www.baidu.com?q=ming'
|
||||
expect(subject.signature(url)[:url]).to eq url
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,279 @@
|
|||
require 'spec_helper'
|
||||
|
||||
include Wechat::Cipher
|
||||
|
||||
ENCODING_AES_KEY = Base64.encode64 SecureRandom.hex(16)
|
||||
|
||||
class WechatCorpController < ActionController::Base
|
||||
wechat_responder corpid: 'corpid', corpsecret: 'corpsecret', token: 'token', access_token: 'controller_access_token',
|
||||
agentid: 1, encoding_aes_key: ENCODING_AES_KEY
|
||||
end
|
||||
|
||||
RSpec.describe WechatCorpController, type: :controller do
|
||||
render_views
|
||||
|
||||
let(:message_base) do
|
||||
{
|
||||
ToUserName: 'toUser',
|
||||
FromUserName: 'fromUser',
|
||||
CreateTime: '1348831860',
|
||||
MsgId: '1234567890123456'
|
||||
}
|
||||
end
|
||||
|
||||
def signature_params(msg = {})
|
||||
xml = message_base.merge(msg).to_xml(root: :xml, skip_instruct: true)
|
||||
|
||||
encrypt = Base64.strict_encode64 encrypt(pack(xml, 'appid'), ENCODING_AES_KEY)
|
||||
xml = { Encrypt: encrypt }
|
||||
timestamp = '1234567'
|
||||
nonce = 'nonce'
|
||||
msg_signature = Digest::SHA1.hexdigest(['token', timestamp, nonce, xml[:Encrypt]].sort.join)
|
||||
{ timestamp: timestamp, nonce: nonce, xml: xml, msg_signature: msg_signature }
|
||||
end
|
||||
|
||||
def signature_echostr(echostr)
|
||||
encrypt_echostr = Base64.strict_encode64 encrypt(pack(echostr, 'appid'), ENCODING_AES_KEY)
|
||||
timestamp = '1234567'
|
||||
nonce = 'nonce'
|
||||
msg_signature = Digest::SHA1.hexdigest(['token', timestamp, nonce, encrypt_echostr].sort.join)
|
||||
{ timestamp: timestamp, nonce: nonce, echostr: encrypt_echostr, msg_signature: msg_signature }
|
||||
end
|
||||
|
||||
def xml_to_hash(xml_message)
|
||||
Hash.from_xml(xml_message)['xml'].symbolize_keys
|
||||
end
|
||||
|
||||
describe 'Verify signature' do
|
||||
it 'on create action faild' do
|
||||
post :create, signature_params.merge(msg_signature: 'invalid')
|
||||
expect(response.code).to eq '403'
|
||||
end
|
||||
|
||||
it 'on create action success' do
|
||||
post :create, signature_params(MsgType: 'voice', Voice: { MediaId: 'mediaID' })
|
||||
expect(response.code).to eq '200'
|
||||
expect(response.body.length).to eq 0
|
||||
end
|
||||
end
|
||||
|
||||
specify "echo 'echostr' param when show" do
|
||||
get :show, signature_echostr('hello')
|
||||
expect(response.body).to eq('hello')
|
||||
end
|
||||
|
||||
describe 'corp' do
|
||||
controller do
|
||||
wechat_responder corpid: 'corpid', corpsecret: 'corpsecret', token: 'token', access_token: 'controller_access_token',
|
||||
agentid: 1, encoding_aes_key: ENCODING_AES_KEY
|
||||
|
||||
on :text do |request, content|
|
||||
request.reply.text "echo: #{content}"
|
||||
end
|
||||
|
||||
on :text, with: 'mpnews' do |request|
|
||||
request.reply.news(0...1) do |article|
|
||||
article.item title: 'title', description: 'desc', pic_url: 'http://www.baidu.com/img/bdlogo.gif', url: 'http://www.baidu.com/'
|
||||
end
|
||||
end
|
||||
|
||||
on :event, with: 'subscribe' do |request|
|
||||
request.reply.text 'welcome!'
|
||||
end
|
||||
|
||||
on :event, with: 'enter_agent' do |request|
|
||||
request.reply.text 'echo: enter_agent'
|
||||
end
|
||||
|
||||
on :click, with: 'BOOK_LUNCH' do |request, key|
|
||||
request.reply.text "#{request[:FromUserName]} click #{key}"
|
||||
end
|
||||
|
||||
on :view, with: 'http://xxx.proxy.qqbrowser.cc/wechat/view_url' do |request, view|
|
||||
request.reply.text "#{request[:FromUserName]} view #{view}"
|
||||
end
|
||||
|
||||
on :scan, with: 'BINDING_QR_CODE' do |request, scan_result, scan_type|
|
||||
request.reply.text "User #{request[:FromUserName]} ScanResult #{scan_result} ScanType #{scan_type}"
|
||||
end
|
||||
|
||||
on :scan, with: 'BINDING_BARCODE' do |message, scan_result|
|
||||
if scan_result.start_with? 'CODE_39,'
|
||||
message.reply.text "User: #{message[:FromUserName]} scan barcode, result is #{scan_result.split(',')[1]}"
|
||||
end
|
||||
end
|
||||
|
||||
on :batch_job, with: 'replace_user' do |request, batch_job|
|
||||
request.reply.text "Replace user job #{batch_job[:JobId]} finished, return code #{batch_job[:ErrCode]}, return message #{batch_job[:ErrMsg]}"
|
||||
end
|
||||
end
|
||||
|
||||
specify 'will set controller wechat api and token' do
|
||||
access_token = controller.class.wechat.access_token
|
||||
expect(access_token.token_file).to eq 'controller_access_token'
|
||||
expect(controller.class.token).to eq 'token'
|
||||
expect(controller.class.agentid).to eq 1
|
||||
expect(controller.class.encrypt_mode).to eq true
|
||||
expect(controller.class.encoding_aes_key).to eq ENCODING_AES_KEY
|
||||
end
|
||||
|
||||
describe 'response' do
|
||||
it 'Verify response signature' do
|
||||
post :create, signature_params(MsgType: 'text', Content: 'hello')
|
||||
expect(response.code).to eq '200'
|
||||
expect(response.body.empty?).to eq false
|
||||
|
||||
data = Hash.from_xml(response.body)['xml']
|
||||
|
||||
msg_signature = Digest::SHA1.hexdigest [data['TimeStamp'], data['Nonce'], 'token', data['Encrypt']].sort.join
|
||||
expect(data['MsgSignature']).to eq msg_signature
|
||||
end
|
||||
|
||||
it 'on text' do
|
||||
post :create, signature_params(MsgType: 'text', Content: 'hello')
|
||||
expect(response.code).to eq '200'
|
||||
expect(response.body.empty?).to eq false
|
||||
|
||||
data = Hash.from_xml(response.body)['xml']
|
||||
|
||||
xml_message, app_id = unpack(decrypt(Base64.decode64(data['Encrypt']), ENCODING_AES_KEY))
|
||||
expect(app_id).to eq 'appid'
|
||||
expect(xml_message.empty?).to eq false
|
||||
|
||||
message = Hash.from_xml(xml_message)['xml']
|
||||
expect(message['MsgType']).to eq 'text'
|
||||
expect(message['Content']).to eq 'echo: hello'
|
||||
end
|
||||
|
||||
it 'on mpnews' do
|
||||
post :create, signature_params(MsgType: 'text', Content: 'mpnews')
|
||||
expect(response.code).to eq '200'
|
||||
expect(response.body.empty?).to eq false
|
||||
|
||||
data = Hash.from_xml(response.body)['xml']
|
||||
|
||||
xml_message, app_id = unpack(decrypt(Base64.decode64(data['Encrypt']), ENCODING_AES_KEY))
|
||||
|
||||
expect(app_id).to eq 'appid'
|
||||
expect(xml_message.empty?).to eq false
|
||||
|
||||
message = Hash.from_xml(xml_message)['xml']
|
||||
articles = { 'item' => { 'Title' => 'title',
|
||||
'Description' => 'desc',
|
||||
'PicUrl' => 'http://www.baidu.com/img/bdlogo.gif',
|
||||
'Url' => 'http://www.baidu.com/' } }
|
||||
expect(message['MsgType']).to eq 'news'
|
||||
expect(message['ArticleCount']).to eq '1'
|
||||
expect(message['Articles']).to eq articles
|
||||
end
|
||||
|
||||
it 'on subscribe' do
|
||||
post :create, signature_params(MsgType: 'event', Event: 'subscribe')
|
||||
expect(response.code).to eq '200'
|
||||
|
||||
data = Hash.from_xml(response.body)['xml']
|
||||
|
||||
xml_message, app_id = unpack(decrypt(Base64.decode64(data['Encrypt']), ENCODING_AES_KEY))
|
||||
expect(app_id).to eq 'appid'
|
||||
expect(xml_message.empty?).to eq false
|
||||
|
||||
message = Hash.from_xml(xml_message)['xml']
|
||||
expect(message['MsgType']).to eq 'text'
|
||||
expect(message['Content']).to eq 'welcome!'
|
||||
end
|
||||
|
||||
it 'on enter_agent' do
|
||||
post :create, signature_params(MsgType: 'event', Event: 'click', EventKey: 'enter_agent')
|
||||
expect(response.code).to eq '200'
|
||||
|
||||
data = Hash.from_xml(response.body)['xml']
|
||||
|
||||
xml_message, app_id = unpack(decrypt(Base64.decode64(data['Encrypt']), ENCODING_AES_KEY))
|
||||
expect(app_id).to eq 'appid'
|
||||
expect(xml_message.empty?).to eq false
|
||||
|
||||
message = Hash.from_xml(xml_message)['xml']
|
||||
expect(message['MsgType']).to eq 'text'
|
||||
expect(message['Content']).to eq 'echo: enter_agent'
|
||||
end
|
||||
|
||||
it 'on click BOOK_LUNCH' do
|
||||
post :create, signature_params(MsgType: 'event', Event: 'click', EventKey: 'BOOK_LUNCH')
|
||||
expect(response.code).to eq '200'
|
||||
|
||||
data = Hash.from_xml(response.body)['xml']
|
||||
|
||||
xml_message, app_id = unpack(decrypt(Base64.decode64(data['Encrypt']), ENCODING_AES_KEY))
|
||||
expect(app_id).to eq 'appid'
|
||||
expect(xml_message.empty?).to eq false
|
||||
|
||||
message = Hash.from_xml(xml_message)['xml']
|
||||
expect(message['MsgType']).to eq 'text'
|
||||
expect(message['Content']).to eq 'fromUser click BOOK_LUNCH'
|
||||
end
|
||||
|
||||
it 'on view http://xxx.proxy.qqbrowser.cc/wechat/view_url' do
|
||||
post :create, signature_params(MsgType: 'event', Event: 'view', EventKey: 'http://xxx.proxy.qqbrowser.cc/wechat/view_url')
|
||||
expect(response.code).to eq '200'
|
||||
|
||||
data = Hash.from_xml(response.body)['xml']
|
||||
|
||||
xml_message, app_id = unpack(decrypt(Base64.decode64(data['Encrypt']), ENCODING_AES_KEY))
|
||||
expect(app_id).to eq 'appid'
|
||||
expect(xml_message.empty?).to eq false
|
||||
|
||||
message = Hash.from_xml(xml_message)['xml']
|
||||
expect(message['MsgType']).to eq 'text'
|
||||
expect(message['Content']).to eq 'fromUser view http://xxx.proxy.qqbrowser.cc/wechat/view_url'
|
||||
end
|
||||
|
||||
it 'on BINDING_QR_CODE' do
|
||||
post :create, signature_params(FromUserName: 'userid', MsgType: 'event', Event: 'scancode_push', EventKey: 'BINDING_QR_CODE',
|
||||
ScanCodeInfo: { ScanType: 'qrcode', ScanResult: 'scan_result' })
|
||||
expect(response.code).to eq '200'
|
||||
|
||||
data = Hash.from_xml(response.body)['xml']
|
||||
|
||||
xml_message, app_id = unpack(decrypt(Base64.decode64(data['Encrypt']), ENCODING_AES_KEY))
|
||||
expect(app_id).to eq 'appid'
|
||||
expect(xml_message.empty?).to eq false
|
||||
|
||||
message = Hash.from_xml(xml_message)['xml']
|
||||
expect(message['MsgType']).to eq 'text'
|
||||
expect(message['Content']).to eq 'User userid ScanResult scan_result ScanType qrcode'
|
||||
end
|
||||
|
||||
it 'response scancode event with matched event' do
|
||||
post :create, signature_params(FromUserName: 'userid', MsgType: 'event', Event: 'scancode_waitmsg', EventKey: 'BINDING_BARCODE',
|
||||
ScanCodeInfo: { ScanType: 'qrcode', ScanResult: 'CODE_39,SAP0D00' })
|
||||
expect(response.code).to eq '200'
|
||||
|
||||
data = Hash.from_xml(response.body)['xml']
|
||||
|
||||
xml_message, app_id = unpack(decrypt(Base64.decode64(data['Encrypt']), ENCODING_AES_KEY))
|
||||
expect(app_id).to eq 'appid'
|
||||
expect(xml_message.empty?).to eq false
|
||||
|
||||
message = Hash.from_xml(xml_message)['xml']
|
||||
expect(message['MsgType']).to eq 'text'
|
||||
expect(message['Content']).to eq 'User: userid scan barcode, result is SAP0D00'
|
||||
end
|
||||
|
||||
it 'on replace_user' do
|
||||
post :create, signature_params(FromUserName: 'sys', MsgType: 'event', Event: 'batch_job_result',
|
||||
BatchJob: { JobId: 'job_id', JobType: 'replace_user', ErrCode: 0, ErrMsg: 'ok' })
|
||||
expect(response.code).to eq '200'
|
||||
|
||||
data = Hash.from_xml(response.body)['xml']
|
||||
|
||||
xml_message, app_id = unpack(decrypt(Base64.decode64(data['Encrypt']), ENCODING_AES_KEY))
|
||||
expect(app_id).to eq 'appid'
|
||||
expect(xml_message.empty?).to eq false
|
||||
|
||||
message = Hash.from_xml(xml_message)['xml']
|
||||
expect(message['MsgType']).to eq 'text'
|
||||
expect(message['Content']).to eq 'Replace user job job_id finished, return code 0, return message ok'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,362 @@
|
|||
require 'spec_helper'
|
||||
|
||||
class WechatController < ActionController::Base
|
||||
wechat_responder
|
||||
end
|
||||
|
||||
RSpec.describe WechatController, type: :controller do
|
||||
def xml_to_hash(response)
|
||||
Hash.from_xml(response.body)['xml'].symbolize_keys
|
||||
end
|
||||
|
||||
render_views
|
||||
|
||||
let(:signature_params) do
|
||||
timestamp = '1234567'
|
||||
nonce = 'nonce'
|
||||
signature = Digest::SHA1.hexdigest([ENV['WECHAT_TOKEN'], timestamp, nonce].sort.join)
|
||||
{ timestamp: timestamp, nonce: nonce, signature: signature }
|
||||
end
|
||||
|
||||
let(:message_base) do
|
||||
{
|
||||
ToUserName: 'toUser',
|
||||
FromUserName: 'fromUser',
|
||||
CreateTime: '1348831860',
|
||||
MsgId: '1234567890123456'
|
||||
}
|
||||
end
|
||||
|
||||
let(:text_message) { message_base.merge(MsgType: 'text', Content: 'text message') }
|
||||
|
||||
specify 'config responder using global config' do
|
||||
expect(controller.class.wechat).to eq(Wechat.api)
|
||||
expect(controller.class.token).to eq(Wechat.config.token)
|
||||
end
|
||||
|
||||
describe 'config responder using per controller configuration' do
|
||||
controller do
|
||||
wechat_responder appid: 'controller_appid', secret: 'controller_secret', token: 'controller_token',
|
||||
access_token: 'controller_access_token',
|
||||
agentid: 1, encoding_aes_key: 'encoding_aes_key'
|
||||
end
|
||||
specify 'will set controller wechat api and token' do
|
||||
access_token = controller.class.wechat.access_token
|
||||
expect(access_token.appid).to eq('controller_appid')
|
||||
expect(access_token.secret).to eq('controller_secret')
|
||||
expect(access_token.token_file).to eq('controller_access_token')
|
||||
expect(controller.class.token).to eq('controller_token')
|
||||
expect(controller.class.agentid).to eq(1)
|
||||
expect(controller.class.encrypt_mode).to eq(false)
|
||||
expect(controller.class.encoding_aes_key).to eq('encoding_aes_key')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Verify signature' do
|
||||
specify 'on show action' do
|
||||
get :show, signature_params.merge(signature: 'invalid_signature')
|
||||
expect(response.code).to eq('403')
|
||||
end
|
||||
|
||||
specify 'on create action' do
|
||||
post :create, signature_params.merge(signature: 'invalid_signature')
|
||||
expect(response.code).to eq('403')
|
||||
end
|
||||
end
|
||||
|
||||
specify "echo 'echostr' param when show" do
|
||||
get :show, signature_params.merge(echostr: 'hello')
|
||||
expect(response.body).to eq('hello')
|
||||
end
|
||||
|
||||
describe 'responders' do
|
||||
specify 'responders only accept :text, :image, :voice, :video, :location, :link, :event, :fallback message type' do
|
||||
[:text, :image, :voice, :video, :location, :link, :event, :fallback].each do |message_type|
|
||||
controller.class.on message_type, respond: 'response'
|
||||
end
|
||||
end
|
||||
|
||||
specify 'will raise error if message type is unkonwn' do
|
||||
expect { controller.class.on :unkonwn, respond: 'response' }.to raise_error
|
||||
end
|
||||
|
||||
specify 'responder take :with argument only for :text and :event message_type' do
|
||||
expect(controller.class.on :text, with: 'command', respond: 'response').to eq(with: 'command', respond: 'response')
|
||||
expect(controller.class.on :event, with: 'subscribe').to eq(with: 'subscribe')
|
||||
expect { controller.class.on :image, with: 'with' }.to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
describe 'responder_for' do
|
||||
controller do
|
||||
wechat_responder
|
||||
on :text, with: 'command', respond: 'string matched'
|
||||
on :text, with: /^cmd:(.*)$/, respond: 'regex matched'
|
||||
on :text, respond: 'text content'
|
||||
on :event, with: 'subscribe', respond: 'subscribe event'
|
||||
on :click, with: 'EVENTKEY', respond: 'EVENTKEY clicked'
|
||||
on :image, respond: 'image content'
|
||||
end
|
||||
|
||||
specify 'find first responder for matched type' do
|
||||
expect do |b|
|
||||
controller.class.responder_for(MsgType: 'image', &b)
|
||||
end.to yield_with_args(respond: 'image content')
|
||||
end
|
||||
|
||||
specify "find 'general text' responder if none of the text responders matches request content" do
|
||||
expect do |b|
|
||||
controller.class.responder_for(MsgType: 'text', Content: 'some text', &b)
|
||||
end.to yield_with_args({ respond: 'text content' }, 'some text')
|
||||
end
|
||||
|
||||
specify "find 'string matched' responder if request content matches string" do
|
||||
expect do |b|
|
||||
controller.class.responder_for(MsgType: 'text', Content: 'command', &b)
|
||||
end.to yield_with_args({ respond: 'string matched', with: 'command' }, 'command')
|
||||
end
|
||||
|
||||
specify "find 'regex mached' responder if request content matches regex" do
|
||||
expect do |b|
|
||||
controller.class.responder_for(MsgType: 'text', Content: 'cmd:my_command', &b)
|
||||
end.to yield_with_args({ respond: 'regex matched', with: /^cmd:(.*)$/ }, 'my_command')
|
||||
end
|
||||
|
||||
specify "find 'subscribe event' responder if event request matches event" do
|
||||
expect do |b|
|
||||
controller.class.responder_for(MsgType: 'event', Event: 'subscribe', &b)
|
||||
end.to yield_with_args({ respond: 'subscribe event', with: 'subscribe' }, 'subscribe')
|
||||
end
|
||||
|
||||
specify "find 'click' responder if event request matches click" do
|
||||
expect do |b|
|
||||
controller.class.responder_for(MsgType: 'event', Event: 'click', EventKey: 'EVENTKEY', &b)
|
||||
end.to yield_with_args({ respond: 'EVENTKEY clicked', with: 'EVENTKEY' }, 'EVENTKEY')
|
||||
end
|
||||
end
|
||||
|
||||
specify 'will respond empty if no responder for the message type' do
|
||||
post :create, signature_params.merge(xml: text_message)
|
||||
expect(response.code).to eq('200')
|
||||
expect(response.body.strip).to be_empty
|
||||
end
|
||||
|
||||
describe 'respond_to wechat helper' do
|
||||
controller do
|
||||
wechat_responder
|
||||
on :text do |_message, _content|
|
||||
wechat
|
||||
end
|
||||
end
|
||||
|
||||
specify 'will return normal' do
|
||||
expect do
|
||||
post :create, signature_params.merge(xml: text_message)
|
||||
end.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
describe 'respond_to wechat_url helper' do
|
||||
controller do
|
||||
wechat_responder
|
||||
on :text do |_message, _content|
|
||||
wechat_url
|
||||
end
|
||||
end
|
||||
|
||||
specify 'will return normal' do
|
||||
expect do
|
||||
post :create, signature_params.merge(xml: text_message)
|
||||
end.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
describe 'fallback responder' do
|
||||
controller do
|
||||
wechat_responder
|
||||
on :fallback, respond: 'fallback responder'
|
||||
end
|
||||
|
||||
specify 'will respond to any message' do
|
||||
post :create, signature_params.merge(xml: text_message)
|
||||
expect(xml_to_hash(response)[:Content]).to eq('fallback responder')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'fallback responder transfer to customer service' do
|
||||
controller do
|
||||
wechat_responder
|
||||
on :fallback do |message|
|
||||
message.reply.transfer_customer_service
|
||||
end
|
||||
end
|
||||
|
||||
specify 'will change MsgType to transfer_customer_service' do
|
||||
post :create, signature_params.merge(xml: text_message)
|
||||
expect(xml_to_hash(response)[:MsgType]).to eq 'transfer_customer_service'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'default text transfer to customer service' do
|
||||
controller do
|
||||
wechat_responder
|
||||
on :text do |request, _content|
|
||||
request.reply.transfer_customer_service
|
||||
end
|
||||
end
|
||||
|
||||
specify 'will change MsgType to transfer_customer_service' do
|
||||
post :create, signature_params.merge(xml: text_message)
|
||||
expect(xml_to_hash(response)[:MsgType]).to eq 'transfer_customer_service'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#create use cases' do
|
||||
controller do
|
||||
wechat_responder
|
||||
on :text, respond: 'text message' do |message, _content|
|
||||
message.replay.text('should not be here')
|
||||
end
|
||||
|
||||
on :text, with: 'command' do |message, content|
|
||||
message.reply.text("text: #{content}")
|
||||
end
|
||||
|
||||
on :text, with: /^cmd:(.*)$/ do |message, cmd|
|
||||
message.reply.text("cmd: #{cmd}")
|
||||
end
|
||||
|
||||
on :text, with: 'session count' do |message|
|
||||
message.session[:count] ||= 0
|
||||
message.session[:count] += 1
|
||||
message.reply.text message.session[:count]
|
||||
end
|
||||
|
||||
on :event, with: 'subscribe' do |message, event|
|
||||
message.reply.text("event: #{event}")
|
||||
end
|
||||
|
||||
on :event, with: 'unsubscribe' do |message|
|
||||
message.reply.success
|
||||
end
|
||||
|
||||
on :scan, with: 'qrscene_xxxxxx' do |request, ticket|
|
||||
request.reply.text "Unsubscribe user #{request[:FromUserName]} Ticket #{ticket}"
|
||||
end
|
||||
|
||||
on :scan, with: 'scene_id' do |request, ticket|
|
||||
request.reply.text "Subscribe user #{request[:FromUserName]} Ticket #{ticket}"
|
||||
end
|
||||
|
||||
on :event, with: 'scan' do |request|
|
||||
if request[:EventKey].present?
|
||||
request.reply.text "event scan got EventKey #{request[:EventKey]} Ticket #{request[:Ticket]}"
|
||||
end
|
||||
end
|
||||
|
||||
on :location do |message|
|
||||
message.reply.text("Latitude: #{message[:Latitude]} Longitude: #{message[:Longitude]}")
|
||||
end
|
||||
|
||||
on :image do |message|
|
||||
message.reply.text("image: #{message[:PicUrl]}")
|
||||
end
|
||||
|
||||
on :voice do |message|
|
||||
message.reply.text("voice: #{message[:MediaId]}")
|
||||
end
|
||||
|
||||
on :video do |message|
|
||||
message.reply.text("video: #{message[:MediaId]}")
|
||||
end
|
||||
|
||||
on :link do |message|
|
||||
message.reply.text("link: #{message[:Url]}")
|
||||
end
|
||||
end
|
||||
|
||||
specify 'response with respond field' do
|
||||
post :create, signature_params.merge(xml: text_message.merge(Content: 'message'))
|
||||
result = xml_to_hash(response)
|
||||
expect(result[:ToUserName]).to eq('fromUser')
|
||||
expect(result[:FromUserName]).to eq('toUser')
|
||||
expect(result[:Content]).to eq('text message')
|
||||
end
|
||||
|
||||
specify 'response text with text match' do
|
||||
post :create, signature_params.merge(xml: text_message.merge(Content: 'command'))
|
||||
expect(xml_to_hash(response)[:Content]).to eq('text: command')
|
||||
end
|
||||
|
||||
specify 'response text with regex matched' do
|
||||
post :create, signature_params.merge(xml: text_message.merge(Content: 'cmd:reload'))
|
||||
expect(xml_to_hash(response)[:Content]).to eq('cmd: reload')
|
||||
end
|
||||
|
||||
specify 'response text with session count' do
|
||||
post :create, signature_params.merge(xml: text_message.merge(Content: 'session count'))
|
||||
expect(xml_to_hash(response)[:Content]).to eq('1')
|
||||
end
|
||||
|
||||
specify 'response subscribe event with matched event' do
|
||||
event_message = message_base.merge(MsgType: 'event', Event: 'subscribe', EventKey: 'qrscene_not_exist')
|
||||
post :create, signature_params.merge(xml: event_message)
|
||||
expect(xml_to_hash(response)[:Content]).to eq('event: subscribe')
|
||||
end
|
||||
|
||||
specify 'response unsubscribe event with matched event' do
|
||||
event_message = message_base.merge(MsgType: 'event', Event: 'unsubscribe')
|
||||
post :create, signature_params.merge(xml: event_message)
|
||||
expect(response.code).to eq('200')
|
||||
expect(response.body).to eq('success')
|
||||
end
|
||||
|
||||
specify 'response subscribe scan event with matched event' do
|
||||
event_message = message_base.merge(MsgType: 'event', Event: 'subscribe', EventKey: 'qrscene_xxxxxx')
|
||||
post :create, signature_params.merge(xml: event_message.merge(Ticket: 'TICKET'))
|
||||
expect(xml_to_hash(response)[:Content]).to eq 'Unsubscribe user fromUser Ticket TICKET'
|
||||
end
|
||||
|
||||
specify 'response scan event with matched event' do
|
||||
event_message = message_base.merge(MsgType: 'event', Event: 'SCAN', EventKey: 'scene_id')
|
||||
post :create, signature_params.merge(xml: event_message.merge(Ticket: 'TICKET'))
|
||||
expect(xml_to_hash(response)[:Content]).to eq 'Subscribe user fromUser Ticket TICKET'
|
||||
end
|
||||
|
||||
specify 'response scan event with by_passed scene_id' do
|
||||
event_message = message_base.merge(MsgType: 'event', Event: 'SCAN', EventKey: 'scene_id_by_pass_scan_process')
|
||||
post :create, signature_params.merge(xml: event_message.merge(Ticket: 'TICKET'))
|
||||
expect(xml_to_hash(response)[:Content]).to eq 'event scan got EventKey scene_id_by_pass_scan_process Ticket TICKET'
|
||||
end
|
||||
|
||||
specify 'response location' do
|
||||
message = message_base.merge(MsgType: 'event', Event: 'LOCATION', Latitude: 23.137466, Longitude: 113.352425, Precision: 119.385040)
|
||||
post :create, signature_params.merge(xml: message)
|
||||
expect(xml_to_hash(response)[:Content]).to eq('Latitude: 23.137466 Longitude: 113.352425')
|
||||
end
|
||||
|
||||
specify 'response image' do
|
||||
image_message = message_base.merge(MsgType: 'image', MediaId: 'image_media_id', PicUrl: 'pic_url')
|
||||
post :create, signature_params.merge(xml: image_message)
|
||||
expect(xml_to_hash(response)[:Content]).to eq('image: pic_url')
|
||||
end
|
||||
|
||||
specify 'response voice' do
|
||||
message = message_base.merge(MsgType: 'voice', MediaId: 'voice_media_id', Format: 'format')
|
||||
post :create, signature_params.merge(xml: message)
|
||||
expect(xml_to_hash(response)[:Content]).to eq('voice: voice_media_id')
|
||||
end
|
||||
|
||||
specify 'response video' do
|
||||
message = message_base.merge(MsgType: 'video', MediaId: 'video_media_id', ThumbMediaId: 'thumb_media_id')
|
||||
post :create, signature_params.merge(xml: message)
|
||||
expect(xml_to_hash(response)[:Content]).to eq('video: video_media_id')
|
||||
end
|
||||
|
||||
specify 'response link' do
|
||||
message = message_base.merge(MsgType: 'link', Url: 'link_url', Title: 'title', Description: 'description')
|
||||
post :create, signature_params.merge(xml: message)
|
||||
expect(xml_to_hash(response)[:Content]).to eq('link: link_url')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,49 @@
|
|||
require 'codeclimate-test-reporter'
|
||||
CodeClimate::TestReporter.start
|
||||
|
||||
ENV['RAILS_ENV'] ||= 'test'
|
||||
|
||||
ENV['WECHAT_APPID'] = 'appid'
|
||||
ENV['WECHAT_SECRET'] = 'secret'
|
||||
ENV['WECHAT_TOKEN'] = 'token'
|
||||
|
||||
require File.expand_path('../dummy/config/environment', __FILE__)
|
||||
require 'rspec/rails'
|
||||
|
||||
Dir[File.join(File.dirname(__FILE__), '../spec/support/**/*.rb')].sort.each { |f| require f }
|
||||
|
||||
ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
|
||||
ActiveRecord::Migration.verbose = false
|
||||
ActiveRecord::Schema.define do
|
||||
create_table :wechat_logs do |t|
|
||||
t.string :openid, null: false, index: true
|
||||
t.text :request_raw
|
||||
t.text :response_raw
|
||||
t.text :session_raw
|
||||
t.datetime :created_at, null: false
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.mock_with :rspec
|
||||
config.infer_base_class_for_anonymous_controllers = true
|
||||
config.order = 'random'
|
||||
|
||||
# Limits the available syntax to the non-monkey patched syntax that is
|
||||
# recommended. For more details, see:
|
||||
# - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
|
||||
# - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
||||
# - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
|
||||
config.disable_monkey_patching!
|
||||
|
||||
# This setting enables warnings. It's recommended, but in some cases may
|
||||
# be too noisy due to issues in dependencies.
|
||||
config.warnings = true
|
||||
|
||||
# Allows RSpec to persist some state between runs in order to support
|
||||
# the `--only-failures` and `--next-failure` CLI options. We recommend
|
||||
# you configure your source control system to ignore this file.
|
||||
config.example_status_persistence_file_path = 'spec/examples.txt'
|
||||
end
|
||||
|
||||
RSpec::Expectations.configuration.warn_about_potential_false_positives = false
|
|
@ -0,0 +1,25 @@
|
|||
version = File.read(File.expand_path('../VERSION', __FILE__)).strip
|
||||
|
||||
# Describe your gem and declare its dependencies:
|
||||
Gem::Specification.new do |s|
|
||||
s.authors = ['Skinnyworm', 'Eric Guo']
|
||||
s.email = 'eric.guocz@gmail.com'
|
||||
s.homepage = 'https://github.com/Eric-Guo/wechat'
|
||||
|
||||
s.name = 'wechat'
|
||||
s.version = version
|
||||
s.licenses = ['MIT']
|
||||
s.summary = 'DSL for wechat message handling and API'
|
||||
s.description = 'API and message handling for WeChat in Rails'
|
||||
|
||||
s.files = Dir['{bin,lib}/**/*'] + %w(LICENSE Rakefile README.md README-CN.md CHANGELOG.md)
|
||||
s.executables << 'wechat'
|
||||
|
||||
s.add_runtime_dependency 'activerecord', '>= 3.2', '< 5.1.x'
|
||||
s.add_runtime_dependency 'nokogiri', '>=1.6.0'
|
||||
s.add_runtime_dependency 'thor'
|
||||
s.add_runtime_dependency 'http', '~> 1.0', '>= 1.0.1'
|
||||
s.add_development_dependency 'rspec-rails', '~> 3.4'
|
||||
s.add_development_dependency 'rails', '>= 3.2'
|
||||
s.add_development_dependency 'sqlite3'
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"folders":
|
||||
[
|
||||
{
|
||||
"path": ".",
|
||||
"folder_exclude_patterns": [".bundle",".idea","tmp","log","pkg"],
|
||||
"file_exclude_patterns": ["*.sublime-workspace","*.sqlite3","spec/examples.txt"]
|
||||
}
|
||||
],
|
||||
"settings":
|
||||
{
|
||||
"translate_tabs_to_spaces": true,
|
||||
"trim_trailing_white_space_on_save": true,
|
||||
"tab_size": 2
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
source 'https://ruby.taobao.org'
|
||||
source 'https://rubygems.org/'
|
||||
|
||||
gem 'rich', '1.4.6'
|
||||
gem 'kaminari'
|
||||
|
|
Loading…
Reference in New Issue