wechat 0.9.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1ba8ea75d218bce256ab3ac81dd380747abc5515713f69cfc7353db5c02a3ebb
4
- data.tar.gz: 40c60efec124604c7141983de97b121735c45be33213f4395ad7df2c5f967d52
3
+ metadata.gz: de72dcc5a201ab7841963fb88eee52e109487969e7a2ab05324fae9977fd9dab
4
+ data.tar.gz: 3f1f797d0224c24d2625c8dfdad2cee66c85cfe98cd2511dace850493d2c11e4
5
5
  SHA512:
6
- metadata.gz: ce2ff6717092db5798ab87791acc1ed75465c49a624ad7f7cb946748a3b144d98ff22758d29ed6699744379555bc95a0929358eeb3e19ae668c87b26dd0014b4
7
- data.tar.gz: 104a924a1f55c75662b32f41600b81dde58bb33a8bc9c0d377517bb092aff8c72d5c51aa772b22e69c3308bc8523fd424f45a894d9686436358f8afa87cb17e1
6
+ metadata.gz: 5a5bf87045b07c01b3d140f981fa3e1dd29a2490e32d718ced9ed82fa55488511514beea467fde8726ed24805d63b874776e6f0f832130f25fa82d592ea5c8f3
7
+ data.tar.gz: f045e5886f34b243fe9fc86dcbe663efdd6292e076a515578436a4fcaeae778304554a59cbdfd01fc5ed5b6538d3ca23127acfe410764bb1c0b30c0a08a65368
checksums.yaml.gz.sig CHANGED
Binary file
data.tar.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.10.0 (released at 5/31/2018)
4
+
5
+ * Support multi wechat account at wechat_responder. by @tuliren #223
6
+ * Support wechat mini program apis & signature check. by @oiahoon #225
7
+ * Support sent template message with miniprogram. by @falm #228
8
+ * Fix request_content could be nil. by @paicha #229
9
+
3
10
  ## v0.9.0 (released at 4/15/2018)
4
11
 
5
12
  * Support multi wechat account dynamically loading from DB. by @tuliren #222
data/README-CN.md CHANGED
@@ -1,4 +1,4 @@
1
- WeChat [![Gem Version](https://badge.fury.io/rb/wechat.svg)](https://rubygems.org/gems/wechat) [![Build Status](https://travis-ci.org/Eric-Guo/wechat.svg)](https://travis-ci.org/Eric-Guo/wechat) [![Code Climate](https://codeclimate.com/github/Eric-Guo/wechat.png)](https://codeclimate.com/github/Eric-Guo/wechat) [![Code Coverage](https://codeclimate.com/github/Eric-Guo/wechat/coverage.png)](https://codeclimate.com/github/Eric-Guo/wechat/coverage)
1
+ WeChat [![Gem Version](https://badge.fury.io/rb/wechat.svg)](https://rubygems.org/gems/wechat) [![Build Status](https://travis-ci.org/Eric-Guo/wechat.svg)](https://travis-ci.org/Eric-Guo/wechat) [![Maintainability](https://api.codeclimate.com/v1/badges/12885358487c13e91e00/maintainability)](https://codeclimate.com/github/Eric-Guo/wechat/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/12885358487c13e91e00/test_coverage)](https://codeclimate.com/github/Eric-Guo/wechat/test_coverage)
2
2
  ======
3
3
 
4
4
  [![Join the chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Eric-Guo/wechat?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
@@ -238,7 +238,7 @@ Wechat服务器有报道曾出现[RestClient::SSLCertificateNotVerified](http://
238
238
 
239
239
  ```ruby
240
240
  class WechatFirstController < ActionController::Base
241
- wechat_responder account: :new_account
241
+ wechat_responder account: :new_account, account_from_request: Proc.new{ |request| request.params[:wechat] }
242
242
 
243
243
  on :text, with:"help", respond: "help content"
244
244
  end
@@ -248,40 +248,14 @@ end
248
248
 
249
249
  ```ruby
250
250
  class WechatFirstController < ActionController::Base
251
- wechat_responder appid: "app1", secret: "secret1", token: "token1", access_token: Rails.root.join("tmp/access_token1")
251
+ wechat_responder appid: "app1", secret: "secret1", token: "token1", access_token: Rails.root.join("tmp/access_token1"),
252
+ account_from_request: Proc.new{ |request| request.params[:wechat] }
252
253
 
253
254
  on :text, with:"help", respond: "help content"
254
255
  end
255
256
  ```
256
257
 
257
- #### 为每个 request 配置不同的 `appid`
258
-
259
- 若需要动态处理不同微信公众号的消息,您需要用数据库存储账户设置,然后调用 `wechat_oauth2` 或者 `Wechat#api`:
260
-
261
- ```ruby
262
- class WechatReportsController < ApplicationController
263
- wechat_api
264
- layout 'wechat'
265
-
266
- def index
267
- # 通过自定义方法,从 request 中得到微信账户名称
268
- account_name = get_account_from_url(request)
269
-
270
- wechat_oauth2('snsapi_base', nil, account_name) do |openid|
271
- @current_user = User.find_by(wechat_openid: openid)
272
- @articles = @current_user.articles
273
- end
274
-
275
- Wechat.api(account_name)
276
- end
277
-
278
- private
279
- # 预期的 URL: .../wechat/<account-name>/...
280
- def get_account_from_url(request)
281
- request.original_url.match(/wechat\/(.*)\//)[1]
282
- end
283
- end
284
- ```
258
+ 其中 `account_from_request` 是一个 `Proc`,接受 `request` 作为唯一参数,返回相应的微信账户名称。以上示例中,`controller` 会根据 `request` 中传入的 `wechat` 参数选择微信账户。如果没有提供 `account_from_request` 或者 `Proc` 的结果是 `nil`,则使用 `account` 或者完整配置。
285
259
 
286
260
  #### JS-SDK 支持
287
261
 
@@ -315,6 +289,11 @@ class CartController < ActionController::Base
315
289
  @current_user = User.find_by(wechat_openid: openid)
316
290
  @articles = @current_user.articles
317
291
  end
292
+
293
+ # 指定 account_name,可以使用任意微信账户
294
+ # wechat_oauth2('snsapi_base', nil, account_name) do |openid|
295
+ # ...
296
+ # end
318
297
  end
319
298
  end
320
299
  ```
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- WeChat [![Gem Version](https://badge.fury.io/rb/wechat.svg)](https://rubygems.org/gems/wechat) [![Build Status](https://travis-ci.org/Eric-Guo/wechat.svg)](https://travis-ci.org/Eric-Guo/wechat) [![Code Climate](https://codeclimate.com/github/Eric-Guo/wechat.png)](https://codeclimate.com/github/Eric-Guo/wechat) [![Code Coverage](https://codeclimate.com/github/Eric-Guo/wechat/coverage.png)](https://codeclimate.com/github/Eric-Guo/wechat/coverage)
1
+ WeChat [![Gem Version](https://badge.fury.io/rb/wechat.svg)](https://rubygems.org/gems/wechat) [![Build Status](https://travis-ci.org/Eric-Guo/wechat.svg)](https://travis-ci.org/Eric-Guo/wechat) [![Maintainability](https://api.codeclimate.com/v1/badges/12885358487c13e91e00/maintainability)](https://codeclimate.com/github/Eric-Guo/wechat/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/12885358487c13e91e00/test_coverage)](https://codeclimate.com/github/Eric-Guo/wechat/test_coverage)
2
2
  ======
3
3
 
4
4
  [![Join the chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Eric-Guo/wechat?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
@@ -207,6 +207,19 @@ test:
207
207
 
208
208
  For multiple accounts details reference [PR 150](https://github.com/Eric-Guo/wechat/pull/150)
209
209
 
210
+ For wechat mini program, can specified by the item `type`:
211
+
212
+ ```yaml
213
+ # Mini Program Accounts
214
+
215
+ mini_development:
216
+ <<: *default
217
+ appid: "my_appid"
218
+ secret: "my_secret"
219
+ # `mp` is short for **mini program**
220
+ type: 'mp'
221
+ ```
222
+
210
223
  #### Database wechat account configuration
211
224
  After enabling database account configuration, the following table will be created:
212
225
 
@@ -251,7 +264,7 @@ Sometimes, you may want to host more than one enterprise/public wechat account i
251
264
 
252
265
  ```ruby
253
266
  class WechatFirstController < ActionController::Base
254
- wechat_responder account: :new_account
267
+ wechat_responder account: :new_account, account_from_request: Proc.new{ |request| request.params[:wechat] }
255
268
 
256
269
  on :text, with:"help", respond: "help content"
257
270
  end
@@ -261,39 +274,14 @@ Or you can provide full list of options.
261
274
 
262
275
  ```ruby
263
276
  class WechatFirstController < ActionController::Base
264
- wechat_responder appid: "app1", secret: "secret1", token: "token1", access_token: Rails.root.join("tmp/access_token1")
277
+ wechat_responder appid: "app1", secret: "secret1", token: "token1", access_token: Rails.root.join("tmp/access_token1"),
278
+ account_from_request: Proc.new{ |request| request.params[:wechat] }
265
279
 
266
280
  on :text, with:"help", respond: "help content"
267
281
  end
268
282
  ```
269
283
 
270
- #### Configure individual request with different `appid`
271
-
272
- If you want the controller to dynamically apply different account configurations for each request, you need to enable database account configuration, and call `wechat_oauth2` or `Wechat#api`:
273
-
274
- ```ruby
275
- class WechatReportsController < ApplicationController
276
- wechat_api
277
- layout 'wechat'
278
-
279
- def index
280
- account_name = get_account_from_url(request)
281
-
282
- wechat_oauth2('snsapi_base', nil, account_name) do |openid|
283
- @current_user = User.find_by(wechat_openid: openid)
284
- @articles = @current_user.articles
285
- end
286
-
287
- Wechat.api(account_name)
288
- end
289
-
290
- private
291
- # Expected URL: .../wechat/<account-name>/...
292
- def get_account_from_url(request)
293
- request.original_url.match(/wechat\/(.*)\//)[1]
294
- end
295
- end
296
- ```
284
+ `account_from_request` is a `Proc` that takes in `request` as its parameter, and returns the corresponding wechat account name. In the above examples, `controller` will choose the account based on the `wechat` parameter passed in the `request`. If `account_from_request` is not specified, or this `Proc` evaluates to `nil`, configuration specified by `account` or the full list of options will be used.
297
285
 
298
286
  #### JS-SDK helper
299
287
 
@@ -327,6 +315,11 @@ class CartController < ActionController::Base
327
315
  @current_user = User.find_by(wechat_openid: openid)
328
316
  @articles = @current_user.articles
329
317
  end
318
+
319
+ # specify account_name to use arbitrary wechat account configuration
320
+ # wechat_oauth2('snsapi_base', nil, account_name) do |openid|
321
+ # ...
322
+ # end
330
323
  end
331
324
  end
332
325
  ```
@@ -643,6 +636,9 @@ end
643
636
 
644
637
  Using `Wechat.api` to access the wechat api function at any place.
645
638
 
639
+ ## Checking the signature
640
+ Using `Wechat.decrypt(encrypted_data,session_key, iv)` to decode the data. via. [Signature Checking](https://developers.weixin.qq.com/miniprogram/dev/api/signature.html)
641
+
646
642
  ## wechat_responder - Rails Responder Controller DSL
647
643
 
648
644
  In order to respond to the message user sent, Rails developer needs to create a wechat responder controller and define the routing in `routes.rb`
@@ -10,6 +10,7 @@ module ActionController
10
10
  def wechat_responder(opts = {})
11
11
  include Wechat::Responder
12
12
  account = opts.delete(:account)
13
+ self.account_from_request = opts.delete(:account_from_request)
13
14
  self.wechat_cfg_account = account ? account.to_sym : :default
14
15
  self.wechat_api_client = load_controller_wechat(wechat_cfg_account, opts)
15
16
  end
data/lib/wechat.rb CHANGED
@@ -1,5 +1,8 @@
1
+ require 'base64'
2
+ require 'openssl/cipher'
1
3
  require 'wechat/api_loader'
2
4
  require 'wechat/api'
5
+ require 'wechat/mp_api'
3
6
  require 'wechat/corp_api'
4
7
  require 'wechat/helpers'
5
8
  require 'action_controller/wechat_responder'
@@ -32,6 +35,18 @@ module Wechat
32
35
  def self.reload_config!
33
36
  ApiLoader.reload_config!
34
37
  end
38
+
39
+ def self.decrypt(encrypted_data, session_key, iv)
40
+ cipher = OpenSSL::Cipher.new('AES-128-CBC')
41
+ cipher.decrypt
42
+
43
+ cipher.key = Base64.decode64(session_key)
44
+ cipher.iv = Base64.decode64(iv)
45
+ decrypted_data = Base64.decode64(encrypted_data)
46
+ JSON.parse(cipher.update(decrypted_data) + cipher.final)
47
+ rescue Exception => e
48
+ { 'errcode': 41003, 'errmsg': e.message }
49
+ end
35
50
  end
36
51
 
37
52
  ActionView::Base.send :include, Wechat::Helpers if defined? ActionView::Base
data/lib/wechat/api.rb CHANGED
@@ -2,225 +2,26 @@ require 'wechat/api_base'
2
2
  require 'wechat/http_client'
3
3
  require 'wechat/token/public_access_token'
4
4
  require 'wechat/ticket/public_jsapi_ticket'
5
+ require 'wechat/concern/common'
5
6
 
6
7
  module Wechat
7
8
  class Api < ApiBase
8
- API_BASE = 'https://api.weixin.qq.com/cgi-bin/'.freeze
9
- WXA_BASE = 'https://api.weixin.qq.com/wxa/'.freeze
10
-
11
- def initialize(appid, secret, token_file, timeout, skip_verify_ssl, jsapi_ticket_file)
12
- @client = HttpClient.new(API_BASE, timeout, skip_verify_ssl)
13
- @access_token = Token::PublicAccessToken.new(@client, appid, secret, token_file)
14
- @jsapi_ticket = Ticket::PublicJsapiTicket.new(@client, @access_token, jsapi_ticket_file)
15
- end
16
-
17
- def groups
18
- get 'groups/get'
19
- end
20
-
21
- def group_create(group_name)
22
- post 'groups/create', JSON.generate(group: { name: group_name })
23
- end
24
-
25
- def group_update(groupid, new_group_name)
26
- post 'groups/update', JSON.generate(group: { id: groupid, name: new_group_name })
27
- end
28
-
29
- def group_delete(groupid)
30
- post 'groups/delete', JSON.generate(group: { id: groupid })
31
- end
32
-
33
- def users(nextid = nil)
34
- params = { params: { next_openid: nextid } } if nextid.present?
35
- get('user/get', params || {})
36
- end
37
-
38
- def user(openid)
39
- get 'user/info', params: { openid: openid }
40
- end
41
-
42
- def user_batchget(openids, lang = 'zh-CN')
43
- post 'user/info/batchget', JSON.generate(user_list: openids.collect { |v| { openid: v, lang: lang } })
44
- end
45
-
46
- def user_group(openid)
47
- post 'groups/getid', JSON.generate(openid: openid)
48
- end
49
-
50
- def user_change_group(openid, to_groupid)
51
- post 'groups/members/update', JSON.generate(openid: openid, to_groupid: to_groupid)
52
- end
53
-
54
- def user_update_remark(openid, remark)
55
- post 'user/info/updateremark', JSON.generate(openid: openid, remark: remark)
56
- end
57
-
58
- def qrcode_create_scene(scene_id_or_str, expire_seconds = 604800)
59
- case scene_id_or_str
60
- when 0.class
61
- post 'qrcode/create', JSON.generate(expire_seconds: expire_seconds,
62
- action_name: 'QR_SCENE',
63
- action_info: { scene: { scene_id: scene_id_or_str } })
64
- else
65
- post 'qrcode/create', JSON.generate(expire_seconds: expire_seconds,
66
- action_name: 'QR_STR_SCENE',
67
- action_info: { scene: { scene_str: scene_id_or_str } })
68
- end
69
-
70
- end
71
-
72
- def qrcode_create_limit_scene(scene_id_or_str)
73
- case scene_id_or_str
74
- when 0.class
75
- post 'qrcode/create', JSON.generate(action_name: 'QR_LIMIT_SCENE',
76
- action_info: { scene: { scene_id: scene_id_or_str } })
77
- else
78
- post 'qrcode/create', JSON.generate(action_name: 'QR_LIMIT_STR_SCENE',
79
- action_info: { scene: { scene_str: scene_id_or_str } })
80
- end
81
- end
82
-
83
- def shorturl(long_url)
84
- post 'shorturl', JSON.generate(action: 'long2short', long_url: long_url)
85
- end
86
-
87
- def message_mass_sendall(message)
88
- post 'message/mass/sendall', message.to_json
89
- end
90
-
91
- def message_mass_delete(msg_id)
92
- post 'message/mass/delete', JSON.generate(msg_id: msg_id)
93
- end
94
-
95
- def message_mass_preview(message)
96
- post 'message/mass/preview', message.to_json
97
- end
98
-
99
- def message_mass_get(msg_id)
100
- post 'message/mass/get', JSON.generate(msg_id: msg_id)
101
- end
102
-
103
- def wxa_get_wxacode(path, width = 430)
104
- post 'getwxacode', JSON.generate(path: path, width: width), base: WXA_BASE
105
- end
106
-
107
- def wxa_create_qrcode(path, width = 430)
108
- post 'wxaapp/createwxaqrcode', JSON.generate(path: path, width: width)
109
- end
110
-
111
- def menu
112
- get 'menu/get'
113
- end
114
-
115
- def menu_delete
116
- get 'menu/delete'
117
- end
118
-
119
- def menu_create(menu)
120
- # 微信不接受7bit escaped json(eg \uxxxx), 中文必须UTF-8编码, 这可能是个安全漏洞
121
- post 'menu/create', JSON.generate(menu)
122
- end
123
-
124
- def menu_addconditional(menu)
125
- # Wechat not accept 7bit escaped json(eg \uxxxx), must using UTF-8, possible security vulnerability?
126
- post 'menu/addconditional', JSON.generate(menu)
127
- end
128
-
129
- def menu_trymatch(user_id)
130
- post 'menu/trymatch', JSON.generate(user_id: user_id)
131
- end
132
-
133
- def menu_delconditional(menuid)
134
- post 'menu/delconditional', JSON.generate(menuid: menuid)
135
- end
136
-
137
- def material(media_id)
138
- get 'material/get', params: { media_id: media_id }, as: :file
139
- end
140
-
141
- def material_count
142
- get 'material/get_materialcount'
143
- end
144
-
145
- def material_list(type, offset, count)
146
- post 'material/batchget_material', JSON.generate(type: type, offset: offset, count: count)
147
- end
148
-
149
- def material_add(type, file)
150
- post_file 'material/add_material', file, params: { type: type }
151
- end
152
-
153
- def material_delete(media_id)
154
- post 'material/del_material', JSON.generate(media_id: media_id)
155
- end
156
-
157
- def custom_message_send(message)
158
- post 'message/custom/send', message.to_json, content_type: :json
159
- end
9
+ include Concern::Common
160
10
 
161
11
  def template_message_send(message)
162
12
  post 'message/template/send', message.to_json, content_type: :json
163
13
  end
164
14
 
165
- def customservice_getonlinekflist
166
- get 'customservice/getonlinekflist'
167
- end
168
-
169
- def tags
170
- get 'tags/get'
171
- end
172
-
173
- def tag_create(tag_name)
174
- post 'tags/create', JSON.generate(tag: { name: tag_name })
175
- end
176
-
177
- def tag_update(tagid, new_tag_name)
178
- post 'tags/update', JSON.generate(tag: { id: tagid, name: new_tag_name })
179
- end
180
-
181
- def tag_delete(tagid)
182
- post 'tags/delete', JSON.generate(tag: { id: tagid })
183
- end
184
-
185
- def tag_add_user(tagid, openids)
186
- post 'tags/members/batchtagging', JSON.generate(openid_list: openids, tagid: tagid)
187
- end
188
-
189
- def tag_del_user(tagid, openids)
190
- post 'tags/members/batchuntagging', JSON.generate(openid_list: openids, tagid: tagid)
191
- end
192
-
193
- def tag(tagid, next_openid = '')
194
- post 'user/tag/get', JSON.generate(tagid: tagid, next_openid: next_openid)
195
- end
196
-
197
- OAUTH2_BASE = 'https://api.weixin.qq.com/sns/'.freeze
198
-
199
- def web_access_token(code)
200
- params = {
201
- appid: access_token.appid,
202
- secret: access_token.secret,
203
- code: code,
204
- grant_type: 'authorization_code'
205
- }
206
- client.get 'oauth2/access_token', params: params, base: OAUTH2_BASE
207
- end
208
-
209
- def web_auth_access_token(web_access_token, openid)
210
- client.get 'auth', params: { access_token: web_access_token, openid: openid }, base: OAUTH2_BASE
15
+ def list_message_template
16
+ get 'template/get_all_private_template'
211
17
  end
212
18
 
213
- def web_refresh_access_token(user_refresh_token)
214
- params = {
215
- appid: access_token.appid,
216
- grant_type: 'refresh_token',
217
- refresh_token: user_refresh_token
218
- }
219
- client.get 'oauth2/refresh_token', params: params, base: OAUTH2_BASE
19
+ def add_message_template(template_id_short)
20
+ post 'template/api_add_template', JSON.generate(template_id_short: template_id_short)
220
21
  end
221
22
 
222
- def web_userinfo(web_access_token, openid, lang = 'zh_CN')
223
- client.get 'userinfo', params: { access_token: web_access_token, openid: openid, lang: lang }, base: OAUTH2_BASE
23
+ def del_message_template(template_id)
24
+ post 'template/del_private_template', JSON.generate(template_id: template_id)
224
25
  end
225
26
  end
226
27
  end
@@ -6,9 +6,10 @@ module Wechat
6
6
 
7
7
  token_file = options[:token_file] || c.access_token.presence || '/var/tmp/wechat_access_token'
8
8
  js_token_file = options[:js_token_file] || c.jsapi_ticket.presence || '/var/tmp/wechat_jsapi_ticket'
9
-
9
+ type = options[:type] || c.type
10
10
  if c.appid && c.secret && token_file.present?
11
- Wechat::Api.new(c.appid, c.secret, token_file, c.timeout, c.skip_verify_ssl, js_token_file)
11
+ wx_class = (type == 'mp') ? Wechat::MpApi : Wechat::Api
12
+ wx_class.new(c.appid, c.secret, token_file, c.timeout, c.skip_verify_ssl, js_token_file)
12
13
  elsif c.corpid && c.corpsecret && token_file.present?
13
14
  Wechat::CorpApi.new(c.corpid, c.corpsecret, token_file, c.agentid, c.timeout, c.skip_verify_ssl, js_token_file)
14
15
  else
data/lib/wechat/cipher.rb CHANGED
@@ -1,10 +1,5 @@
1
- require 'openssl/cipher'
2
- require 'base64'
3
-
4
1
  module Wechat
5
2
  module Cipher
6
- extend ActiveSupport::Concern
7
-
8
3
  BLOCK_SIZE = 32
9
4
  CIPHER = 'AES-256-CBC'.freeze
10
5
 
@@ -0,0 +1,217 @@
1
+ module Wechat
2
+ module Concern
3
+ module Common
4
+ WXA_BASE = 'https://api.weixin.qq.com/wxa/'.freeze
5
+ API_BASE = 'https://api.weixin.qq.com/cgi-bin/'.freeze
6
+ OAUTH2_BASE = 'https://api.weixin.qq.com/sns/'.freeze
7
+
8
+ def initialize(appid, secret, token_file, timeout, skip_verify_ssl, jsapi_ticket_file)
9
+ @client = HttpClient.new(API_BASE, timeout, skip_verify_ssl)
10
+ @access_token = Token::PublicAccessToken.new(@client, appid, secret, token_file)
11
+ @jsapi_ticket = Ticket::PublicJsapiTicket.new(@client, @access_token, jsapi_ticket_file)
12
+ end
13
+
14
+ def groups
15
+ get 'groups/get'
16
+ end
17
+
18
+ def group_create(group_name)
19
+ post 'groups/create', JSON.generate(group: { name: group_name })
20
+ end
21
+
22
+ def group_update(groupid, new_group_name)
23
+ post 'groups/update', JSON.generate(group: { id: groupid, name: new_group_name })
24
+ end
25
+
26
+ def group_delete(groupid)
27
+ post 'groups/delete', JSON.generate(group: { id: groupid })
28
+ end
29
+
30
+ def users(nextid = nil)
31
+ params = { params: { next_openid: nextid } } if nextid.present?
32
+ get('user/get', params || {})
33
+ end
34
+
35
+ def user(openid)
36
+ get 'user/info', params: { openid: openid }
37
+ end
38
+
39
+ def user_batchget(openids, lang = 'zh-CN')
40
+ post 'user/info/batchget', JSON.generate(user_list: openids.collect { |v| { openid: v, lang: lang } })
41
+ end
42
+
43
+ def user_group(openid)
44
+ post 'groups/getid', JSON.generate(openid: openid)
45
+ end
46
+
47
+ def user_change_group(openid, to_groupid)
48
+ post 'groups/members/update', JSON.generate(openid: openid, to_groupid: to_groupid)
49
+ end
50
+
51
+ def user_update_remark(openid, remark)
52
+ post 'user/info/updateremark', JSON.generate(openid: openid, remark: remark)
53
+ end
54
+
55
+ def qrcode_create_scene(scene_id_or_str, expire_seconds = 604800)
56
+ case scene_id_or_str
57
+ when 0.class
58
+ post 'qrcode/create', JSON.generate(expire_seconds: expire_seconds,
59
+ action_name: 'QR_SCENE',
60
+ action_info: { scene: { scene_id: scene_id_or_str } })
61
+ else
62
+ post 'qrcode/create', JSON.generate(expire_seconds: expire_seconds,
63
+ action_name: 'QR_STR_SCENE',
64
+ action_info: { scene: { scene_str: scene_id_or_str } })
65
+ end
66
+ end
67
+
68
+ def qrcode_create_limit_scene(scene_id_or_str)
69
+ case scene_id_or_str
70
+ when 0.class
71
+ post 'qrcode/create', JSON.generate(action_name: 'QR_LIMIT_SCENE',
72
+ action_info: { scene: { scene_id: scene_id_or_str } })
73
+ else
74
+ post 'qrcode/create', JSON.generate(action_name: 'QR_LIMIT_STR_SCENE',
75
+ action_info: { scene: { scene_str: scene_id_or_str } })
76
+ end
77
+ end
78
+
79
+ def shorturl(long_url)
80
+ post 'shorturl', JSON.generate(action: 'long2short', long_url: long_url)
81
+ end
82
+
83
+ def message_mass_sendall(message)
84
+ post 'message/mass/sendall', message.to_json
85
+ end
86
+
87
+ def message_mass_delete(msg_id)
88
+ post 'message/mass/delete', JSON.generate(msg_id: msg_id)
89
+ end
90
+
91
+ def message_mass_preview(message)
92
+ post 'message/mass/preview', message.to_json
93
+ end
94
+
95
+ def message_mass_get(msg_id)
96
+ post 'message/mass/get', JSON.generate(msg_id: msg_id)
97
+ end
98
+
99
+ def wxa_get_wxacode(path, width = 430)
100
+ post 'getwxacode', JSON.generate(path: path, width: width), base: WXA_BASE
101
+ end
102
+
103
+ def wxa_create_qrcode(path, width = 430)
104
+ post 'wxaapp/createwxaqrcode', JSON.generate(path: path, width: width)
105
+ end
106
+
107
+ def menu
108
+ get 'menu/get'
109
+ end
110
+
111
+ def menu_delete
112
+ get 'menu/delete'
113
+ end
114
+
115
+ def menu_create(menu)
116
+ # 微信不接受7bit escaped json(eg \uxxxx), 中文必须UTF-8编码, 这可能是个安全漏洞
117
+ post 'menu/create', JSON.generate(menu)
118
+ end
119
+
120
+ def menu_addconditional(menu)
121
+ # Wechat not accept 7bit escaped json(eg \uxxxx), must using UTF-8, possible security vulnerability?
122
+ post 'menu/addconditional', JSON.generate(menu)
123
+ end
124
+
125
+ def menu_trymatch(user_id)
126
+ post 'menu/trymatch', JSON.generate(user_id: user_id)
127
+ end
128
+
129
+ def menu_delconditional(menuid)
130
+ post 'menu/delconditional', JSON.generate(menuid: menuid)
131
+ end
132
+
133
+ def material(media_id)
134
+ get 'material/get', params: { media_id: media_id }, as: :file
135
+ end
136
+
137
+ def material_count
138
+ get 'material/get_materialcount'
139
+ end
140
+
141
+ def material_list(type, offset, count)
142
+ post 'material/batchget_material', JSON.generate(type: type, offset: offset, count: count)
143
+ end
144
+
145
+ def material_add(type, file)
146
+ post_file 'material/add_material', file, params: { type: type }
147
+ end
148
+
149
+ def material_delete(media_id)
150
+ post 'material/del_material', JSON.generate(media_id: media_id)
151
+ end
152
+
153
+ def custom_message_send(message)
154
+ post 'message/custom/send', message.to_json, content_type: :json
155
+ end
156
+
157
+ def customservice_getonlinekflist
158
+ get 'customservice/getonlinekflist'
159
+ end
160
+
161
+ def tags
162
+ get 'tags/get'
163
+ end
164
+
165
+ def tag_create(tag_name)
166
+ post 'tags/create', JSON.generate(tag: { name: tag_name })
167
+ end
168
+
169
+ def tag_update(tagid, new_tag_name)
170
+ post 'tags/update', JSON.generate(tag: { id: tagid, name: new_tag_name })
171
+ end
172
+
173
+ def tag_delete(tagid)
174
+ post 'tags/delete', JSON.generate(tag: { id: tagid })
175
+ end
176
+
177
+ def tag_add_user(tagid, openids)
178
+ post 'tags/members/batchtagging', JSON.generate(openid_list: openids, tagid: tagid)
179
+ end
180
+
181
+ def tag_del_user(tagid, openids)
182
+ post 'tags/members/batchuntagging', JSON.generate(openid_list: openids, tagid: tagid)
183
+ end
184
+
185
+ def tag(tagid, next_openid = '')
186
+ post 'user/tag/get', JSON.generate(tagid: tagid, next_openid: next_openid)
187
+ end
188
+
189
+ def web_access_token(code)
190
+ params = {
191
+ appid: access_token.appid,
192
+ secret: access_token.secret,
193
+ code: code,
194
+ grant_type: 'authorization_code'
195
+ }
196
+ client.get 'oauth2/access_token', params: params, base: OAUTH2_BASE
197
+ end
198
+
199
+ def web_auth_access_token(web_access_token, openid)
200
+ client.get 'auth', params: { access_token: web_access_token, openid: openid }, base: OAUTH2_BASE
201
+ end
202
+
203
+ def web_refresh_access_token(user_refresh_token)
204
+ params = {
205
+ appid: access_token.appid,
206
+ grant_type: 'refresh_token',
207
+ refresh_token: user_refresh_token
208
+ }
209
+ client.get 'oauth2/refresh_token', params: params, base: OAUTH2_BASE
210
+ end
211
+
212
+ def web_userinfo(web_access_token, openid, lang = 'zh_CN')
213
+ client.get 'userinfo', params: { access_token: web_access_token, openid: openid, lang: lang }, base: OAUTH2_BASE
214
+ end
215
+ end
216
+ end
217
+ end
@@ -56,12 +56,11 @@ module Wechat
56
56
  # 42001: access_token timeout
57
57
  # 40014: invalid access_token
58
58
  # 40001, invalid credential, access_token is invalid or not latest hint
59
- # 48001, api unauthorized hint, for qrcode creation # 71
60
- when 42001, 40014, 40001, 48001
59
+ # 48001, api unauthorized hint, should not handle here # GH-230
60
+ when 42001, 40014, 40001
61
61
  raise AccessTokenExpiredError
62
+ # 40029, invalid code for mp # GH-225
62
63
  # 43004, require subscribe hint # GH-214
63
- when 43004
64
- Rails.logger.info "wechat gem template_message_send failure, errcode 43004, errmsg: #{data['errmsg']}"
65
64
  else
66
65
  raise ResponseError.new(data['errcode'], data['errmsg'])
67
66
  end
@@ -171,8 +171,11 @@ module Wechat
171
171
  update(MsgType: 'ref_mpnews', MpNews: { MediaId: media_id })
172
172
  end
173
173
 
174
+ TEMPLATE_KEYS = %i[template_id form_id page color
175
+ emphasis_keyword topcolor url miniprogram data].freeze
176
+
174
177
  def template(opts = {})
175
- template_fields = opts.symbolize_keys.slice(:template_id, :topcolor, :url, :data)
178
+ template_fields = opts.symbolize_keys.slice(*TEMPLATE_KEYS)
176
179
  update(MsgType: 'template', Template: template_fields)
177
180
  end
178
181
 
@@ -184,17 +187,20 @@ module Wechat
184
187
  end
185
188
 
186
189
  TO_JSON_KEY_MAP = {
187
- 'ToUserName' => 'touser',
188
- 'ToWxName' => 'towxname',
189
- 'MediaId' => 'media_id',
190
- 'MpNews' => 'mpnews',
191
- 'ThumbMediaId' => 'thumb_media_id',
192
- 'TemplateId' => 'template_id',
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',
193
197
  'ContentSourceUrl' => 'content_source_url',
194
- 'ShowCoverPic' => 'show_cover_pic'
198
+ 'ShowCoverPic' => 'show_cover_pic'
195
199
  }.freeze
196
200
 
197
- TO_JSON_ALLOWED = %w(touser msgtype content image voice video file music news articles template agentid filter send_ignore_reprint mpnews towxname).freeze
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
198
204
 
199
205
  def to_json
200
206
  keep_camel_case_key = message_hash[:MsgType] == 'template'
@@ -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
@@ -19,14 +19,18 @@ module Wechat
19
19
  skip_before_action :verify_authenticity_token, raise: false
20
20
  end
21
21
 
22
+ before_action :config_account, only: [:show, :create]
22
23
  before_action :verify_signature, only: [:show, :create]
23
24
  else
24
25
  skip_before_filter :verify_authenticity_token
26
+ before_filter :config_account, only: [:show, :create]
25
27
  before_filter :verify_signature, only: [:show, :create]
26
28
  end
27
29
  end
28
30
 
29
31
  module ClassMethods
32
+ attr_accessor :account_from_request
33
+
30
34
  def on(message_type, with: nil, respond: nil, &block)
31
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)
32
36
  config = respond.nil? ? {} : { respond: respond }
@@ -170,8 +174,8 @@ module Wechat
170
174
  end
171
175
 
172
176
  def show
173
- if self.class.corpid.present?
174
- echostr, _corp_id = unpack(decrypt(Base64.decode64(params[:echostr]), self.class.encoding_aes_key))
177
+ if @we_corpid.present?
178
+ echostr, _corp_id = unpack(decrypt(Base64.decode64(params[:echostr]), @we_encoding_aes_key))
175
179
  if Rails::VERSION::MAJOR >= 4
176
180
  render plain: echostr
177
181
  else
@@ -207,17 +211,27 @@ module Wechat
207
211
 
208
212
  private
209
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
+
210
224
  def verify_signature
211
- if self.class.encrypt_mode
225
+ if @we_encrypt_mode
212
226
  signature = params[:signature] || params[:msg_signature]
213
227
  msg_encrypt = params[:echostr] || request_encrypt_content
214
228
  else
215
229
  signature = params[:signature]
216
230
  end
217
231
 
218
- msg_encrypt = nil unless self.class.corpid.present?
232
+ msg_encrypt = nil unless @we_corpid.present?
219
233
 
220
- render plain: 'Forbidden', status: 403 if signature != Signature.hexdigest(self.class.token,
234
+ render plain: 'Forbidden', status: 403 if signature != Signature.hexdigest(@we_token,
221
235
  params[:timestamp],
222
236
  params[:nonce],
223
237
  msg_encrypt)
@@ -226,8 +240,8 @@ module Wechat
226
240
  def post_xml
227
241
  data = request_content
228
242
 
229
- if self.class.encrypt_mode && request_encrypt_content.present?
230
- content, @app_id = unpack(decrypt(Base64.decode64(request_encrypt_content), self.class.encoding_aes_key))
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))
231
245
  data = Hash.from_xml(content)
232
246
  end
233
247
 
@@ -265,8 +279,8 @@ module Wechat
265
279
  def process_response(response)
266
280
  msg = response[:MsgType] == 'success' ? 'success' : response.to_xml
267
281
 
268
- if self.class.encrypt_mode
269
- encrypt = Base64.strict_encode64(encrypt(pack(msg, @app_id), self.class.encoding_aes_key))
282
+ if @we_encrypt_mode
283
+ encrypt = Base64.strict_encode64(encrypt(pack(msg, @we_app_id), @we_encoding_aes_key))
270
284
  msg = gen_msg(encrypt, params[:timestamp], params[:nonce])
271
285
  end
272
286
 
@@ -274,7 +288,7 @@ module Wechat
274
288
  end
275
289
 
276
290
  def gen_msg(encrypt, timestamp, nonce)
277
- msg_sign = Signature.hexdigest(self.class.token, timestamp, nonce, encrypt)
291
+ msg_sign = Signature.hexdigest(@we_token, timestamp, nonce, encrypt)
278
292
 
279
293
  { Encrypt: encrypt,
280
294
  MsgSignature: msg_sign,
@@ -284,7 +298,7 @@ module Wechat
284
298
  end
285
299
 
286
300
  def request_encrypt_content
287
- request_content['xml']['Encrypt']
301
+ request_content&.dig('xml', 'Encrypt')
288
302
  end
289
303
 
290
304
  def request_content
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wechat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Skinnyworm
@@ -31,7 +31,7 @@ cert_chain:
31
31
  fGxGnQhzVaW07NKOCRrAZlrF8iqso4JR7Vm9bhFdzxUPLr70njwHLtDS2CHgo1VW
32
32
  1xAjN8ZXXpAmVv7V6cI9RTmQHPu/fFn+E0sG9w==
33
33
  -----END CERTIFICATE-----
34
- date: 2018-04-15 00:00:00.000000000 Z
34
+ date: 2018-05-31 00:00:00.000000000 Z
35
35
  dependencies:
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: activesupport
@@ -53,6 +53,26 @@ dependencies:
53
53
  - - "<="
54
54
  - !ruby/object:Gem::Version
55
55
  version: '5.2'
56
+ - !ruby/object:Gem::Dependency
57
+ name: http
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 1.0.4
63
+ - - "<"
64
+ - !ruby/object:Gem::Version
65
+ version: '4'
66
+ type: :runtime
67
+ prerelease: false
68
+ version_requirements: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 1.0.4
73
+ - - "<"
74
+ - !ruby/object:Gem::Version
75
+ version: '4'
56
76
  - !ruby/object:Gem::Dependency
57
77
  name: nokogiri
58
78
  requirement: !ruby/object:Gem::Requirement
@@ -82,25 +102,19 @@ dependencies:
82
102
  - !ruby/object:Gem::Version
83
103
  version: '0'
84
104
  - !ruby/object:Gem::Dependency
85
- name: http
105
+ name: rails
86
106
  requirement: !ruby/object:Gem::Requirement
87
107
  requirements:
88
108
  - - ">="
89
109
  - !ruby/object:Gem::Version
90
- version: 1.0.4
91
- - - "<"
92
- - !ruby/object:Gem::Version
93
- version: '4'
94
- type: :runtime
110
+ version: '5.1'
111
+ type: :development
95
112
  prerelease: false
96
113
  version_requirements: !ruby/object:Gem::Requirement
97
114
  requirements:
98
115
  - - ">="
99
116
  - !ruby/object:Gem::Version
100
- version: 1.0.4
101
- - - "<"
102
- - !ruby/object:Gem::Version
103
- version: '4'
117
+ version: '5.1'
104
118
  - !ruby/object:Gem::Dependency
105
119
  name: rspec-rails
106
120
  requirement: !ruby/object:Gem::Requirement
@@ -115,20 +129,6 @@ dependencies:
115
129
  - - "~>"
116
130
  - !ruby/object:Gem::Version
117
131
  version: '3.6'
118
- - !ruby/object:Gem::Dependency
119
- name: rails
120
- requirement: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - ">="
123
- - !ruby/object:Gem::Version
124
- version: '5.1'
125
- type: :development
126
- prerelease: false
127
- version_requirements: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ">="
130
- - !ruby/object:Gem::Version
131
- version: '5.1'
132
132
  - !ruby/object:Gem::Dependency
133
133
  name: sqlite3
134
134
  requirement: !ruby/object:Gem::Requirement
@@ -176,11 +176,13 @@ files:
176
176
  - lib/wechat/api_base.rb
177
177
  - lib/wechat/api_loader.rb
178
178
  - lib/wechat/cipher.rb
179
+ - lib/wechat/concern/common.rb
179
180
  - lib/wechat/controller_api.rb
180
181
  - lib/wechat/corp_api.rb
181
182
  - lib/wechat/helpers.rb
182
183
  - lib/wechat/http_client.rb
183
184
  - lib/wechat/message.rb
185
+ - lib/wechat/mp_api.rb
184
186
  - lib/wechat/responder.rb
185
187
  - lib/wechat/signature.rb
186
188
  - lib/wechat/ticket/corp_jsapi_ticket.rb
@@ -199,9 +201,9 @@ require_paths:
199
201
  - lib
200
202
  required_ruby_version: !ruby/object:Gem::Requirement
201
203
  requirements:
202
- - - ">="
204
+ - - "~>"
203
205
  - !ruby/object:Gem::Version
204
- version: '0'
206
+ version: '2.3'
205
207
  required_rubygems_version: !ruby/object:Gem::Requirement
206
208
  requirements:
207
209
  - - ">="
metadata.gz.sig CHANGED
Binary file