gfd_wechat 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +321 -0
- data/LICENSE +21 -0
- data/README-CN.md +815 -0
- data/README.md +844 -0
- data/bin/wechat +520 -0
- data/lib/action_controller/wechat_responder.rb +72 -0
- data/lib/generators/wechat/config_generator.rb +36 -0
- data/lib/generators/wechat/install_generator.rb +20 -0
- data/lib/generators/wechat/menu_generator.rb +21 -0
- data/lib/generators/wechat/redis_store_generator.rb +16 -0
- data/lib/generators/wechat/session_generator.rb +36 -0
- data/lib/generators/wechat/templates/MENU_README +3 -0
- data/lib/generators/wechat/templates/app/controllers/wechats_controller.rb +12 -0
- data/lib/generators/wechat/templates/app/models/wechat_config.rb +46 -0
- data/lib/generators/wechat/templates/app/models/wechat_session.rb +17 -0
- data/lib/generators/wechat/templates/config/initializers/wechat_redis_store.rb +42 -0
- data/lib/generators/wechat/templates/config/wechat.yml +72 -0
- data/lib/generators/wechat/templates/config/wechat_menu.yml +6 -0
- data/lib/generators/wechat/templates/config/wechat_menu_android.yml +15 -0
- data/lib/generators/wechat/templates/db/config_migration.rb.erb +40 -0
- data/lib/generators/wechat/templates/db/session_migration.rb.erb +10 -0
- data/lib/wechat/api.rb +54 -0
- data/lib/wechat/api_base.rb +63 -0
- data/lib/wechat/api_loader.rb +145 -0
- data/lib/wechat/cipher.rb +66 -0
- data/lib/wechat/concern/common.rb +217 -0
- data/lib/wechat/controller_api.rb +96 -0
- data/lib/wechat/corp_api.rb +168 -0
- data/lib/wechat/helpers.rb +47 -0
- data/lib/wechat/http_client.rb +112 -0
- data/lib/wechat/message.rb +265 -0
- data/lib/wechat/mp_api.rb +46 -0
- data/lib/wechat/responder.rb +308 -0
- data/lib/wechat/signature.rb +10 -0
- data/lib/wechat/ticket/corp_jsapi_ticket.rb +14 -0
- data/lib/wechat/ticket/jsapi_base.rb +84 -0
- data/lib/wechat/ticket/public_jsapi_ticket.rb +14 -0
- data/lib/wechat/token/access_token_base.rb +53 -0
- data/lib/wechat/token/corp_access_token.rb +13 -0
- data/lib/wechat/token/public_access_token.rb +13 -0
- data/lib/wechat.rb +52 -0
- metadata +195 -0
@@ -0,0 +1,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
|