wechat 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|