wechat 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +26 -10
- data/Rakefile +1 -3
- data/bin/wechat +38 -4
- data/lib/{wechat → action_controller}/responder.rb +47 -39
- data/lib/action_controller/wechat_responder.rb +36 -0
- data/lib/wechat.rb +5 -57
- data/lib/wechat/access_token.rb +3 -5
- data/lib/wechat/api.rb +96 -103
- data/lib/wechat/api_base.rb +31 -0
- data/lib/wechat/client.rb +14 -14
- data/lib/wechat/corp_api.rb +59 -46
- data/lib/wechat/jsapi_ticket.rb +2 -2
- data/lib/wechat/message.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d8fc6b062fba978416473332b7eeb0ebe89a6bfd
|
4
|
+
data.tar.gz: 5b7c50450e9ca878520825678580aec7f2e7b4f0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0131981fbcfa43e241ec4151a004ec702185f41641e9a67ad8cf45adf59f9372586409ee0a34f5cc9f3141dd3c44a056fd6c13d906ae3abe8bdb5c171d08bda5
|
7
|
+
data.tar.gz: 40b6edfe0146f00d27cb41ce14c5dc612892d59569fc1469355ce52245e560893c936ec43b0a159e0838435968720f4f6b00750259c6b940c9b7c998f7383dca
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## v0.4.0 (released at 9/5/2015)
|
4
|
+
|
5
|
+
* Enable the verify SSL for enterprise mode by default, as security is more importent than speed, but still can switch off by configure
|
6
|
+
* Support scancode_push/scancode_waitmsg event.
|
7
|
+
* New API method can get wechat server IP list
|
8
|
+
* New API to query/create department/media/material
|
9
|
+
* Fix can not read token_file in mingw bug, which introduce at #43
|
10
|
+
|
3
11
|
## v0.3.0 (released at 8/30/2015)
|
4
12
|
|
5
13
|
* New user group management API
|
data/README.md
CHANGED
@@ -83,7 +83,6 @@ default: &default
|
|
83
83
|
corpsecret: "corpsecret"
|
84
84
|
agentid: "1"
|
85
85
|
access_token: "C:/Users/[user_name]/wechat_access_token"
|
86
|
-
encrypt_mode: true
|
87
86
|
token: ""
|
88
87
|
encoding_aes_key: ""
|
89
88
|
|
@@ -92,8 +91,8 @@ production:
|
|
92
91
|
corpsecret: <%= ENV['WECHAT_CORPSECRET'] %>
|
93
92
|
agentid: <%= ENV['WECHAT_AGENTID'] %>
|
94
93
|
access_token: <%= ENV['WECHAT_ACCESS_TOKEN'] %>
|
95
|
-
encrypt_mode: <%= ENV['WECHAT_ENCRYPT_MODE'] %>
|
96
94
|
token: <%= ENV['WECHAT_TOKEN'] %>
|
95
|
+
skip_verify_ssl: false
|
97
96
|
encoding_aes_key: <%= ENV['WECHAT_ENCODING_AES_KEY'] %>
|
98
97
|
|
99
98
|
development:
|
@@ -103,8 +102,14 @@ test:
|
|
103
102
|
<<: *default
|
104
103
|
```
|
105
104
|
|
105
|
+
##### 配置优先级
|
106
|
+
|
106
107
|
注意在Rails项目根目录下运行`wechat`命令行工具会优先使用`config/wechat.yml`中的`default`配置,如果失败则使用`~\.wechat.yml`中的配置,以便于在生产环境下管理多个微信账号应用。
|
107
108
|
|
109
|
+
##### 配置跳过SSL认证
|
110
|
+
|
111
|
+
Wechat服务器有报道曾出现[RestClient::SSLCertificateNotVerified](http://qydev.weixin.qq.com/qa/index.php?qa=11037)错误,此时可以选择关闭SSL验证(skip_verify_ssl)。
|
112
|
+
|
108
113
|
#### 为每个Responder配置不同的appid和secret
|
109
114
|
|
110
115
|
在个别情况下,单个Rails应用可能需要处理来自多个账号的消息,此时可以配置多个responder controller。
|
@@ -134,16 +139,21 @@ wechat gems 内部不会检查权限。但因公众号类型不同,和微信
|
|
134
139
|
```
|
135
140
|
$ wechat
|
136
141
|
Wechat commands:
|
142
|
+
wechat callbackip # 获取微信服务器IP地址
|
137
143
|
wechat custom_image [OPENID, IMAGE_PATH] # 发送图片客服消息
|
138
144
|
wechat custom_music [OPENID, THUMBNAIL_PATH, MUSIC_URL] # 发送音乐客服消息
|
139
145
|
wechat custom_news [OPENID, NEWS_YAML_PATH] # 发送图文客服消息
|
140
146
|
wechat custom_text [OPENID, TEXT_MESSAGE] # 发送文字客服消息
|
141
147
|
wechat custom_video [OPENID, VIDEO_PATH] # 发送视频客服消息
|
142
148
|
wechat custom_voice [OPENID, VOICE_PATH] # 发送语音客服消息
|
149
|
+
wechat department [DEPARTMENT_ID] # 获取部门列表
|
143
150
|
wechat group_create [GROUP_NAME] # 创建分组
|
144
151
|
wechat group_delete [GROUP_ID] # 删除分组
|
145
152
|
wechat group_update [GROUP_ID, NEW_GROUP_NAME] # 修改分组名
|
146
153
|
wechat groups # 所有用户分组列表
|
154
|
+
wechat invite_user [USER_ID] # 邀请成员关注
|
155
|
+
wechat material [MEDIA_ID, PATH] # 永久媒体下载
|
156
|
+
wechat material_add [MEDIA_TYPE, PATH] # 永久媒体上传
|
147
157
|
wechat media [MEDIA_ID, PATH] # 媒体下载
|
148
158
|
wechat media_create [MEDIA_TYPE, PATH] # 媒体上传
|
149
159
|
wechat menu # 当前菜单
|
@@ -151,8 +161,9 @@ Wechat commands:
|
|
151
161
|
wechat menu_delete # 删除菜单
|
152
162
|
wechat message_send [OPENID, TEXT_MESSAGE] # 发送文字消息(仅企业号)
|
153
163
|
wechat template_message [OPENID, TEMPLATE_YAML_PATH] # 模板消息接口
|
154
|
-
wechat user [OPEN_ID] #
|
164
|
+
wechat user [OPEN_ID] # 获取用户基本信息
|
155
165
|
wechat user_change_group [OPEN_ID, TO_GROUP_ID] # 移动用户分组
|
166
|
+
wechat user_delete [USER_ID] # 删除成员
|
156
167
|
wechat user_group [OPEN_ID] # 查询用户所在分组
|
157
168
|
wechat users # 关注者列表
|
158
169
|
```
|
@@ -203,6 +214,12 @@ button:
|
|
203
214
|
-
|
204
215
|
name: "我要"
|
205
216
|
sub_button:
|
217
|
+
-
|
218
|
+
type: "scancode_waitmsg"
|
219
|
+
name: "绑定用餐二维码"
|
220
|
+
key: "BINDING_QR_CODE"
|
221
|
+
sub_button:
|
222
|
+
-
|
206
223
|
-
|
207
224
|
type: "click"
|
208
225
|
name: "预订午餐"
|
@@ -215,12 +232,6 @@ button:
|
|
215
232
|
key: "BOOK_DINNER"
|
216
233
|
sub_button:
|
217
234
|
-
|
218
|
-
-
|
219
|
-
type: "click"
|
220
|
-
name: "预订半夜餐"
|
221
|
-
key: "BOOK_NIGHT_SNACK"
|
222
|
-
sub_button:
|
223
|
-
-
|
224
235
|
-
|
225
236
|
name: "查询"
|
226
237
|
sub_button:
|
@@ -343,6 +354,11 @@ class WechatsController < ApplicationController
|
|
343
354
|
request.reply.text "收到来自#{request[:FromUserName]} 的EventKey 为 #{key} 的事件"
|
344
355
|
end
|
345
356
|
|
357
|
+
# 当收到EventKey 为二维码扫描结果事件时
|
358
|
+
on :event, with: 'BINDING_QR_CODE' do |request, scan_type, scan_result|
|
359
|
+
request.reply.text "User #{request[:FromUserName]} ScanType #{scan_type} ScanResult #{scan_result}"
|
360
|
+
end
|
361
|
+
|
346
362
|
# 处理图片信息
|
347
363
|
on :image do |request|
|
348
364
|
request.reply.image(request[:MediaId]) #直接将图片返回给用户
|
@@ -406,7 +422,7 @@ end
|
|
406
422
|
```ruby
|
407
423
|
class WechatsController < ApplicationController
|
408
424
|
# 当无任何responder处理用户信息时,转发至客服处理。
|
409
|
-
on :fallback
|
425
|
+
on :fallback do |message|
|
410
426
|
message.reply.transfer_customer_service
|
411
427
|
end
|
412
428
|
end
|
data/Rakefile
CHANGED
@@ -20,10 +20,8 @@ RDoc::Task.new(:rdoc) do |rdoc|
|
|
20
20
|
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
21
|
end
|
22
22
|
|
23
|
-
|
24
23
|
require File.join('bundler', 'gem_tasks')
|
25
24
|
require File.join('rspec', 'core', 'rake_task')
|
26
25
|
RSpec::Core::RakeTask.new(:spec)
|
27
26
|
|
28
|
-
|
29
|
-
task :default => :spec
|
27
|
+
task default: :spec
|
data/bin/wechat
CHANGED
@@ -23,11 +23,12 @@ class App < Thor
|
|
23
23
|
corpsecret = config['corpsecret']
|
24
24
|
token_file = options[:toke_file] || config['access_token'] || '/var/tmp/wechat_access_token'
|
25
25
|
agentid = config['agentid']
|
26
|
+
skip_verify_ssl = config['skip_verify_ssl']
|
26
27
|
|
27
28
|
if appid.present? && secret.present? && token_file.present?
|
28
|
-
Wechat::Api.new(appid, secret, token_file)
|
29
|
+
Wechat::Api.new(appid, secret, token_file, skip_verify_ssl)
|
29
30
|
elsif corpid.present? && corpsecret.present? && token_file.present?
|
30
|
-
Wechat::CorpApi.new(corpid, corpsecret, token_file, agentid)
|
31
|
+
Wechat::CorpApi.new(corpid, corpsecret, token_file, agentid, skip_verify_ssl)
|
31
32
|
else
|
32
33
|
puts <<-HELP
|
33
34
|
Need create ~/.wechat.yml with wechat appid and secret
|
@@ -62,6 +63,11 @@ HELP
|
|
62
63
|
package_name 'Wechat'
|
63
64
|
option :toke_file, aliases: '-t', desc: 'File to store access token'
|
64
65
|
|
66
|
+
desc 'callbackip', '获取微信服务器IP地址'
|
67
|
+
def callbackip
|
68
|
+
puts Helper.with(options).callbackip
|
69
|
+
end
|
70
|
+
|
65
71
|
desc 'groups', '所有用户分组列表'
|
66
72
|
def groups
|
67
73
|
puts Helper.with(options).groups
|
@@ -82,16 +88,31 @@ HELP
|
|
82
88
|
puts Helper.with(options).group_delete(groupid)
|
83
89
|
end
|
84
90
|
|
91
|
+
desc 'department [DEPARTMENT_ID]', '获取部门列表'
|
92
|
+
def department(departmentid)
|
93
|
+
puts Helper.with(options).department(departmentid)
|
94
|
+
end
|
95
|
+
|
85
96
|
desc 'users', '关注者列表'
|
86
97
|
def users
|
87
98
|
puts Helper.with(options).users
|
88
99
|
end
|
89
100
|
|
90
|
-
desc 'user [OPEN_ID]', '
|
101
|
+
desc 'user [OPEN_ID]', '获取用户基本信息'
|
91
102
|
def user(open_id)
|
92
103
|
puts Helper.with(options).user(open_id)
|
93
104
|
end
|
94
105
|
|
106
|
+
desc 'invite_user [USER_ID]', '邀请成员关注'
|
107
|
+
def invite_user(userid)
|
108
|
+
puts Helper.with(options).invite_user(userid)
|
109
|
+
end
|
110
|
+
|
111
|
+
desc 'user_delete [USER_ID]', '删除成员'
|
112
|
+
def user_delete(userid)
|
113
|
+
puts Helper.with(options).user_delete(userid)
|
114
|
+
end
|
115
|
+
|
95
116
|
desc 'user_group [OPEN_ID]', '查询用户所在分组'
|
96
117
|
def user_group(openid)
|
97
118
|
puts Helper.with(options).user_group(openid)
|
@@ -131,9 +152,22 @@ HELP
|
|
131
152
|
puts Helper.with(options).media_create(type, file)
|
132
153
|
end
|
133
154
|
|
155
|
+
desc 'material [MEDIA_ID, PATH]', '永久媒体下载'
|
156
|
+
def material(media_id, path)
|
157
|
+
tmp_file = Helper.with(options).material(media_id)
|
158
|
+
FileUtils.mv(tmp_file.path, path)
|
159
|
+
puts 'File downloaded'
|
160
|
+
end
|
161
|
+
|
162
|
+
desc 'material_add [MEDIA_TYPE, PATH]', '永久媒体上传'
|
163
|
+
def material_add(type, path)
|
164
|
+
file = File.new(path)
|
165
|
+
puts Helper.with(options).material_add(type, file)
|
166
|
+
end
|
167
|
+
|
134
168
|
desc 'message_send [OPENID, TEXT_MESSAGE]', '发送文字消息(仅企业号)'
|
135
169
|
def message_send(openid, text_message)
|
136
|
-
puts Helper.with(options).message_send
|
170
|
+
puts Helper.with(options).message_send openid, text_message
|
137
171
|
end
|
138
172
|
|
139
173
|
desc 'custom_text [OPENID, TEXT_MESSAGE]', '发送文字客服消息'
|
@@ -8,19 +8,18 @@ module Wechat
|
|
8
8
|
included do
|
9
9
|
skip_before_filter :verify_authenticity_token
|
10
10
|
before_filter :verify_signature, only: [:show, :create]
|
11
|
-
#delegate :wechat, to: :class
|
12
11
|
end
|
13
12
|
|
14
13
|
module ClassMethods
|
15
|
-
attr_accessor :wechat, :token, :corpid, :agentid, :encrypt_mode, :encoding_aes_key
|
14
|
+
attr_accessor :wechat, :token, :corpid, :agentid, :encrypt_mode, :skip_verify_ssl, :encoding_aes_key
|
16
15
|
|
17
16
|
def on(message_type, with: nil, respond: nil, &block)
|
18
|
-
|
17
|
+
fail 'Unknow message type' unless message_type.in? [:text, :image, :voice, :video, :location, :link, :event, :fallback]
|
19
18
|
config = respond.nil? ? {} : { respond: respond }
|
20
19
|
config.merge!(proc: block) if block_given?
|
21
20
|
|
22
21
|
if with.present? && !message_type.in?([:text, :event])
|
23
|
-
|
22
|
+
fail 'Only text and event message can take :with parameters'
|
24
23
|
else
|
25
24
|
config.merge!(with: with) if with.present?
|
26
25
|
end
|
@@ -43,8 +42,12 @@ module Wechat
|
|
43
42
|
yield(* match_responders(responders, message[:Content]))
|
44
43
|
|
45
44
|
when :event
|
46
|
-
if message[:Event]
|
45
|
+
if 'click' == message[:Event]
|
47
46
|
yield(* match_responders(responders, message[:EventKey]))
|
47
|
+
elsif %w(scancode_push scancode_waitmsg).include? message[:Event]
|
48
|
+
yield(* match_responders(responders, event_key: message[:EventKey],
|
49
|
+
scan_type: message[:ScanCodeInfo][:ScanType],
|
50
|
+
scan_result: message[:ScanCodeInfo][:ScanResult]))
|
48
51
|
else
|
49
52
|
yield(* match_responders(responders, message[:Event]))
|
50
53
|
end
|
@@ -66,6 +69,8 @@ module Wechat
|
|
66
69
|
|
67
70
|
if condition.is_a? Regexp
|
68
71
|
memo[:scoped] ||= [responder] + $LAST_MATCH_INFO.captures if value =~ condition
|
72
|
+
elsif value.is_a? Hash
|
73
|
+
memo[:scoped] ||= [responder, value[:scan_type], value[:scan_result]] if value[:event_key] == condition
|
69
74
|
else
|
70
75
|
memo[:scoped] ||= [responder, value] if value == condition
|
71
76
|
end
|
@@ -85,20 +90,7 @@ module Wechat
|
|
85
90
|
|
86
91
|
def create
|
87
92
|
request = Wechat::Message.from_hash(post_xml)
|
88
|
-
response =
|
89
|
-
responder ||= self.class.responders(:fallback).first
|
90
|
-
|
91
|
-
next if responder.nil?
|
92
|
-
case
|
93
|
-
when responder[:respond]
|
94
|
-
request.reply.text responder[:respond]
|
95
|
-
when responder[:proc]
|
96
|
-
define_singleton_method :process, responder[:proc]
|
97
|
-
send(:process, *args.unshift(request))
|
98
|
-
else
|
99
|
-
next
|
100
|
-
end
|
101
|
-
end
|
93
|
+
response = run_responder(request)
|
102
94
|
|
103
95
|
if response.respond_to? :to_xml
|
104
96
|
render xml: process_response(response)
|
@@ -110,33 +102,32 @@ module Wechat
|
|
110
102
|
private
|
111
103
|
|
112
104
|
def verify_signature
|
105
|
+
signature = params[:signature] || params[:msg_signature]
|
106
|
+
|
107
|
+
render text: 'Forbidden', status: 403 if signature != Digest::SHA1.hexdigest(content_to_verify)
|
108
|
+
end
|
109
|
+
|
110
|
+
def content_to_verify
|
113
111
|
array = [self.class.token, params[:timestamp], params[:nonce]]
|
114
|
-
signature = params[:signature]
|
115
112
|
|
116
113
|
# 默认使用明文方式验证, 企业号验证加密签名
|
117
114
|
if params[:signature].blank? && params[:msg_signature]
|
118
|
-
|
119
|
-
if params[:echostr]
|
115
|
+
if params[:echostr].present?
|
120
116
|
array << params[:echostr]
|
121
117
|
else
|
122
|
-
array <<
|
118
|
+
array << request_encrypt_content
|
123
119
|
end
|
124
120
|
end
|
125
121
|
|
126
|
-
|
127
|
-
render text: 'Forbidden', status: 403 if signature != Digest::SHA1.hexdigest(str)
|
122
|
+
array.compact.collect(&:to_s).sort.join
|
128
123
|
end
|
129
124
|
|
130
125
|
def post_xml
|
131
126
|
data = request_content
|
132
127
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
if encrypt_msg.present?
|
137
|
-
content, @app_id = unpack(decrypt(Base64.decode64(encrypt_msg), self.class.encoding_aes_key))
|
138
|
-
data = Hash.from_xml(content)
|
139
|
-
end
|
128
|
+
if self.class.encrypt_mode && request_encrypt_content.present?
|
129
|
+
content, @app_id = unpack(decrypt(Base64.decode64(request_encrypt_content), self.class.encoding_aes_key))
|
130
|
+
data = Hash.from_xml(content)
|
140
131
|
end
|
141
132
|
|
142
133
|
HashWithIndifferentAccess.new_from_hash_copying_default(data.fetch('xml', {})).tap do |msg|
|
@@ -144,16 +135,29 @@ module Wechat
|
|
144
135
|
end
|
145
136
|
end
|
146
137
|
|
138
|
+
def run_responder(request)
|
139
|
+
self.class.responder_for(request) do |responder, *args|
|
140
|
+
responder ||= self.class.responders(:fallback).first
|
141
|
+
|
142
|
+
next if responder.nil?
|
143
|
+
case
|
144
|
+
when responder[:respond]
|
145
|
+
request.reply.text responder[:respond]
|
146
|
+
when responder[:proc]
|
147
|
+
define_singleton_method :process, responder[:proc]
|
148
|
+
send(:process, *args.unshift(request))
|
149
|
+
else
|
150
|
+
next
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
147
155
|
def process_response(response)
|
148
156
|
msg = response.to_xml
|
149
157
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
if data['xml']['Encrypt']
|
154
|
-
encrypt = Base64.strict_encode64 encrypt(pack(msg, @app_id), self.class.encoding_aes_key)
|
155
|
-
msg = gen_msg(encrypt, params[:timestamp], params[:nonce])
|
156
|
-
end
|
158
|
+
if self.class.encrypt_mode && request_encrypt_content.present?
|
159
|
+
encrypt = Base64.strict_encode64(encrypt(pack(msg, @app_id), self.class.encoding_aes_key))
|
160
|
+
msg = gen_msg(encrypt, params[:timestamp], params[:nonce])
|
157
161
|
end
|
158
162
|
|
159
163
|
msg
|
@@ -169,6 +173,10 @@ module Wechat
|
|
169
173
|
}.to_xml(root: 'xml', children: 'item', skip_instruct: true, skip_types: true)
|
170
174
|
end
|
171
175
|
|
176
|
+
def request_encrypt_content
|
177
|
+
request_content['xml']['Encrypt']
|
178
|
+
end
|
179
|
+
|
172
180
|
def request_content
|
173
181
|
params[:xml].nil? ? Hash.from_xml(request.raw_post) : { 'xml' => params[:xml] }
|
174
182
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module ActionController
|
2
|
+
module WechatResponder
|
3
|
+
def wechat_responder(opts = {})
|
4
|
+
include Wechat::Responder
|
5
|
+
|
6
|
+
self.corpid = opts[:corpid] || Wechat.config.corpid
|
7
|
+
self.agentid = opts[:agentid] || Wechat.config.agentid
|
8
|
+
self.encrypt_mode = opts[:encrypt_mode] || Wechat.config.encrypt_mode || corpid.present?
|
9
|
+
self.skip_verify_ssl = opts[:skip_verify_ssl]
|
10
|
+
self.token = opts[:token] || Wechat.config.token
|
11
|
+
self.encoding_aes_key = opts[:encoding_aes_key] || Wechat.config.encoding_aes_key
|
12
|
+
|
13
|
+
if opts.empty?
|
14
|
+
self.wechat = Wechat.api
|
15
|
+
else
|
16
|
+
if corpid.present?
|
17
|
+
self.wechat = Wechat::CorpApi.new(corpid, opts[:corpsecret], opts[:access_token], agentid, skip_verify_ssl)
|
18
|
+
else
|
19
|
+
self.wechat = Wechat::Api.new(opts[:appid], opts[:secret], opts[:access_token], skip_verify_ssl, opts[:jsapi_ticket])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
if defined? Base
|
26
|
+
class << Base
|
27
|
+
include WechatResponder
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
if defined? API
|
32
|
+
class << API
|
33
|
+
include WechatResponder
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/wechat.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
require 'wechat/api'
|
2
2
|
require 'wechat/corp_api'
|
3
|
+
require 'action_controller/wechat_responder'
|
3
4
|
|
4
5
|
module Wechat
|
5
6
|
autoload :Message, 'wechat/message'
|
6
|
-
autoload :Responder, '
|
7
|
+
autoload :Responder, 'action_controller/responder'
|
7
8
|
autoload :Cipher, 'wechat/cipher'
|
8
9
|
|
9
10
|
class AccessTokenExpiredError < StandardError; end
|
@@ -32,6 +33,7 @@ module Wechat
|
|
32
33
|
token: ENV['WECHAT_TOKEN'],
|
33
34
|
access_token: ENV['WECHAT_ACCESS_TOKEN'],
|
34
35
|
encrypt_mode: ENV['WECHAT_ENCRYPT_MODE'],
|
36
|
+
skip_verify_ssl: ENV['WECHAT_SKIP_VERIFY_SSL'],
|
35
37
|
encoding_aes_key: ENV['WECHAT_ENCODING_AES_KEY'] }
|
36
38
|
config.symbolize_keys!
|
37
39
|
config[:access_token] ||= Rails.root.join('tmp/access_token').to_s
|
@@ -42,63 +44,9 @@ module Wechat
|
|
42
44
|
|
43
45
|
def self.api
|
44
46
|
if config.corpid.present?
|
45
|
-
@api ||=
|
47
|
+
@api ||= CorpApi.new(config.corpid, config.corpsecret, config.access_token, config.agentid, config.skip_verify_ssl)
|
46
48
|
else
|
47
|
-
@api ||=
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
if defined? ActionController::Base
|
53
|
-
class ActionController::Base
|
54
|
-
def self.wechat_responder(opts = {})
|
55
|
-
send(:include, Wechat::Responder)
|
56
|
-
if opts.empty?
|
57
|
-
self.corpid = Wechat.config.corpid
|
58
|
-
self.wechat = Wechat.api
|
59
|
-
self.agentid = Wechat.config.agentid
|
60
|
-
self.token = Wechat.config.token
|
61
|
-
self.encrypt_mode = Wechat.config.encrypt_mode
|
62
|
-
self.encoding_aes_key = Wechat.config.encoding_aes_key
|
63
|
-
else
|
64
|
-
self.corpid = opts[:corpid]
|
65
|
-
if corpid.present?
|
66
|
-
self.wechat = Wechat::CorpApi.new(opts[:corpid], opts[:corpsecret], opts[:access_token], opts[:agentid])
|
67
|
-
else
|
68
|
-
self.wechat = Wechat::Api.new(opts[:appid], opts[:secret], opts[:access_token], opts[:jsapi_ticket])
|
69
|
-
end
|
70
|
-
self.agentid = opts[:agentid]
|
71
|
-
self.token = opts[:token]
|
72
|
-
self.encrypt_mode = opts[:encrypt_mode]
|
73
|
-
self.encoding_aes_key = opts[:encoding_aes_key]
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
if defined? ActionController::API
|
80
|
-
class ActionController::API
|
81
|
-
def self.wechat_responder(opts = {})
|
82
|
-
send(:include, Wechat::Responder)
|
83
|
-
if opts.empty?
|
84
|
-
self.corpid = Wechat.config.corpid
|
85
|
-
self.wechat = Wechat.api
|
86
|
-
self.agentid = Wechat.config.agentid
|
87
|
-
self.token = Wechat.config.token
|
88
|
-
self.encrypt_mode = Wechat.config.encrypt_mode
|
89
|
-
self.encoding_aes_key = Wechat.config.encoding_aes_key
|
90
|
-
else
|
91
|
-
self.corpid = opts[:corpid]
|
92
|
-
if corpid.present?
|
93
|
-
self.wechat = Wechat::CorpApi.new(opts[:corpid], opts[:corpsecret], opts[:access_token], opts[:agentid])
|
94
|
-
else
|
95
|
-
self.wechat = Wechat::Api.new(opts[:appid], opts[:secret], opts[:access_token], opts[:jsapi_ticket])
|
96
|
-
end
|
97
|
-
self.agentid = opts[:agentid]
|
98
|
-
self.token = opts[:token]
|
99
|
-
self.encrypt_mode = opts[:encrypt_mode]
|
100
|
-
self.encoding_aes_key = opts[:encoding_aes_key]
|
101
|
-
end
|
49
|
+
@api ||= Api.new(config.appid, config.secret, config.access_token, config.jsapi_ticket, config.skip_verify_ssl)
|
102
50
|
end
|
103
51
|
end
|
104
52
|
end
|
data/lib/wechat/access_token.rb
CHANGED
@@ -11,12 +11,10 @@ module Wechat
|
|
11
11
|
|
12
12
|
def token
|
13
13
|
begin
|
14
|
-
@token_data ||= JSON.parse(File.read(token_file
|
14
|
+
@token_data ||= JSON.parse(File.read(token_file))
|
15
15
|
created_at = token_data['created_at'].to_i
|
16
16
|
expires_in = token_data['expires_in'].to_i
|
17
|
-
if Time.now.to_i - created_at >= expires_in - 3 * 60
|
18
|
-
raise 'token_data may be expired'
|
19
|
-
end
|
17
|
+
fail 'token_data may be expired' if Time.now.to_i - created_at >= expires_in - 3 * 60
|
20
18
|
rescue
|
21
19
|
refresh
|
22
20
|
end
|
@@ -34,7 +32,7 @@ module Wechat
|
|
34
32
|
|
35
33
|
def valid_token(token_data)
|
36
34
|
access_token = token_data['access_token']
|
37
|
-
|
35
|
+
fail "Response didn't have access_token" if access_token.blank?
|
38
36
|
access_token
|
39
37
|
end
|
40
38
|
end
|
data/lib/wechat/api.rb
CHANGED
@@ -1,109 +1,102 @@
|
|
1
|
+
require 'wechat/api_base'
|
1
2
|
require 'wechat/client'
|
2
3
|
require 'wechat/access_token'
|
3
4
|
require 'wechat/jsapi_ticket'
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
end
|
101
|
-
|
102
|
-
def with_access_token(params = {}, tries = 2)
|
103
|
-
params ||= {}
|
104
|
-
yield(params.merge(access_token: access_token.token))
|
105
|
-
rescue Wechat::AccessTokenExpiredError
|
106
|
-
access_token.refresh
|
107
|
-
retry unless (tries -= 1).zero?
|
6
|
+
module Wechat
|
7
|
+
class Api < ApiBase
|
8
|
+
attr_reader :jsapi_ticket
|
9
|
+
|
10
|
+
API_BASE = 'https://api.weixin.qq.com/cgi-bin/'
|
11
|
+
FILE_BASE = 'http://file.api.weixin.qq.com/cgi-bin/'
|
12
|
+
OAUTH2_BASE = 'https://api.weixin.qq.com/sns/oauth2/'
|
13
|
+
|
14
|
+
def initialize(appid, secret, token_file, skip_verify_ssl, jsapi_ticket_file = '/var/tmp/wechat_jsapi_ticket')
|
15
|
+
@client = Client.new(API_BASE, skip_verify_ssl)
|
16
|
+
@access_token = AccessToken.new(@client, appid, secret, token_file)
|
17
|
+
@jsapi_ticket = JsapiTicket.new(@client, @access_token, jsapi_ticket_file)
|
18
|
+
end
|
19
|
+
|
20
|
+
def groups
|
21
|
+
get('groups/get')
|
22
|
+
end
|
23
|
+
|
24
|
+
def group_create(group_name)
|
25
|
+
post 'groups/create', JSON.generate(group: { name: group_name })
|
26
|
+
end
|
27
|
+
|
28
|
+
def group_update(groupid, new_group_name)
|
29
|
+
post 'groups/update', JSON.generate(group: { id: groupid, name: new_group_name })
|
30
|
+
end
|
31
|
+
|
32
|
+
def group_delete(groupid)
|
33
|
+
post 'groups/delete', JSON.generate(group: { id: groupid })
|
34
|
+
end
|
35
|
+
|
36
|
+
def users(nextid = nil)
|
37
|
+
params = { params: { next_openid: nextid } } if nextid.present?
|
38
|
+
get('user/get', params || {})
|
39
|
+
end
|
40
|
+
|
41
|
+
def user(openid)
|
42
|
+
get('user/info', params: { openid: openid })
|
43
|
+
end
|
44
|
+
|
45
|
+
def user_group(openid)
|
46
|
+
post 'groups/getid', JSON.generate(openid: openid)
|
47
|
+
end
|
48
|
+
|
49
|
+
def user_change_group(openid, to_groupid)
|
50
|
+
post 'groups/members/update', JSON.generate(openid: openid, to_groupid: to_groupid)
|
51
|
+
end
|
52
|
+
|
53
|
+
def menu
|
54
|
+
get('menu/get')
|
55
|
+
end
|
56
|
+
|
57
|
+
def menu_delete
|
58
|
+
get('menu/delete')
|
59
|
+
end
|
60
|
+
|
61
|
+
def menu_create(menu)
|
62
|
+
# 微信不接受7bit escaped json(eg \uxxxx), 中文必须UTF-8编码, 这可能是个安全漏洞
|
63
|
+
post('menu/create', JSON.generate(menu))
|
64
|
+
end
|
65
|
+
|
66
|
+
def media(media_id)
|
67
|
+
get 'media/get', params: { media_id: media_id }, base: FILE_BASE, as: :file
|
68
|
+
end
|
69
|
+
|
70
|
+
def media_create(type, file)
|
71
|
+
post 'media/upload', { upload: { media: file } }, params: { type: type }, base: FILE_BASE
|
72
|
+
end
|
73
|
+
|
74
|
+
def material(media_id)
|
75
|
+
get 'material/get', params: { media_id: media_id }, base: FILE_BASE, as: :file
|
76
|
+
end
|
77
|
+
|
78
|
+
def material_add(type, file)
|
79
|
+
post 'material/add_material', { upload: { media: file } }, params: { type: type }, base: FILE_BASE
|
80
|
+
end
|
81
|
+
|
82
|
+
def custom_message_send(message)
|
83
|
+
post 'message/custom/send', message.to_json, content_type: :json
|
84
|
+
end
|
85
|
+
|
86
|
+
def template_message_send(message)
|
87
|
+
post 'message/template/send', message.to_json, content_type: :json
|
88
|
+
end
|
89
|
+
|
90
|
+
# http://mp.weixin.qq.com/wiki/17/c0f37d5704f0b64713d5d2c37b468d75.html
|
91
|
+
# 第二步:通过code换取网页授权access_token
|
92
|
+
def web_access_token(code)
|
93
|
+
params = {
|
94
|
+
appid: access_token.appid,
|
95
|
+
secret: access_token.secret,
|
96
|
+
code: code,
|
97
|
+
grant_type: 'authorization_code'
|
98
|
+
}
|
99
|
+
get 'access_token', params: params, base: OAUTH2_BASE
|
100
|
+
end
|
108
101
|
end
|
109
102
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Wechat
|
2
|
+
class ApiBase
|
3
|
+
attr_reader :access_token, :client
|
4
|
+
|
5
|
+
def callbackip
|
6
|
+
get('getcallbackip')
|
7
|
+
end
|
8
|
+
|
9
|
+
protected
|
10
|
+
|
11
|
+
def get(path, headers = {})
|
12
|
+
with_access_token(headers[:params]) do |params|
|
13
|
+
client.get path, headers.merge(params: params)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def post(path, payload, headers = {})
|
18
|
+
with_access_token(headers[:params]) do |params|
|
19
|
+
client.post path, payload, headers.merge(params: params)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def with_access_token(params = {}, tries = 2)
|
24
|
+
params ||= {}
|
25
|
+
yield(params.merge(access_token: access_token.token))
|
26
|
+
rescue AccessTokenExpiredError
|
27
|
+
access_token.refresh
|
28
|
+
retry unless (tries -= 1).zero?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/wechat/client.rb
CHANGED
@@ -2,13 +2,14 @@ require 'rest_client'
|
|
2
2
|
|
3
3
|
module Wechat
|
4
4
|
class Client
|
5
|
-
attr_reader :base
|
5
|
+
attr_reader :base, :verify_ssl
|
6
6
|
|
7
|
-
def initialize(base)
|
7
|
+
def initialize(base, skip_verify_ssl)
|
8
8
|
@base = base
|
9
|
+
@verify_ssl = !skip_verify_ssl
|
9
10
|
end
|
10
11
|
|
11
|
-
def get(path, header = {}
|
12
|
+
def get(path, header = {})
|
12
13
|
request(path, header) do |url, header|
|
13
14
|
if verify_ssl
|
14
15
|
RestClient.get(url, header)
|
@@ -18,7 +19,7 @@ module Wechat
|
|
18
19
|
end
|
19
20
|
end
|
20
21
|
|
21
|
-
def post(path, payload, header = {}
|
22
|
+
def post(path, payload, header = {})
|
22
23
|
request(path, header) do |url, header|
|
23
24
|
if verify_ssl
|
24
25
|
RestClient.post(url, payload, header)
|
@@ -29,22 +30,22 @@ module Wechat
|
|
29
30
|
end
|
30
31
|
|
31
32
|
def request(path, header = {}, &block)
|
32
|
-
url = "#{header.delete(:base) ||
|
33
|
+
url = "#{header.delete(:base) || base}#{path}"
|
33
34
|
as = header.delete(:as)
|
34
|
-
header.merge!(:
|
35
|
+
header.merge!(accept: :json)
|
35
36
|
response = yield(url, header)
|
36
37
|
|
37
|
-
|
38
|
+
fail "Request not OK, response code #{response.code}" if response.code != 200
|
38
39
|
parse_response(response, as || :json) do |parse_as, data|
|
39
40
|
break data unless parse_as == :json && data['errcode'].present?
|
40
41
|
|
41
42
|
case data['errcode']
|
42
43
|
when 0 # for request didn't expect results
|
43
44
|
data
|
44
|
-
when
|
45
|
-
|
45
|
+
when 42001, 40014 # 42001: access_token超时, 40014:不合法的access_token
|
46
|
+
fail AccessTokenExpiredError
|
46
47
|
else
|
47
|
-
|
48
|
+
fail ResponseError.new(data['errcode'], data['errmsg'])
|
48
49
|
end
|
49
50
|
end
|
50
51
|
end
|
@@ -54,9 +55,9 @@ module Wechat
|
|
54
55
|
def parse_response(response, as)
|
55
56
|
content_type = response.headers[:content_type]
|
56
57
|
parse_as = {
|
57
|
-
|
58
|
-
|
59
|
-
}.
|
58
|
+
%r{^application\/json} => :json,
|
59
|
+
%r{^image\/.*} => :file
|
60
|
+
}.each_with_object([]) { |match, memo| memo << match[1] if content_type =~ match[0] }.first || as || :text
|
60
61
|
|
61
62
|
case parse_as
|
62
63
|
when :file
|
@@ -68,7 +69,6 @@ module Wechat
|
|
68
69
|
|
69
70
|
when :json
|
70
71
|
data = JSON.parse(response.body.gsub /[\u0000-\u001f]+/, '')
|
71
|
-
|
72
72
|
else
|
73
73
|
data = response.body
|
74
74
|
end
|
data/lib/wechat/corp_api.rb
CHANGED
@@ -1,66 +1,79 @@
|
|
1
|
+
require 'wechat/api_base'
|
1
2
|
require 'wechat/client'
|
2
3
|
require 'wechat/access_token'
|
3
4
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
module Wechat
|
6
|
+
class CorpAccessToken < AccessToken
|
7
|
+
def refresh
|
8
|
+
data = client.get('gettoken', params: { corpid: appid, corpsecret: secret })
|
9
|
+
data.merge!(created_at: Time.now.to_i)
|
10
|
+
File.write(token_file, data.to_json) if valid_token(data)
|
11
|
+
@token_data = data
|
12
|
+
end
|
10
13
|
end
|
11
|
-
end
|
12
14
|
|
13
|
-
class
|
14
|
-
|
15
|
+
class CorpApi < ApiBase
|
16
|
+
attr_reader :agentid
|
15
17
|
|
16
|
-
|
18
|
+
API_BASE = 'https://qyapi.weixin.qq.com/cgi-bin/'
|
17
19
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
def initialize(appid, secret, token_file, agentid, skip_verify_ssl)
|
21
|
+
@client = Client.new(API_BASE, skip_verify_ssl)
|
22
|
+
@access_token = CorpAccessToken.new(@client, appid, secret, token_file)
|
23
|
+
@agentid = agentid
|
24
|
+
end
|
23
25
|
|
24
|
-
|
25
|
-
|
26
|
-
|
26
|
+
def user(userid)
|
27
|
+
get('user/get', params: { userid: userid })
|
28
|
+
end
|
27
29
|
|
28
|
-
|
29
|
-
|
30
|
-
|
30
|
+
def invite_user(userid)
|
31
|
+
post 'invite/send', JSON.generate(userid: userid)
|
32
|
+
end
|
31
33
|
|
32
|
-
|
33
|
-
|
34
|
-
|
34
|
+
def user_auth_success(userid)
|
35
|
+
get('user/authsucc', params: { userid: userid })
|
36
|
+
end
|
35
37
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
end
|
38
|
+
def user_delete(userid)
|
39
|
+
get('user/delete', params: { userid: userid })
|
40
|
+
end
|
40
41
|
|
41
|
-
|
42
|
-
|
43
|
-
|
42
|
+
def department(departmentid = 1)
|
43
|
+
get('department/list', params: { id: departmentid })
|
44
|
+
end
|
44
45
|
|
45
|
-
|
46
|
+
def menu
|
47
|
+
get('menu/get', params: { agentid: agentid })
|
48
|
+
end
|
46
49
|
|
47
|
-
|
48
|
-
|
49
|
-
client.get path, headers.merge(params: params), false
|
50
|
+
def menu_delete
|
51
|
+
get('menu/delete', params: { agentid: agentid })
|
50
52
|
end
|
51
|
-
end
|
52
53
|
|
53
|
-
|
54
|
-
|
55
|
-
|
54
|
+
def menu_create(menu)
|
55
|
+
# 微信不接受7bit escaped json(eg \uxxxx), 中文必须UTF-8编码, 这可能是个安全漏洞
|
56
|
+
post 'menu/create', JSON.generate(menu), params: { agentid: agentid }
|
56
57
|
end
|
57
|
-
end
|
58
58
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
59
|
+
def media(media_id)
|
60
|
+
get 'media/get', params: { media_id: media_id }, as: :file
|
61
|
+
end
|
62
|
+
|
63
|
+
def media_create(type, file)
|
64
|
+
post 'media/upload', { upload: { media: file } }, params: { type: type }
|
65
|
+
end
|
66
|
+
|
67
|
+
def material(media_id)
|
68
|
+
get 'material/get', params: { media_id: media_id, agentid: agentid }, as: :file
|
69
|
+
end
|
70
|
+
|
71
|
+
def material_add(type, file)
|
72
|
+
post 'material/add_material', { upload: { media: file } }, params: { type: type, agentid: agentid }
|
73
|
+
end
|
74
|
+
|
75
|
+
def message_send(openid, message)
|
76
|
+
post 'message/send', Message.to(openid).text(message).agent_id(agentid).to_json, content_type: :json
|
77
|
+
end
|
65
78
|
end
|
66
79
|
end
|
data/lib/wechat/jsapi_ticket.rb
CHANGED
@@ -23,7 +23,7 @@ module Wechat
|
|
23
23
|
created_at = jsapi_ticket_data['created_at'].to_i
|
24
24
|
expires_in = jsapi_ticket_data['expires_in'].to_i
|
25
25
|
if Time.now.to_i - created_at >= expires_in - 3 * 60
|
26
|
-
|
26
|
+
fail 'jsapi_ticket may be expired'
|
27
27
|
end
|
28
28
|
rescue
|
29
29
|
refresh
|
@@ -67,7 +67,7 @@ module Wechat
|
|
67
67
|
|
68
68
|
def valid_ticket(jsapi_ticket_data)
|
69
69
|
ticket = jsapi_ticket_data['ticket'] || jsapi_ticket_data[:ticket]
|
70
|
-
|
70
|
+
fail "Response didn't have ticket" if ticket.blank?
|
71
71
|
ticket
|
72
72
|
end
|
73
73
|
end
|
data/lib/wechat/message.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: wechat
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Skinnyworm
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-
|
12
|
+
date: 2015-09-05 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -79,15 +79,17 @@ files:
|
|
79
79
|
- README.md
|
80
80
|
- Rakefile
|
81
81
|
- bin/wechat
|
82
|
+
- lib/action_controller/responder.rb
|
83
|
+
- lib/action_controller/wechat_responder.rb
|
82
84
|
- lib/wechat.rb
|
83
85
|
- lib/wechat/access_token.rb
|
84
86
|
- lib/wechat/api.rb
|
87
|
+
- lib/wechat/api_base.rb
|
85
88
|
- lib/wechat/cipher.rb
|
86
89
|
- lib/wechat/client.rb
|
87
90
|
- lib/wechat/corp_api.rb
|
88
91
|
- lib/wechat/jsapi_ticket.rb
|
89
92
|
- lib/wechat/message.rb
|
90
|
-
- lib/wechat/responder.rb
|
91
93
|
homepage: https://github.com/Eric-Guo/wechat
|
92
94
|
licenses:
|
93
95
|
- MIT
|