wechat 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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