gfd_wechat 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +321 -0
  3. data/LICENSE +21 -0
  4. data/README-CN.md +815 -0
  5. data/README.md +844 -0
  6. data/bin/wechat +520 -0
  7. data/lib/action_controller/wechat_responder.rb +72 -0
  8. data/lib/generators/wechat/config_generator.rb +36 -0
  9. data/lib/generators/wechat/install_generator.rb +20 -0
  10. data/lib/generators/wechat/menu_generator.rb +21 -0
  11. data/lib/generators/wechat/redis_store_generator.rb +16 -0
  12. data/lib/generators/wechat/session_generator.rb +36 -0
  13. data/lib/generators/wechat/templates/MENU_README +3 -0
  14. data/lib/generators/wechat/templates/app/controllers/wechats_controller.rb +12 -0
  15. data/lib/generators/wechat/templates/app/models/wechat_config.rb +46 -0
  16. data/lib/generators/wechat/templates/app/models/wechat_session.rb +17 -0
  17. data/lib/generators/wechat/templates/config/initializers/wechat_redis_store.rb +42 -0
  18. data/lib/generators/wechat/templates/config/wechat.yml +72 -0
  19. data/lib/generators/wechat/templates/config/wechat_menu.yml +6 -0
  20. data/lib/generators/wechat/templates/config/wechat_menu_android.yml +15 -0
  21. data/lib/generators/wechat/templates/db/config_migration.rb.erb +40 -0
  22. data/lib/generators/wechat/templates/db/session_migration.rb.erb +10 -0
  23. data/lib/wechat/api.rb +54 -0
  24. data/lib/wechat/api_base.rb +63 -0
  25. data/lib/wechat/api_loader.rb +145 -0
  26. data/lib/wechat/cipher.rb +66 -0
  27. data/lib/wechat/concern/common.rb +217 -0
  28. data/lib/wechat/controller_api.rb +96 -0
  29. data/lib/wechat/corp_api.rb +168 -0
  30. data/lib/wechat/helpers.rb +47 -0
  31. data/lib/wechat/http_client.rb +112 -0
  32. data/lib/wechat/message.rb +265 -0
  33. data/lib/wechat/mp_api.rb +46 -0
  34. data/lib/wechat/responder.rb +308 -0
  35. data/lib/wechat/signature.rb +10 -0
  36. data/lib/wechat/ticket/corp_jsapi_ticket.rb +14 -0
  37. data/lib/wechat/ticket/jsapi_base.rb +84 -0
  38. data/lib/wechat/ticket/public_jsapi_ticket.rb +14 -0
  39. data/lib/wechat/token/access_token_base.rb +53 -0
  40. data/lib/wechat/token/corp_access_token.rb +13 -0
  41. data/lib/wechat/token/public_access_token.rb +13 -0
  42. data/lib/wechat.rb +52 -0
  43. metadata +195 -0
@@ -0,0 +1,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.is_a?(Wechat::Message) ? message.to_json : JSON.generate(message), 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
@@ -0,0 +1,96 @@
1
+ module Wechat
2
+ module ControllerApi
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ attr_accessor :wechat_api_client, :wechat_cfg_account, :token, :appid, :corpid, :agentid, :encrypt_mode, :timeout,
7
+ :skip_verify_ssl, :encoding_aes_key, :trusted_domain_fullname, :oauth2_cookie_duration
8
+ end
9
+
10
+ def wechat(account = nil)
11
+ # Make sure user can continue access wechat at instance level similar to class level
12
+ self.class.wechat(account)
13
+ end
14
+
15
+ def wechat_oauth2(scope = 'snsapi_base', page_url = nil, account = nil, &block)
16
+ # ensure wechat initialization
17
+ self.class.corpid || self.class.appid || self.class.wechat
18
+
19
+ api = wechat(account)
20
+ if account
21
+ config = Wechat.config(account)
22
+ appid = config.corpid || config.appid
23
+ is_crop_account = !!config.corpid
24
+ else
25
+ appid = self.class.corpid || self.class.appid
26
+ is_crop_account = !!self.class.corpid
27
+ end
28
+
29
+ raise 'Can not get corpid or appid, so please configure it first to using wechat_oauth2' if appid.blank?
30
+
31
+ oauth2_params = {
32
+ appid: appid,
33
+ redirect_uri: page_url || generate_redirect_uri(account),
34
+ scope: scope,
35
+ response_type: 'code',
36
+ state: api.jsapi_ticket.oauth2_state
37
+ }
38
+
39
+ return generate_oauth2_url(oauth2_params) unless block_given?
40
+ is_crop_account ? wechat_corp_oauth2(oauth2_params, account, &block) : wechat_public_oauth2(oauth2_params, account, &block)
41
+ end
42
+
43
+ private
44
+
45
+ def wechat_public_oauth2(oauth2_params, account = nil)
46
+ openid = cookies.signed_or_encrypted[:we_openid]
47
+ unionid = cookies.signed_or_encrypted[:we_unionid]
48
+ we_token = cookies.signed_or_encrypted[:we_access_token]
49
+ if openid.present?
50
+ yield openid, { 'openid' => openid, 'unionid' => unionid, 'access_token' => we_token}
51
+ elsif params[:code].present? && params[:state] == oauth2_params[:state]
52
+ access_info = wechat(account).web_access_token(params[:code])
53
+ cookies.signed_or_encrypted[:we_openid] = { value: access_info['openid'], expires: self.class.oauth2_cookie_duration.from_now }
54
+ cookies.signed_or_encrypted[:we_unionid] = { value: access_info['unionid'], expires: self.class.oauth2_cookie_duration.from_now }
55
+ cookies.signed_or_encrypted[:we_access_token] = { value: access_info['access_token'], expires: self.class.oauth2_cookie_duration.from_now }
56
+ yield access_info['openid'], access_info
57
+ else
58
+ redirect_to generate_oauth2_url(oauth2_params)
59
+ end
60
+ end
61
+
62
+ def wechat_corp_oauth2(oauth2_params, account = nil)
63
+ userid = cookies.signed_or_encrypted[:we_userid]
64
+ deviceid = cookies.signed_or_encrypted[:we_deviceid]
65
+ if userid.present? && deviceid.present?
66
+ yield userid, { 'UserId' => userid, 'DeviceId' => deviceid }
67
+ elsif params[:code].present? && params[:state] == oauth2_params[:state]
68
+ userinfo = wechat(account).getuserinfo(params[:code])
69
+ cookies.signed_or_encrypted[:we_userid] = { value: userinfo['UserId'], expires: self.class.oauth2_cookie_duration.from_now }
70
+ cookies.signed_or_encrypted[:we_deviceid] = { value: userinfo['DeviceId'], expires: self.class.oauth2_cookie_duration.from_now }
71
+ yield userinfo['UserId'], userinfo
72
+ else
73
+ redirect_to generate_oauth2_url(oauth2_params)
74
+ end
75
+ end
76
+
77
+ def generate_redirect_uri(account = nil)
78
+ domain_name = if account
79
+ Wechat.config(account).trusted_domain_fullname
80
+ else
81
+ self.class.trusted_domain_fullname
82
+ end
83
+ page_url = domain_name ? "#{domain_name}#{request.original_fullpath}" : request.original_url
84
+ safe_query = request.query_parameters.reject { |k, _| %w(code state access_token).include? k }.to_query
85
+ page_url.sub(request.query_string, safe_query)
86
+ end
87
+
88
+ def generate_oauth2_url(oauth2_params)
89
+ if oauth2_params[:scope] == 'snsapi_login'
90
+ "https://open.weixin.qq.com/connect/qrconnect?#{oauth2_params.to_query}#wechat_redirect"
91
+ else
92
+ "https://open.weixin.qq.com/connect/oauth2/authorize?#{oauth2_params.to_query}#wechat_redirect"
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,168 @@
1
+ require 'wechat/api_base'
2
+ require 'wechat/http_client'
3
+ require 'wechat/token/corp_access_token'
4
+ require 'wechat/ticket/corp_jsapi_ticket'
5
+
6
+ module Wechat
7
+ class CorpApi < ApiBase
8
+ attr_reader :agentid
9
+
10
+ API_BASE = 'https://qyapi.weixin.qq.com/cgi-bin/'.freeze
11
+
12
+ def initialize(appid, secret, token_file, agentid, timeout, skip_verify_ssl, jsapi_ticket_file)
13
+ @client = HttpClient.new(API_BASE, timeout, skip_verify_ssl)
14
+ @access_token = Token::CorpAccessToken.new(@client, appid, secret, token_file)
15
+ @agentid = agentid
16
+ @jsapi_ticket = Ticket::CorpJsapiTicket.new(@client, @access_token, jsapi_ticket_file)
17
+ end
18
+
19
+ def agent_list
20
+ get 'agent/list'
21
+ end
22
+
23
+ def agent(agentid)
24
+ get 'agent/get', params: { agentid: agentid }
25
+ end
26
+
27
+ def user(userid)
28
+ get 'user/get', params: { userid: userid }
29
+ end
30
+
31
+ def getuserinfo(code)
32
+ get 'user/getuserinfo', params: { code: code }
33
+ end
34
+
35
+ def convert_to_openid(userid)
36
+ post 'user/convert_to_openid', JSON.generate(userid: userid, agentid: agentid)
37
+ end
38
+
39
+ def invite_user(userid)
40
+ post 'invite/send', JSON.generate(userid: userid)
41
+ end
42
+
43
+ def user_auth_success(userid)
44
+ get 'user/authsucc', params: { userid: userid }
45
+ end
46
+
47
+ def user_create(user)
48
+ post 'user/create', JSON.generate(user)
49
+ end
50
+
51
+ def user_delete(userid)
52
+ get 'user/delete', params: { userid: userid }
53
+ end
54
+
55
+ def user_batchdelete(useridlist)
56
+ post 'user/batchdelete', JSON.generate(useridlist: useridlist)
57
+ end
58
+
59
+ def batch_job_result(jobid)
60
+ get 'batch/getresult', params: { jobid: jobid }
61
+ end
62
+
63
+ def batch_replaceparty(media_id)
64
+ post 'batch/replaceparty', JSON.generate(media_id: media_id)
65
+ end
66
+
67
+ def batch_syncuser(media_id)
68
+ post 'batch/syncuser', JSON.generate(media_id: media_id)
69
+ end
70
+
71
+ def batch_replaceuser(media_id)
72
+ post 'batch/replaceuser', JSON.generate(media_id: media_id)
73
+ end
74
+
75
+ def department_create(name, parentid)
76
+ post 'department/create', JSON.generate(name: name, parentid: parentid)
77
+ end
78
+
79
+ def department_delete(departmentid)
80
+ get 'department/delete', params: { id: departmentid }
81
+ end
82
+
83
+ def department_update(departmentid, name = nil, parentid = nil, order = nil)
84
+ post 'department/update', JSON.generate({ id: departmentid, name: name, parentid: parentid, order: order }.reject { |_k, v| v.nil? })
85
+ end
86
+
87
+ def department(departmentid = 1)
88
+ get 'department/list', params: { id: departmentid }
89
+ end
90
+
91
+ def user_simplelist(department_id, fetch_child = 0, status = 0)
92
+ get 'user/simplelist', params: { department_id: department_id, fetch_child: fetch_child, status: status }
93
+ end
94
+
95
+ def user_list(department_id, fetch_child = 0, status = 0)
96
+ get 'user/list', params: { department_id: department_id, fetch_child: fetch_child, status: status }
97
+ end
98
+
99
+ def tag_create(tagname, tagid = nil)
100
+ post 'tag/create', JSON.generate(tagname: tagname, tagid: tagid)
101
+ end
102
+
103
+ def tag_update(tagid, tagname)
104
+ post 'tag/update', JSON.generate(tagid: tagid, tagname: tagname)
105
+ end
106
+
107
+ def tag_delete(tagid)
108
+ get 'tag/delete', params: { tagid: tagid }
109
+ end
110
+
111
+ def tags
112
+ get 'tag/list'
113
+ end
114
+
115
+ def tag(tagid)
116
+ get 'tag/get', params: { tagid: tagid }
117
+ end
118
+
119
+ def tag_add_user(tagid, userids = nil, departmentids = nil)
120
+ post 'tag/addtagusers', JSON.generate(tagid: tagid, userlist: userids, partylist: departmentids)
121
+ end
122
+
123
+ def tag_del_user(tagid, userids = nil, departmentids = nil)
124
+ post 'tag/deltagusers', JSON.generate(tagid: tagid, userlist: userids, partylist: departmentids)
125
+ end
126
+
127
+ def menu
128
+ get 'menu/get', params: { agentid: agentid }
129
+ end
130
+
131
+ def menu_delete
132
+ get 'menu/delete', params: { agentid: agentid }
133
+ end
134
+
135
+ def menu_create(menu)
136
+ # 微信不接受7bit escaped json(eg \uxxxx), 中文必须UTF-8编码, 这可能是个安全漏洞
137
+ post 'menu/create', JSON.generate(menu), params: { agentid: agentid }
138
+ end
139
+
140
+ def material_count
141
+ get 'material/get_count', params: { agentid: agentid }
142
+ end
143
+
144
+ def material_list(type, offset, count)
145
+ post 'material/batchget', JSON.generate(type: type, agentid: agentid, offset: offset, count: count)
146
+ end
147
+
148
+ def material(media_id)
149
+ get 'material/get', params: { media_id: media_id, agentid: agentid }, as: :file
150
+ end
151
+
152
+ def material_add(type, file)
153
+ post_file 'material/add_material', file, params: { type: type, agentid: agentid }
154
+ end
155
+
156
+ def material_delete(media_id)
157
+ get 'material/del', params: { media_id: media_id, agentid: agentid }
158
+ end
159
+
160
+ def message_send(userid, message)
161
+ post 'message/send', Message.to(userid).text(message).agent_id(agentid).to_json, content_type: :json
162
+ end
163
+
164
+ def custom_message_send(message)
165
+ post 'message/send', message.is_a?(Wechat::Message) ? message.agent_id(agentid).to_json : JSON.generate(message.merge(agent_id: agentid)), content_type: :json
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,47 @@
1
+ module Wechat
2
+ module Helpers
3
+ def wechat_config_js(config_options = {})
4
+ account = config_options[:account]
5
+
6
+ # Get domain_name, api and app_id
7
+ if account.blank? || account == controller.class.wechat_cfg_account
8
+ # default account
9
+ domain_name = controller.class.trusted_domain_fullname
10
+ api = controller.wechat
11
+ app_id = controller.class.corpid || controller.class.appid
12
+ else
13
+ # not default account
14
+ config = Wechat.config(account)
15
+ domain_name = config.trusted_domain_fullname
16
+ api = controller.wechat(account)
17
+ app_id = config.corpid || config.appid
18
+ end
19
+
20
+ page_url = if domain_name
21
+ "#{domain_name}#{controller.request.original_fullpath}"
22
+ else
23
+ controller.request.original_url
24
+ end
25
+ page_url = page_url.split('#').first if is_ios?
26
+ js_hash = api.jsapi_ticket.signature(page_url)
27
+
28
+ config_js = <<-WECHAT_CONFIG_JS
29
+ wx.config({
30
+ debug: #{config_options[:debug]},
31
+ appId: "#{app_id}",
32
+ timestamp: "#{js_hash[:timestamp]}",
33
+ nonceStr: "#{js_hash[:noncestr]}",
34
+ signature: "#{js_hash[:signature]}",
35
+ jsApiList: ['#{config_options[:api].join("','")}']
36
+ });
37
+ WECHAT_CONFIG_JS
38
+ javascript_tag config_js, type: 'application/javascript'
39
+ end
40
+
41
+ private
42
+
43
+ def is_ios?
44
+ controller.request.user_agent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,112 @@
1
+ require 'http'
2
+
3
+ module Wechat
4
+ class HttpClient
5
+ attr_reader :base, :ssl_context, :httprb
6
+
7
+ def initialize(base, timeout, skip_verify_ssl)
8
+ @base = base
9
+ @httprb = HTTP.timeout(:global, write: timeout, connect: timeout, read: timeout)
10
+ @ssl_context = OpenSSL::SSL::SSLContext.new
11
+ @ssl_context.ssl_version = :TLSv1
12
+ @ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE if skip_verify_ssl
13
+ end
14
+
15
+ def get(path, get_header = {})
16
+ request(path, get_header) do |url, header|
17
+ params = header.delete(:params)
18
+ httprb.headers(header).get(url, params: params, ssl_context: ssl_context)
19
+ end
20
+ end
21
+
22
+ def post(path, payload, post_header = {})
23
+ request(path, post_header) do |url, header|
24
+ params = header.delete(:params)
25
+ httprb.headers(header).post(url, params: params, body: payload, ssl_context: ssl_context)
26
+ end
27
+ end
28
+
29
+ def post_file(path, file, post_header = {})
30
+ request(path, post_header) do |url, header|
31
+ params = header.delete(:params)
32
+ form_file = file.is_a?(HTTP::FormData::File) ? file : HTTP::FormData::File.new(file)
33
+ httprb.headers(header)
34
+ .post(url, params: params,
35
+ form: { media: form_file,
36
+ hack: 'X' }, # Existing here for http-form_data 1.0.1 handle single param improperly
37
+ ssl_context: ssl_context)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def request(path, header = {}, &_block)
44
+ url_base = header.delete(:base) || base
45
+ as = header.delete(:as)
46
+ access_token = header.delete(:access_token)
47
+ token = access_token.present? ? "?access_token=#{access_token}" : nil
48
+ header['Accept'] ||= 'application/json'
49
+ response = yield("#{url_base}#{path}#{token}", header)
50
+
51
+ raise "Request not OK, response status #{response.status}" if response.status != 200
52
+ parse_response(response, as || :json) do |parse_as, data|
53
+ break data unless parse_as == :json && data['errcode'].present?
54
+
55
+ case data['errcode']
56
+ when 0 # for request didn't expect results
57
+ data
58
+ # 42001: access_token timeout
59
+ # 40014: invalid access_token
60
+ # 40001, invalid credential, access_token is invalid or not latest hint
61
+ # 48001, api unauthorized hint, should not handle here # GH-230
62
+ when 42001, 40014, 40001
63
+ raise AccessTokenExpiredError
64
+ # 40029, invalid code for mp # GH-225
65
+ # 43004, require subscribe hint # GH-214
66
+ else
67
+ raise ResponseError.new(data['errcode'], data['errmsg'])
68
+ end
69
+ end
70
+ end
71
+
72
+ def parse_response(response, as)
73
+ content_type = response.headers[:content_type]
74
+ parse_as = {
75
+ %r{^application\/json} => :json,
76
+ %r{^image\/.*} => :file,
77
+ %r{^audio\/.*} => :file,
78
+ %r{^voice\/.*} => :file,
79
+ %r{^text\/html} => :xml,
80
+ %r{^text\/plain} => :probably_json
81
+ }.each_with_object([]) { |match, memo| memo << match[1] if content_type =~ match[0] }.first || as || :text
82
+
83
+ # try to parse response as json, fallback to user-specified format or text if failed
84
+ if parse_as == :probably_json
85
+ data = JSON.parse response.body.to_s.gsub(/[\u0000-\u001f]+/, '') rescue nil
86
+ if data
87
+ return yield(:json, data)
88
+ else
89
+ parse_as = as || :text
90
+ end
91
+ end
92
+
93
+ case parse_as
94
+ when :file
95
+ file = Tempfile.new('tmp')
96
+ file.binmode
97
+ file.write(response.body)
98
+ file.close
99
+ data = file
100
+
101
+ when :json
102
+ data = JSON.parse response.body.to_s.gsub(/[\u0000-\u001f]+/, '')
103
+ when :xml
104
+ data = Hash.from_xml(response.body.to_s)
105
+ else
106
+ data = response.body
107
+ end
108
+
109
+ yield(parse_as, data)
110
+ end
111
+ end
112
+ end