wechat 0.2.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 +7 -0
- data/CHANGELOG.md +13 -0
- data/LICENSE +21 -0
- data/README.md +405 -0
- data/Rakefile +29 -0
- data/bin/wechat +172 -0
- data/lib/wechat.rb +104 -0
- data/lib/wechat/access_token.rb +41 -0
- data/lib/wechat/api.rb +85 -0
- data/lib/wechat/cipher.rb +72 -0
- data/lib/wechat/client.rb +79 -0
- data/lib/wechat/corp_api.rb +66 -0
- data/lib/wechat/jsapi_ticket.rb +86 -0
- data/lib/wechat/message.rb +176 -0
- data/lib/wechat/responder.rb +177 -0
- metadata +115 -0
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'rest_client'
|
2
|
+
|
3
|
+
module Wechat
|
4
|
+
class Client
|
5
|
+
attr_reader :base
|
6
|
+
|
7
|
+
def initialize(base)
|
8
|
+
@base = base
|
9
|
+
end
|
10
|
+
|
11
|
+
def get(path, header = {}, verify_ssl = true)
|
12
|
+
request(path, header) do |url, header|
|
13
|
+
if verify_ssl
|
14
|
+
RestClient.get(url, header)
|
15
|
+
else
|
16
|
+
RestClient::Request.execute(url: url, method: :get, headers: header, verify_ssl: OpenSSL::SSL::VERIFY_NONE)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def post(path, payload, header = {}, verify_ssl = true)
|
22
|
+
request(path, header) do |url, header|
|
23
|
+
if verify_ssl
|
24
|
+
RestClient.post(url, payload, header)
|
25
|
+
else
|
26
|
+
RestClient::Request.execute(url: url, method: :post, payload: payload, headers: header, verify_ssl: OpenSSL::SSL::VERIFY_NONE)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def request(path, header = {}, &block)
|
32
|
+
url = "#{header.delete(:base) || self.base}#{path}"
|
33
|
+
as = header.delete(:as)
|
34
|
+
header.merge!(:accept => :json)
|
35
|
+
response = yield(url, header)
|
36
|
+
|
37
|
+
raise "Request not OK, response code #{response.code}" if response.code != 200
|
38
|
+
parse_response(response, as || :json) do |parse_as, data|
|
39
|
+
break data unless parse_as == :json && data['errcode'].present?
|
40
|
+
|
41
|
+
case data['errcode']
|
42
|
+
when 0 # for request didn't expect results
|
43
|
+
data
|
44
|
+
when 42_001, 40_014 # 42001: access_token超时, 40014:不合法的access_token
|
45
|
+
raise AccessTokenExpiredError
|
46
|
+
else
|
47
|
+
raise ResponseError.new(data['errcode'], data['errmsg'])
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def parse_response(response, as)
|
55
|
+
content_type = response.headers[:content_type]
|
56
|
+
parse_as = {
|
57
|
+
/^application\/json/ => :json,
|
58
|
+
/^image\/.*/ => :file
|
59
|
+
}.inject([]){ |memo, match| memo << match[1] if content_type =~ match[0]; memo }.first || as || :text
|
60
|
+
|
61
|
+
case parse_as
|
62
|
+
when :file
|
63
|
+
file = Tempfile.new('tmp')
|
64
|
+
file.binmode
|
65
|
+
file.write(response.body)
|
66
|
+
file.close
|
67
|
+
data = file
|
68
|
+
|
69
|
+
when :json
|
70
|
+
data = JSON.parse(response.body.gsub /[\u0000-\u001f]+/, '')
|
71
|
+
|
72
|
+
else
|
73
|
+
data = response.body
|
74
|
+
end
|
75
|
+
|
76
|
+
yield(parse_as, data)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'wechat/client'
|
2
|
+
require 'wechat/access_token'
|
3
|
+
|
4
|
+
class Wechat::CorpAccessToken < Wechat::AccessToken
|
5
|
+
def refresh
|
6
|
+
data = client.get('gettoken', { params: { corpid: appid, corpsecret: secret }}, false)
|
7
|
+
data.merge!(created_at: Time.now.to_i)
|
8
|
+
File.open(token_file, 'w') { |f| f.write(data.to_json) } if valid_token(data)
|
9
|
+
@token_data = data
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Wechat::CorpApi
|
14
|
+
attr_reader :access_token, :client, :agentid
|
15
|
+
|
16
|
+
API_BASE = 'https://qyapi.weixin.qq.com/cgi-bin/'
|
17
|
+
|
18
|
+
def initialize(appid, secret, token_file, agentid)
|
19
|
+
@client = Wechat::Client.new(API_BASE)
|
20
|
+
@access_token = Wechat::CorpAccessToken.new(@client, appid, secret, token_file)
|
21
|
+
@agentid = agentid
|
22
|
+
end
|
23
|
+
|
24
|
+
def user(userid)
|
25
|
+
get('user/get', params: { userid: userid })
|
26
|
+
end
|
27
|
+
|
28
|
+
def menu
|
29
|
+
get('menu/get', params: { agentid: agentid })
|
30
|
+
end
|
31
|
+
|
32
|
+
def menu_delete
|
33
|
+
get('menu/delete', params: { agentid: agentid })
|
34
|
+
end
|
35
|
+
|
36
|
+
def menu_create(menu)
|
37
|
+
# 微信不接受7bit escaped json(eg \uxxxx), 中文必须UTF-8编码, 这可能是个安全漏洞
|
38
|
+
post 'menu/create', JSON.generate(menu), { params: { agentid: agentid } }
|
39
|
+
end
|
40
|
+
|
41
|
+
def message_send(message)
|
42
|
+
post 'message/send', message.agent_id(agentid).to_json, content_type: :json
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
def get(path, headers = {})
|
48
|
+
with_access_token(headers[:params]) do |params|
|
49
|
+
client.get path, headers.merge(params: params), false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def post(path, payload, headers = {})
|
54
|
+
with_access_token(headers[:params]) do |params|
|
55
|
+
client.post path, payload, headers.merge(params: params), false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def with_access_token(params = {}, tries = 2)
|
60
|
+
params ||= {}
|
61
|
+
yield(params.merge(access_token: access_token.token))
|
62
|
+
rescue Wechat::AccessTokenExpiredError
|
63
|
+
access_token.refresh
|
64
|
+
retry unless (tries -= 1).zero?
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
|
3
|
+
module Wechat
|
4
|
+
class JsapiTicket
|
5
|
+
attr_reader :client, :access_token, :jsapi_ticket_file, :jsapi_ticket_data
|
6
|
+
|
7
|
+
def initialize(client, access_token, jsapi_ticket_file)
|
8
|
+
@client = client
|
9
|
+
@access_token = access_token
|
10
|
+
@jsapi_ticket_file = jsapi_ticket_file
|
11
|
+
end
|
12
|
+
|
13
|
+
# 获取微信 jssdk 签名所需的 jsapi_ticket, 返回具有如下结构的 hash:
|
14
|
+
# {
|
15
|
+
# "errcode":0,
|
16
|
+
# "errmsg":"ok",
|
17
|
+
# "ticket":"bxLdikRXVbTPdHSM05e5u5sUoXNKd8-41ZO3MhKoyN5OfkWITDGgnr2fwJ0m9E8NYzWKVZvdVtaUgWvsdshFKA",
|
18
|
+
# "expires_in":7200
|
19
|
+
# }
|
20
|
+
def ticket
|
21
|
+
begin
|
22
|
+
@jsapi_ticket_data ||= JSON.parse(File.read(jsapi_ticket_file))
|
23
|
+
created_at = jsapi_ticket_data['created_at'].to_i
|
24
|
+
expires_in = jsapi_ticket_data['expires_in'].to_i
|
25
|
+
if Time.now.to_i - created_at >= expires_in - 3 * 60
|
26
|
+
raise 'jsapi_ticket may be expired'
|
27
|
+
end
|
28
|
+
rescue
|
29
|
+
refresh
|
30
|
+
end
|
31
|
+
valid_ticket(@jsapi_ticket_data)
|
32
|
+
end
|
33
|
+
|
34
|
+
# 刷新 jsapi_ticket
|
35
|
+
def refresh
|
36
|
+
data = client.get('ticket/getticket', params: { access_token: access_token.token, type: 'jsapi' })
|
37
|
+
data.merge!(created_at: Time.now.to_i)
|
38
|
+
File.open(jsapi_ticket_file, 'w') { |f| f.write(data.to_json) } if valid_ticket(data)
|
39
|
+
@jsapi_ticket_data = data
|
40
|
+
end
|
41
|
+
|
42
|
+
# 获取 jssdk 签名及注册所需其他参数, 返回具有如下结构的 hash:
|
43
|
+
# params = {
|
44
|
+
# noncestr: noncestr,
|
45
|
+
# timestamp: timestamp,
|
46
|
+
# jsapi_ticket: ticket,
|
47
|
+
# url: url,
|
48
|
+
# signature: signature
|
49
|
+
# }
|
50
|
+
def signature(url)
|
51
|
+
timestamp = Time.now.to_i
|
52
|
+
noncestr = generate_noncestr
|
53
|
+
params = {
|
54
|
+
noncestr: noncestr,
|
55
|
+
timestamp: timestamp,
|
56
|
+
jsapi_ticket: ticket,
|
57
|
+
url: url
|
58
|
+
}
|
59
|
+
pairs = params.keys.sort.map do |key|
|
60
|
+
"#{key}=#{params[key]}"
|
61
|
+
end
|
62
|
+
result = Digest::SHA1.hexdigest pairs.join('&')
|
63
|
+
params.merge(signature: result)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def valid_ticket(jsapi_ticket_data)
|
69
|
+
ticket = jsapi_ticket_data['ticket'] || jsapi_ticket_data[:ticket]
|
70
|
+
raise "Response didn't have ticket" if ticket.blank?
|
71
|
+
ticket
|
72
|
+
end
|
73
|
+
|
74
|
+
# 生成随机字符串
|
75
|
+
# @param Integer length 长度, 默认为16
|
76
|
+
# @return String
|
77
|
+
def generate_noncestr(length = 16)
|
78
|
+
chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
79
|
+
str = ''
|
80
|
+
1.upto(length) do |i|
|
81
|
+
str += chars[rand(chars.length)]
|
82
|
+
end
|
83
|
+
str
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
module Wechat
|
2
|
+
class Message
|
3
|
+
class << self
|
4
|
+
def from_hash(message_hash)
|
5
|
+
new(message_hash)
|
6
|
+
end
|
7
|
+
|
8
|
+
def to(to_user)
|
9
|
+
new(ToUserName: to_user, CreateTime: Time.now.to_i)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class ArticleBuilder
|
14
|
+
attr_reader :items
|
15
|
+
delegate :count, to: :items
|
16
|
+
def initialize
|
17
|
+
@items = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def item(title: 'title', description: nil, pic_url: nil, url: nil)
|
21
|
+
items << { Title: title, Description: description, PicUrl: pic_url, Url: url }.reject { |_k, v| v.nil? }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :message_hash
|
26
|
+
|
27
|
+
def initialize(message_hash)
|
28
|
+
@message_hash = message_hash || {}
|
29
|
+
end
|
30
|
+
|
31
|
+
def [](key)
|
32
|
+
message_hash[key]
|
33
|
+
end
|
34
|
+
|
35
|
+
def reply
|
36
|
+
Message.new(
|
37
|
+
ToUserName: message_hash[:FromUserName],
|
38
|
+
FromUserName: message_hash[:ToUserName],
|
39
|
+
CreateTime: Time.now.to_i
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
def as(type)
|
44
|
+
case type
|
45
|
+
when :text
|
46
|
+
message_hash[:Content]
|
47
|
+
|
48
|
+
when :image, :voice, :video
|
49
|
+
Wechat.api.media(message_hash[:MediaId])
|
50
|
+
|
51
|
+
when :location
|
52
|
+
message_hash.slice(:Location_X, :Location_Y, :Scale, :Label).inject({}) { |results, value|
|
53
|
+
results[value[0].to_s.underscore.to_sym] = value[1]; results }
|
54
|
+
else
|
55
|
+
raise "Don't know how to parse message as #{type}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def to(openid)
|
60
|
+
update(ToUserName: openid)
|
61
|
+
end
|
62
|
+
|
63
|
+
def agent_id(agentid)
|
64
|
+
update(AgentId: agentid)
|
65
|
+
end
|
66
|
+
|
67
|
+
def text(content)
|
68
|
+
update(MsgType: 'text', Content: content)
|
69
|
+
end
|
70
|
+
|
71
|
+
def image(media_id)
|
72
|
+
update(MsgType: 'image', Image: { MediaId: media_id })
|
73
|
+
end
|
74
|
+
|
75
|
+
def voice(media_id)
|
76
|
+
update(MsgType: 'voice', Voice: { MediaId: media_id })
|
77
|
+
end
|
78
|
+
|
79
|
+
def video(media_id, opts = {})
|
80
|
+
video_fields = camelize_hash_keys({ media_id: media_id }.merge(opts.slice(:title, :description)))
|
81
|
+
update(MsgType: 'video', Video: video_fields)
|
82
|
+
end
|
83
|
+
|
84
|
+
def music(thumb_media_id, music_url, opts = {})
|
85
|
+
music_fields = camelize_hash_keys(opts.slice(:title, :description, :HQ_music_url).merge(music_url: music_url, thumb_media_id: thumb_media_id))
|
86
|
+
update(MsgType: 'music', Music: music_fields)
|
87
|
+
end
|
88
|
+
|
89
|
+
def news(collection, &block)
|
90
|
+
if block_given?
|
91
|
+
article = ArticleBuilder.new
|
92
|
+
collection.each { |item| yield(article, item) }
|
93
|
+
items = article.items
|
94
|
+
else
|
95
|
+
items = collection.collect do |item|
|
96
|
+
camelize_hash_keys(item.symbolize_keys.slice(:title, :description, :pic_url, :url).reject { |_k, v| v.nil? })
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
update(MsgType: 'news', ArticleCount: items.count,
|
101
|
+
Articles: items.collect { |item| camelize_hash_keys(item) })
|
102
|
+
end
|
103
|
+
|
104
|
+
def template(opts = {})
|
105
|
+
template_fields = camelize_hash_keys(opts.symbolize_keys.slice(:template_id, :topcolor, :url, :data))
|
106
|
+
update(MsgType: 'template', Template: template_fields)
|
107
|
+
end
|
108
|
+
|
109
|
+
def to_xml
|
110
|
+
message_hash.to_xml(root: 'xml', children: 'item', skip_instruct: true, skip_types: true)
|
111
|
+
end
|
112
|
+
|
113
|
+
TO_JSON_KEY_MAP = {
|
114
|
+
'ToUserName' => 'touser',
|
115
|
+
'MediaId' => 'media_id',
|
116
|
+
'ThumbMediaId' => 'thumb_media_id',
|
117
|
+
'TemplateId' => 'template_id'
|
118
|
+
}
|
119
|
+
|
120
|
+
TO_JSON_ALLOWED = %w(touser msgtype content image voice video music news articles template agentid)
|
121
|
+
|
122
|
+
def to_json
|
123
|
+
json_hash = deep_recursive(message_hash) do |key, value|
|
124
|
+
key = key.to_s
|
125
|
+
[(TO_JSON_KEY_MAP[key] || key.downcase), value]
|
126
|
+
end
|
127
|
+
|
128
|
+
json_hash = json_hash.select { |k, _v| TO_JSON_ALLOWED.include? k }
|
129
|
+
case json_hash['msgtype']
|
130
|
+
when 'text'
|
131
|
+
json_hash['text'] = { 'content' => json_hash.delete('content') }
|
132
|
+
when 'news'
|
133
|
+
json_hash['news'] = { 'articles' => json_hash.delete('articles') }
|
134
|
+
when 'template'
|
135
|
+
json_hash.merge! json_hash['template']
|
136
|
+
end
|
137
|
+
JSON.generate(json_hash)
|
138
|
+
end
|
139
|
+
|
140
|
+
def save_to!(model_class)
|
141
|
+
model = model_class.new(underscore_hash_keys(message_hash))
|
142
|
+
model.save!
|
143
|
+
self
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
def camelize_hash_keys(hash)
|
149
|
+
deep_recursive(hash) { |key, value| [key.to_s.camelize.to_sym, value] }
|
150
|
+
end
|
151
|
+
|
152
|
+
def underscore_hash_keys(hash)
|
153
|
+
deep_recursive(hash) { |key, value| [key.to_s.underscore.to_sym, value] }
|
154
|
+
end
|
155
|
+
|
156
|
+
def update(fields = {})
|
157
|
+
message_hash.merge!(fields)
|
158
|
+
self
|
159
|
+
end
|
160
|
+
|
161
|
+
def deep_recursive(hash, &block)
|
162
|
+
hash.inject({}) do |memo, val|
|
163
|
+
key, value = *val
|
164
|
+
case value.class.name
|
165
|
+
when 'Hash'
|
166
|
+
value = deep_recursive(value, &block)
|
167
|
+
when 'Array'
|
168
|
+
value = value.collect { |item| item.is_a?(Hash) ? deep_recursive(item, &block) : item }
|
169
|
+
end
|
170
|
+
|
171
|
+
key, value = yield(key, value)
|
172
|
+
memo.merge!(key => value)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
require 'English'
|
2
|
+
|
3
|
+
module Wechat
|
4
|
+
module Responder
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
include Cipher
|
7
|
+
|
8
|
+
included do
|
9
|
+
skip_before_filter :verify_authenticity_token
|
10
|
+
before_filter :verify_signature, only: [:show, :create]
|
11
|
+
#delegate :wechat, to: :class
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
attr_accessor :wechat, :token, :corpid, :agentid, :encrypt_mode, :encoding_aes_key
|
16
|
+
|
17
|
+
def on(message_type, with: nil, respond: nil, &block)
|
18
|
+
raise 'Unknow message type' unless message_type.in? [:text, :image, :voice, :video, :location, :link, :event, :fallback]
|
19
|
+
config = respond.nil? ? {} : { respond: respond }
|
20
|
+
config.merge!(proc: block) if block_given?
|
21
|
+
|
22
|
+
if with.present? && !message_type.in?([:text, :event])
|
23
|
+
raise 'Only text and event message can take :with parameters'
|
24
|
+
else
|
25
|
+
config.merge!(with: with) if with.present?
|
26
|
+
end
|
27
|
+
|
28
|
+
responders(message_type) << config
|
29
|
+
config
|
30
|
+
end
|
31
|
+
|
32
|
+
def responders(type)
|
33
|
+
@responders ||= {}
|
34
|
+
@responders[type] ||= []
|
35
|
+
end
|
36
|
+
|
37
|
+
def responder_for(message, &block)
|
38
|
+
message_type = message[:MsgType].to_sym
|
39
|
+
responders = responders(message_type)
|
40
|
+
|
41
|
+
case message_type
|
42
|
+
when :text
|
43
|
+
yield(* match_responders(responders, message[:Content]))
|
44
|
+
|
45
|
+
when :event
|
46
|
+
if message[:Event] == 'click'
|
47
|
+
yield(* match_responders(responders, message[:EventKey]))
|
48
|
+
else
|
49
|
+
yield(* match_responders(responders, message[:Event]))
|
50
|
+
end
|
51
|
+
else
|
52
|
+
yield(responders.first)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def match_responders(responders, value)
|
59
|
+
matched = responders.inject({ scoped: nil, general: nil }) do |matched, responder|
|
60
|
+
condition = responder[:with]
|
61
|
+
|
62
|
+
if condition.nil?
|
63
|
+
matched[:general] ||= [responder, value]
|
64
|
+
next matched
|
65
|
+
end
|
66
|
+
|
67
|
+
if condition.is_a? Regexp
|
68
|
+
matched[:scoped] ||= [responder] + $LAST_MATCH_INFO.captures if value =~ condition
|
69
|
+
else
|
70
|
+
matched[:scoped] ||= [responder, value] if value == condition
|
71
|
+
end
|
72
|
+
matched
|
73
|
+
end
|
74
|
+
matched[:scoped] || matched[:general]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def show
|
79
|
+
if self.class.corpid.present?
|
80
|
+
echostr, _corp_id = unpack(decrypt(Base64.decode64(params[:echostr]), self.class.encoding_aes_key))
|
81
|
+
render text: echostr
|
82
|
+
else
|
83
|
+
render text: params[:echostr]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def create
|
88
|
+
request = Wechat::Message.from_hash(post_xml)
|
89
|
+
response = self.class.responder_for(request) do |responder, *args|
|
90
|
+
responder ||= self.class.responders(:fallback).first
|
91
|
+
|
92
|
+
next if responder.nil?
|
93
|
+
case
|
94
|
+
when responder[:respond]
|
95
|
+
request.reply.text responder[:respond]
|
96
|
+
when responder[:proc]
|
97
|
+
define_singleton_method :process, responder[:proc]
|
98
|
+
send(:process, *args.unshift(request))
|
99
|
+
else
|
100
|
+
next
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
if response.respond_to? :to_xml
|
105
|
+
render xml: process_response(response)
|
106
|
+
else
|
107
|
+
render nothing: true, status: 200, content_type: 'text/html'
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def verify_signature
|
114
|
+
array = [self.class.token, params[:timestamp], params[:nonce]]
|
115
|
+
signature = params[:signature]
|
116
|
+
|
117
|
+
# 默认使用明文方式验证, 企业号验证加密签名
|
118
|
+
if params[:signature].blank? && params[:msg_signature]
|
119
|
+
signature = params[:msg_signature]
|
120
|
+
if params[:echostr]
|
121
|
+
array << params[:echostr]
|
122
|
+
else
|
123
|
+
array << request_content['xml']['Encrypt']
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
str = array.compact.collect(&:to_s).sort.join
|
128
|
+
render text: 'Forbidden', status: 403 if signature != Digest::SHA1.hexdigest(str)
|
129
|
+
end
|
130
|
+
|
131
|
+
def post_xml
|
132
|
+
data = request_content
|
133
|
+
|
134
|
+
# 如果是加密模式解密
|
135
|
+
if self.class.encrypt_mode || self.class.corpid.present?
|
136
|
+
encrypt_msg = data['xml']['Encrypt']
|
137
|
+
if encrypt_msg.present?
|
138
|
+
content, @app_id = unpack(decrypt(Base64.decode64(encrypt_msg), self.class.encoding_aes_key))
|
139
|
+
data = Hash.from_xml(content)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
HashWithIndifferentAccess.new_from_hash_copying_default(data.fetch('xml', {})).tap do |msg|
|
144
|
+
msg[:Event].downcase! if msg[:Event]
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def process_response(response)
|
149
|
+
msg = response.to_xml
|
150
|
+
|
151
|
+
# 返回加密消息
|
152
|
+
if self.class.encrypt_mode || self.class.corpid.present?
|
153
|
+
data = request_content
|
154
|
+
if data['xml']['Encrypt']
|
155
|
+
encrypt = Base64.strict_encode64 encrypt(pack(msg, @app_id), self.class.encoding_aes_key)
|
156
|
+
msg = gen_msg(encrypt, params[:timestamp], params[:nonce])
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
msg
|
161
|
+
end
|
162
|
+
|
163
|
+
def gen_msg(encrypt, timestamp, nonce)
|
164
|
+
msg_sign = Digest::SHA1.hexdigest [self.class.token, encrypt, timestamp, nonce].compact.collect(&:to_s).sort.join
|
165
|
+
|
166
|
+
{ Encrypt: encrypt,
|
167
|
+
MsgSignature: msg_sign,
|
168
|
+
TimeStamp: timestamp,
|
169
|
+
Nonce: nonce
|
170
|
+
}.to_xml(root: 'xml', children: 'item', skip_instruct: true, skip_types: true)
|
171
|
+
end
|
172
|
+
|
173
|
+
def request_content
|
174
|
+
params[:xml].nil? ? Hash.from_xml(request.raw_post) : { 'xml' => params[:xml] }
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|