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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +321 -0
- data/LICENSE +21 -0
- data/README-CN.md +815 -0
- data/README.md +844 -0
- data/bin/wechat +520 -0
- data/lib/action_controller/wechat_responder.rb +72 -0
- data/lib/generators/wechat/config_generator.rb +36 -0
- data/lib/generators/wechat/install_generator.rb +20 -0
- data/lib/generators/wechat/menu_generator.rb +21 -0
- data/lib/generators/wechat/redis_store_generator.rb +16 -0
- data/lib/generators/wechat/session_generator.rb +36 -0
- data/lib/generators/wechat/templates/MENU_README +3 -0
- data/lib/generators/wechat/templates/app/controllers/wechats_controller.rb +12 -0
- data/lib/generators/wechat/templates/app/models/wechat_config.rb +46 -0
- data/lib/generators/wechat/templates/app/models/wechat_session.rb +17 -0
- data/lib/generators/wechat/templates/config/initializers/wechat_redis_store.rb +42 -0
- data/lib/generators/wechat/templates/config/wechat.yml +72 -0
- data/lib/generators/wechat/templates/config/wechat_menu.yml +6 -0
- data/lib/generators/wechat/templates/config/wechat_menu_android.yml +15 -0
- data/lib/generators/wechat/templates/db/config_migration.rb.erb +40 -0
- data/lib/generators/wechat/templates/db/session_migration.rb.erb +10 -0
- data/lib/wechat/api.rb +54 -0
- data/lib/wechat/api_base.rb +63 -0
- data/lib/wechat/api_loader.rb +145 -0
- data/lib/wechat/cipher.rb +66 -0
- data/lib/wechat/concern/common.rb +217 -0
- data/lib/wechat/controller_api.rb +96 -0
- data/lib/wechat/corp_api.rb +168 -0
- data/lib/wechat/helpers.rb +47 -0
- data/lib/wechat/http_client.rb +112 -0
- data/lib/wechat/message.rb +265 -0
- data/lib/wechat/mp_api.rb +46 -0
- data/lib/wechat/responder.rb +308 -0
- data/lib/wechat/signature.rb +10 -0
- data/lib/wechat/ticket/corp_jsapi_ticket.rb +14 -0
- data/lib/wechat/ticket/jsapi_base.rb +84 -0
- data/lib/wechat/ticket/public_jsapi_ticket.rb +14 -0
- data/lib/wechat/token/access_token_base.rb +53 -0
- data/lib/wechat/token/corp_access_token.rb +13 -0
- data/lib/wechat/token/public_access_token.rb +13 -0
- data/lib/wechat.rb +52 -0
- 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
|