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