gfd_wechat 0.0.1

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.
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