gfd_wechat 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +321 -0
  3. data/LICENSE +21 -0
  4. data/README-CN.md +815 -0
  5. data/README.md +844 -0
  6. data/bin/wechat +520 -0
  7. data/lib/action_controller/wechat_responder.rb +72 -0
  8. data/lib/generators/wechat/config_generator.rb +36 -0
  9. data/lib/generators/wechat/install_generator.rb +20 -0
  10. data/lib/generators/wechat/menu_generator.rb +21 -0
  11. data/lib/generators/wechat/redis_store_generator.rb +16 -0
  12. data/lib/generators/wechat/session_generator.rb +36 -0
  13. data/lib/generators/wechat/templates/MENU_README +3 -0
  14. data/lib/generators/wechat/templates/app/controllers/wechats_controller.rb +12 -0
  15. data/lib/generators/wechat/templates/app/models/wechat_config.rb +46 -0
  16. data/lib/generators/wechat/templates/app/models/wechat_session.rb +17 -0
  17. data/lib/generators/wechat/templates/config/initializers/wechat_redis_store.rb +42 -0
  18. data/lib/generators/wechat/templates/config/wechat.yml +72 -0
  19. data/lib/generators/wechat/templates/config/wechat_menu.yml +6 -0
  20. data/lib/generators/wechat/templates/config/wechat_menu_android.yml +15 -0
  21. data/lib/generators/wechat/templates/db/config_migration.rb.erb +40 -0
  22. data/lib/generators/wechat/templates/db/session_migration.rb.erb +10 -0
  23. data/lib/wechat/api.rb +54 -0
  24. data/lib/wechat/api_base.rb +63 -0
  25. data/lib/wechat/api_loader.rb +145 -0
  26. data/lib/wechat/cipher.rb +66 -0
  27. data/lib/wechat/concern/common.rb +217 -0
  28. data/lib/wechat/controller_api.rb +96 -0
  29. data/lib/wechat/corp_api.rb +168 -0
  30. data/lib/wechat/helpers.rb +47 -0
  31. data/lib/wechat/http_client.rb +112 -0
  32. data/lib/wechat/message.rb +265 -0
  33. data/lib/wechat/mp_api.rb +46 -0
  34. data/lib/wechat/responder.rb +308 -0
  35. data/lib/wechat/signature.rb +10 -0
  36. data/lib/wechat/ticket/corp_jsapi_ticket.rb +14 -0
  37. data/lib/wechat/ticket/jsapi_base.rb +84 -0
  38. data/lib/wechat/ticket/public_jsapi_ticket.rb +14 -0
  39. data/lib/wechat/token/access_token_base.rb +53 -0
  40. data/lib/wechat/token/corp_access_token.rb +13 -0
  41. data/lib/wechat/token/public_access_token.rb +13 -0
  42. data/lib/wechat.rb +52 -0
  43. metadata +195 -0
@@ -0,0 +1,265 @@
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_users = '', towxname: nil, send_ignore_reprint: 0)
9
+ if towxname.present?
10
+ new(ToWxName: towxname, CreateTime: Time.now.to_i)
11
+ elsif send_ignore_reprint == 1
12
+ new(ToUserName: to_users, CreateTime: Time.now.to_i, send_ignore_reprint: send_ignore_reprint)
13
+ else
14
+ new(ToUserName: to_users, CreateTime: Time.now.to_i)
15
+ end
16
+ end
17
+
18
+ def to_mass(tag_id: nil, send_ignore_reprint: 0)
19
+ if tag_id
20
+ new(filter: { is_to_all: false, tag_id: tag_id }, send_ignore_reprint: send_ignore_reprint)
21
+ else
22
+ new(filter: { is_to_all: true }, send_ignore_reprint: send_ignore_reprint)
23
+ end
24
+ end
25
+ end
26
+
27
+ class ArticleBuilder
28
+ attr_reader :items
29
+ delegate :count, to: :items
30
+ def initialize
31
+ @items = []
32
+ end
33
+ end
34
+
35
+ class NewsArticleBuilder < ArticleBuilder
36
+ def item(title: 'title', description: nil, pic_url: nil, url: nil)
37
+ items << { Title: title, Description: description, PicUrl: pic_url, Url: url }.reject { |_k, v| v.nil? }
38
+ end
39
+ end
40
+
41
+ class MpNewsArticleBuilder < ArticleBuilder
42
+ def item(thumb_media_id:, title:, content:, author: nil, content_source_url: nil, digest: nil, show_cover_pic: '0')
43
+ items << { Thumb_Media_ID: thumb_media_id, Author: author, Title: title, ContentSourceUrl: content_source_url,
44
+ Content: content, Digest: digest, ShowCoverPic: show_cover_pic }.reject { |_k, v| v.nil? }
45
+ end
46
+ end
47
+
48
+ attr_reader :message_hash
49
+
50
+ def initialize(message_hash)
51
+ @message_hash = message_hash || {}
52
+ end
53
+
54
+ def [](key)
55
+ message_hash[key]
56
+ end
57
+
58
+ def reply
59
+ Message.new(
60
+ ToUserName: message_hash[:FromUserName],
61
+ FromUserName: message_hash[:ToUserName],
62
+ CreateTime: Time.now.to_i,
63
+ WechatSession: session
64
+ )
65
+ end
66
+
67
+ def session
68
+ return nil unless Wechat.config.have_session_class
69
+ @message_hash[:WechatSession] ||= WechatSession.find_or_initialize_session(underscore_hash_keys(message_hash))
70
+ end
71
+
72
+ def save_session
73
+ ws = message_hash.delete(:WechatSession)
74
+ ws.try(:save_session, underscore_hash_keys(message_hash))
75
+ @message_hash[:WechatSession] = ws
76
+ end
77
+
78
+ def as(type)
79
+ case type
80
+ when :text
81
+ message_hash[:Content]
82
+
83
+ when :image, :voice, :video
84
+ Wechat.api.media(message_hash[:MediaId])
85
+
86
+ when :location
87
+ message_hash.slice(:Location_X, :Location_Y, :Scale, :Label).each_with_object({}) do |value, results|
88
+ results[value[0].to_s.underscore.to_sym] = value[1]
89
+ end
90
+ else
91
+ raise "Don't know how to parse message as #{type}"
92
+ end
93
+ end
94
+
95
+ def to(openid_or_userid)
96
+ update(ToUserName: openid_or_userid)
97
+ end
98
+
99
+ def agent_id(agentid)
100
+ update(AgentId: agentid)
101
+ end
102
+
103
+ def text(content)
104
+ update(MsgType: 'text', Content: content)
105
+ end
106
+
107
+ def transfer_customer_service(kf_account = nil)
108
+ if kf_account
109
+ update(MsgType: 'transfer_customer_service', TransInfo: { KfAccount: kf_account })
110
+ else
111
+ update(MsgType: 'transfer_customer_service')
112
+ end
113
+ end
114
+
115
+ def success
116
+ update(MsgType: 'success')
117
+ end
118
+
119
+ def image(media_id)
120
+ update(MsgType: 'image', Image: { MediaId: media_id })
121
+ end
122
+
123
+ def voice(media_id)
124
+ update(MsgType: 'voice', Voice: { MediaId: media_id })
125
+ end
126
+
127
+ def video(media_id, opts = {})
128
+ video_fields = camelize_hash_keys({ media_id: media_id }.merge(opts.slice(:title, :description)))
129
+ update(MsgType: 'video', Video: video_fields)
130
+ end
131
+
132
+ def file(media_id)
133
+ update(MsgType: 'file', File: { MediaId: media_id })
134
+ end
135
+
136
+ def music(thumb_media_id, music_url, opts = {})
137
+ music_fields = camelize_hash_keys(opts.slice(:title, :description, :HQ_music_url).merge(music_url: music_url, thumb_media_id: thumb_media_id))
138
+ update(MsgType: 'music', Music: music_fields)
139
+ end
140
+
141
+ def news(collection, &_block)
142
+ if block_given?
143
+ article = NewsArticleBuilder.new
144
+ collection.take(8).each_with_index { |item, index| yield(article, item, index) }
145
+ items = article.items
146
+ else
147
+ items = collection.collect do |item|
148
+ camelize_hash_keys(item.symbolize_keys.slice(:title, :description, :pic_url, :url).reject { |_k, v| v.nil? })
149
+ end
150
+ end
151
+
152
+ update(MsgType: 'news', ArticleCount: items.count,
153
+ Articles: items.collect { |item| camelize_hash_keys(item) })
154
+ end
155
+
156
+ def mpnews(collection, &_block)
157
+ if block_given?
158
+ article = MpNewsArticleBuilder.new
159
+ collection.take(8).each_with_index { |item, index| yield(article, item, index) }
160
+ items = article.items
161
+ else
162
+ items = collection.collect do |item|
163
+ camelize_hash_keys(item.symbolize_keys.slice(:thumb_media_id, :title, :content, :author, :content_source_url, :digest, :show_cover_pic).reject { |_k, v| v.nil? })
164
+ end
165
+ end
166
+
167
+ update(MsgType: 'mpnews', Articles: items.collect { |item| camelize_hash_keys(item) })
168
+ end
169
+
170
+ def ref_mpnews(media_id)
171
+ update(MsgType: 'ref_mpnews', MpNews: { MediaId: media_id })
172
+ end
173
+
174
+ TEMPLATE_KEYS = %i[template_id form_id page color
175
+ emphasis_keyword topcolor url miniprogram data].freeze
176
+
177
+ def template(opts = {})
178
+ template_fields = opts.symbolize_keys.slice(*TEMPLATE_KEYS)
179
+ update(MsgType: 'template', Template: template_fields)
180
+ end
181
+
182
+ def to_xml
183
+ ws = message_hash.delete(:WechatSession)
184
+ xml = message_hash.to_xml(root: 'xml', children: 'item', skip_instruct: true, skip_types: true)
185
+ @message_hash[:WechatSession] = ws
186
+ xml
187
+ end
188
+
189
+ TO_JSON_KEY_MAP = {
190
+ 'ToUserName' => 'touser',
191
+ 'ToWxName' => 'towxname',
192
+ 'MediaId' => 'media_id',
193
+ 'MpNews' => 'mpnews',
194
+ 'ThumbMediaId' => 'thumb_media_id',
195
+ 'TemplateId' => 'template_id',
196
+ 'FormId' => 'form_id',
197
+ 'ContentSourceUrl' => 'content_source_url',
198
+ 'ShowCoverPic' => 'show_cover_pic'
199
+ }.freeze
200
+
201
+ TO_JSON_ALLOWED = %w[touser msgtype content image voice video file
202
+ music news articles template agentid filter
203
+ send_ignore_reprint mpnews towxname].freeze
204
+
205
+ def to_json
206
+ keep_camel_case_key = message_hash[:MsgType] == 'template'
207
+ json_hash = deep_recursive(message_hash) do |key, value|
208
+ key = key.to_s
209
+ [(TO_JSON_KEY_MAP[key] || (keep_camel_case_key ? key : key.downcase)), value]
210
+ end
211
+ json_hash = json_hash.transform_keys(&:downcase).select { |k, _v| TO_JSON_ALLOWED.include? k }
212
+
213
+ case json_hash['msgtype']
214
+ when 'text'
215
+ json_hash['text'] = { 'content' => json_hash.delete('content') }
216
+ when 'news'
217
+ json_hash['news'] = { 'articles' => json_hash.delete('articles') }
218
+ when 'mpnews'
219
+ json_hash = { 'articles' => json_hash['articles'] }
220
+ when 'ref_mpnews'
221
+ json_hash['msgtype'] = 'mpnews'
222
+ json_hash.delete('articles')
223
+ when 'template'
224
+ json_hash = { 'touser' => json_hash['touser'] }.merge!(json_hash['template'])
225
+ end
226
+ JSON.generate(json_hash)
227
+ end
228
+
229
+ def save_to!(model_class)
230
+ model = model_class.new(underscore_hash_keys(message_hash))
231
+ model.save!
232
+ self
233
+ end
234
+
235
+ private
236
+
237
+ def camelize_hash_keys(hash)
238
+ deep_recursive(hash) { |key, value| [key.to_s.camelize.to_sym, value] }
239
+ end
240
+
241
+ def underscore_hash_keys(hash)
242
+ deep_recursive(hash) { |key, value| [key.to_s.underscore.to_sym, value] }
243
+ end
244
+
245
+ def update(fields = {})
246
+ message_hash.merge!(fields)
247
+ self
248
+ end
249
+
250
+ def deep_recursive(hash, &block)
251
+ hash.inject({}) do |memo, val|
252
+ key, value = *val
253
+ case value.class.name
254
+ when 'Hash'
255
+ value = deep_recursive(value, &block)
256
+ when 'Array'
257
+ value = value.collect { |item| item.is_a?(Hash) ? deep_recursive(item, &block) : item }
258
+ end
259
+
260
+ key, value = yield(key, value) unless key == :WechatSession
261
+ memo.merge!(key => value)
262
+ end
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,46 @@
1
+ require 'wechat/api_base'
2
+ require 'wechat/http_client'
3
+ require 'wechat/token/public_access_token'
4
+ require 'wechat/ticket/public_jsapi_ticket'
5
+ require 'wechat/concern/common'
6
+
7
+ module Wechat
8
+ class MpApi < ApiBase
9
+ include Concern::Common
10
+
11
+ def template_message_send(message)
12
+ post 'message/wxopen/template/send', message.to_json, content_type: :json
13
+ end
14
+
15
+ def list_template_library(offset: 0, count: 20)
16
+ post 'wxopen/template/library/list', JSON.generate(offset: offset, count: count)
17
+ end
18
+
19
+ def list_template_library_keywords(id)
20
+ post 'wxopen/template/library/get', JSON.generate(id: id)
21
+ end
22
+
23
+ def add_message_template(id, keyword_id_list)
24
+ post 'wxopen/template/add', JSON.generate(id: id, keyword_id_list: keyword_id_list)
25
+ end
26
+
27
+ def list_message_template(offset: 0, count: 20)
28
+ post 'wxopen/template/list', JSON.generate(offset: offset, count: count)
29
+ end
30
+
31
+ def del_message_template(template_id)
32
+ post 'wxopen/template/del', JSON.generate(template_id: template_id)
33
+ end
34
+
35
+ def jscode2session(code)
36
+ params = {
37
+ appid: access_token.appid,
38
+ secret: access_token.secret,
39
+ js_code: code,
40
+ grant_type: 'authorization_code'
41
+ }
42
+
43
+ client.get 'jscode2session', params: params, base: OAUTH2_BASE
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,308 @@
1
+ require 'English'
2
+ require 'wechat/signature'
3
+
4
+ module Wechat
5
+ module Responder
6
+ extend ActiveSupport::Concern
7
+ include Wechat::ControllerApi
8
+ include Cipher
9
+
10
+ included do
11
+ # Rails 5 remove before_filter and skip_before_filter
12
+ if respond_to?(:skip_before_action)
13
+ if respond_to?(:verify_authenticity_token)
14
+ skip_before_action :verify_authenticity_token
15
+ else
16
+ # Rails 5 API mode won't define verify_authenticity_token
17
+ # https://github.com/rails/rails/blob/v5.0.0.beta3/actionpack/lib/abstract_controller/callbacks.rb#L66
18
+ # https://github.com/rails/rails/blob/v5.0.0.beta3/activesupport/lib/active_support/callbacks.rb#L640
19
+ skip_before_action :verify_authenticity_token, raise: false
20
+ end
21
+
22
+ before_action :config_account, only: [:show, :create]
23
+ before_action :verify_signature, only: [:show, :create]
24
+ else
25
+ skip_before_filter :verify_authenticity_token
26
+ before_filter :config_account, only: [:show, :create]
27
+ before_filter :verify_signature, only: [:show, :create]
28
+ end
29
+ end
30
+
31
+ module ClassMethods
32
+ attr_accessor :account_from_request
33
+
34
+ def on(message_type, with: nil, respond: nil, &block)
35
+ raise 'Unknow message type' unless [:text, :image, :voice, :video, :shortvideo, :link, :event, :click, :view, :scan, :batch_job, :location, :label_location, :fallback].include?(message_type)
36
+ config = respond.nil? ? {} : { respond: respond }
37
+ config[:proc] = block if block_given?
38
+
39
+ if with.present?
40
+ raise 'Only text, event, click, view, scan and batch_job can having :with parameters' unless [:text, :event, :click, :view, :scan, :batch_job].include?(message_type)
41
+ config[:with] = with
42
+ if message_type == :scan
43
+ if with.is_a?(String)
44
+ self.known_scan_key_lists = with
45
+ else
46
+ raise 'on :scan only support string in parameter with, detail see https://github.com/Eric-Guo/wechat/issues/84'
47
+ end
48
+ end
49
+ else
50
+ raise 'Message type click, view, scan and batch_job must specify :with parameters' if [:click, :view, :scan, :batch_job].include?(message_type)
51
+ end
52
+
53
+ case message_type
54
+ when :click
55
+ user_defined_click_responders(with) << config
56
+ when :view
57
+ user_defined_view_responders(with) << config
58
+ when :batch_job
59
+ user_defined_batch_job_responders(with) << config
60
+ when :scan
61
+ user_defined_scan_responders << config
62
+ when :location
63
+ user_defined_location_responders << config
64
+ when :label_location
65
+ user_defined_label_location_responders << config
66
+ else
67
+ user_defined_responders(message_type) << config
68
+ end
69
+
70
+ config
71
+ end
72
+
73
+ def user_defined_click_responders(with)
74
+ @click_responders ||= {}
75
+ @click_responders[with] ||= []
76
+ end
77
+
78
+ def user_defined_view_responders(with)
79
+ @view_responders ||= {}
80
+ @view_responders[with] ||= []
81
+ end
82
+
83
+ def user_defined_batch_job_responders(with)
84
+ @batch_job_responders ||= {}
85
+ @batch_job_responders[with] ||= []
86
+ end
87
+
88
+ def user_defined_scan_responders
89
+ @scan_responders ||= []
90
+ end
91
+
92
+ def user_defined_location_responders
93
+ @location_responders ||= []
94
+ end
95
+
96
+ def user_defined_label_location_responders
97
+ @label_location_responders ||= []
98
+ end
99
+
100
+ def user_defined_responders(type)
101
+ @responders ||= {}
102
+ @responders[type] ||= []
103
+ end
104
+
105
+ def responder_for(message)
106
+ message_type = message[:MsgType].to_sym
107
+ responders = user_defined_responders(message_type)
108
+
109
+ case message_type
110
+ when :text
111
+ yield(* match_responders(responders, message[:Content]))
112
+ when :event
113
+ if 'click' == message[:Event] && !user_defined_click_responders(message[:EventKey]).empty?
114
+ yield(* user_defined_click_responders(message[:EventKey]), message[:EventKey])
115
+ elsif 'view' == message[:Event] && !user_defined_view_responders(message[:EventKey]).empty?
116
+ yield(* user_defined_view_responders(message[:EventKey]), message[:EventKey])
117
+ elsif 'click' == message[:Event]
118
+ yield(* match_responders(responders, message[:EventKey]))
119
+ elsif known_scan_key_lists.include?(message[:EventKey]) && %w(scan subscribe scancode_push scancode_waitmsg).freeze.include?(message[:Event])
120
+ yield(* known_scan_with_match_responders(user_defined_scan_responders, message))
121
+ elsif 'batch_job_result' == message[:Event]
122
+ yield(* user_defined_batch_job_responders(message[:BatchJob][:JobType]), message[:BatchJob])
123
+ elsif 'location' == message[:Event]
124
+ yield(* user_defined_location_responders, message)
125
+ else
126
+ yield(* match_responders(responders, message[:Event]))
127
+ end
128
+ when :location
129
+ yield(* user_defined_label_location_responders, message)
130
+ else
131
+ yield(responders.first)
132
+ end
133
+ end
134
+
135
+ private
136
+
137
+ def match_responders(responders, value)
138
+ matched = responders.each_with_object({}) do |responder, memo|
139
+ condition = responder[:with]
140
+
141
+ if condition.nil?
142
+ memo[:general] ||= [responder, value]
143
+ next
144
+ end
145
+
146
+ if condition.is_a? Regexp
147
+ memo[:scoped] ||= [responder] + $LAST_MATCH_INFO.captures if value =~ condition
148
+ else
149
+ memo[:scoped] ||= [responder, value] if value == condition
150
+ end
151
+ end
152
+ matched[:scoped] || matched[:general]
153
+ end
154
+
155
+ def known_scan_with_match_responders(responders, message)
156
+ matched = responders.each_with_object({}) do |responder, memo|
157
+ if %w(scan subscribe).freeze.include?(message[:Event]) && message[:EventKey] == responder[:with]
158
+ memo[:scaned] ||= [responder, message[:Ticket]]
159
+ elsif %w(scancode_push scancode_waitmsg).freeze.include?(message[:Event]) && message[:EventKey] == responder[:with]
160
+ memo[:scaned] ||= [responder, message[:ScanCodeInfo][:ScanResult], message[:ScanCodeInfo][:ScanType]]
161
+ end
162
+ end
163
+ matched[:scaned]
164
+ end
165
+
166
+ def known_scan_key_lists
167
+ @known_scan_key_lists ||= []
168
+ end
169
+
170
+ def known_scan_key_lists=(qrscene_value)
171
+ @known_scan_key_lists ||= []
172
+ @known_scan_key_lists << qrscene_value
173
+ end
174
+ end
175
+
176
+ def show
177
+ if @we_corpid.present?
178
+ echostr, _corp_id = unpack(decrypt(Base64.decode64(params[:echostr]), @we_encoding_aes_key))
179
+ if Rails::VERSION::MAJOR >= 4
180
+ render plain: echostr
181
+ else
182
+ render text: echostr
183
+ end
184
+ else
185
+ if Rails::VERSION::MAJOR >= 4
186
+ render plain: params[:echostr]
187
+ else
188
+ render text: params[:echostr]
189
+ end
190
+ end
191
+ end
192
+
193
+ def create
194
+ request_msg = Wechat::Message.from_hash(post_xml)
195
+ response_msg = run_responder(request_msg)
196
+
197
+ if response_msg.respond_to? :to_xml
198
+ if Rails::VERSION::MAJOR >= 4
199
+ render plain: process_response(response_msg)
200
+ else
201
+ render text: process_response(response_msg)
202
+ end
203
+ else
204
+ head :ok, content_type: 'text/html'
205
+ end
206
+
207
+ response_msg.save_session if response_msg.is_a?(Wechat::Message) && Wechat.config.have_session_class
208
+
209
+ ActiveSupport::Notifications.instrument 'wechat.responder.after_create', request: request_msg, response: response_msg
210
+ end
211
+
212
+ private
213
+
214
+ def config_account
215
+ account = self.class.account_from_request&.call(request)
216
+ config = account ? Wechat.config(account) : nil
217
+
218
+ @we_encrypt_mode = config&.encrypt_mode || self.class.encrypt_mode
219
+ @we_encoding_aes_key = config&.encoding_aes_key || self.class.encoding_aes_key
220
+ @we_token = config&.token || self.class.token
221
+ @we_corpid = config&.corpid || self.class.corpid
222
+ end
223
+
224
+ def verify_signature
225
+ if @we_encrypt_mode
226
+ signature = params[:signature] || params[:msg_signature]
227
+ msg_encrypt = params[:echostr] || request_encrypt_content
228
+ else
229
+ signature = params[:signature]
230
+ end
231
+
232
+ msg_encrypt = nil unless @we_corpid.present?
233
+
234
+ render plain: 'Forbidden', status: 403 if signature != Signature.hexdigest(@we_token,
235
+ params[:timestamp],
236
+ params[:nonce],
237
+ msg_encrypt)
238
+ end
239
+
240
+ def post_xml
241
+ data = request_content
242
+
243
+ if @we_encrypt_mode && request_encrypt_content.present?
244
+ content, @we_app_id = unpack(decrypt(Base64.decode64(request_encrypt_content), @we_encoding_aes_key))
245
+ data = Hash.from_xml(content)
246
+ end
247
+
248
+ data_hash = data.fetch('xml', {}).merge(openid: data[:openid])
249
+ if Rails::VERSION::MAJOR >= 5
250
+ data_hash = data_hash.to_unsafe_hash if data_hash.instance_of?(ActionController::Parameters)
251
+ HashWithIndifferentAccess.new(data_hash).tap do |msg|
252
+ msg[:Event].downcase! if msg[:Event]
253
+ end
254
+ else
255
+ HashWithIndifferentAccess.new_from_hash_copying_default(data_hash).tap do |msg|
256
+ msg[:Event].downcase! if msg[:Event]
257
+ end
258
+ end
259
+ end
260
+
261
+ def run_responder(request)
262
+ self.class.responder_for(request) do |responder, *args|
263
+ responder ||= self.class.user_defined_responders(:fallback).first
264
+
265
+ next if responder.nil?
266
+ case
267
+ when responder[:respond]
268
+ request.reply.text responder[:respond]
269
+ when responder[:proc]
270
+ define_singleton_method :process, responder[:proc]
271
+ number_of_block_parameter = responder[:proc].arity
272
+ send(:process, *args.unshift(request).take(number_of_block_parameter))
273
+ else
274
+ next
275
+ end
276
+ end
277
+ end
278
+
279
+ def process_response(response)
280
+ msg = response[:MsgType] == 'success' ? 'success' : response.to_xml
281
+
282
+ if @we_encrypt_mode
283
+ encrypt = Base64.strict_encode64(encrypt(pack(msg, @we_app_id), @we_encoding_aes_key))
284
+ msg = gen_msg(encrypt, params[:timestamp], params[:nonce])
285
+ end
286
+
287
+ msg
288
+ end
289
+
290
+ def gen_msg(encrypt, timestamp, nonce)
291
+ msg_sign = Signature.hexdigest(@we_token, timestamp, nonce, encrypt)
292
+
293
+ { Encrypt: encrypt,
294
+ MsgSignature: msg_sign,
295
+ TimeStamp: timestamp,
296
+ Nonce: nonce
297
+ }.to_xml(root: 'xml', children: 'item', skip_instruct: true, skip_types: true)
298
+ end
299
+
300
+ def request_encrypt_content
301
+ request_content&.dig('xml', 'Encrypt')
302
+ end
303
+
304
+ def request_content
305
+ params[:xml].nil? ? Hash.from_xml(request.raw_post) : { 'xml' => params[:xml] }.merge(openid: params[:openid])
306
+ end
307
+ end
308
+ end
@@ -0,0 +1,10 @@
1
+ module Wechat
2
+ module Signature
3
+ def self.hexdigest(token, timestamp, nonce, msg_encrypt)
4
+ array = [token, timestamp, nonce]
5
+ array << msg_encrypt unless msg_encrypt.nil?
6
+ dev_msg_signature = array.compact.collect(&:to_s).sort.join
7
+ Digest::SHA1.hexdigest(dev_msg_signature)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,14 @@
1
+ require 'wechat/ticket/jsapi_base'
2
+
3
+ module Wechat
4
+ module Ticket
5
+ class CorpJsapiTicket < JsapiBase
6
+ def refresh
7
+ data = client.get('get_jsapi_ticket', params: { access_token: access_token.token })
8
+ data['oauth2_state'] = SecureRandom.hex(16)
9
+ write_ticket_to_store(data)
10
+ read_ticket_from_store
11
+ end
12
+ end
13
+ end
14
+ end