gfd_wechat 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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