wechat 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f1a9041e7d411063724f87e3a8393a120542e1bf
4
- data.tar.gz: 1dfa35792fa8f41f9872b00e645196115658308c
3
+ metadata.gz: d8fc6b062fba978416473332b7eeb0ebe89a6bfd
4
+ data.tar.gz: 5b7c50450e9ca878520825678580aec7f2e7b4f0
5
5
  SHA512:
6
- metadata.gz: 14df5df5f86412bbb4076095903884c26a8a7942927d3afa5bdb34d2b0d4c65ff330f41fed0bdf38f970f80893117429efd48e531b6a8f9f566b169314cbfc3d
7
- data.tar.gz: 7daf46b7df72eb76dc8508a6bbc6f575acd44e2beb214d9c81d6cb51bf551591ec0c1a172d942e89e0b65fe92fc5bd713454ccd7a4b93add0a40059500a9fc36
6
+ metadata.gz: 0131981fbcfa43e241ec4151a004ec702185f41641e9a67ad8cf45adf59f9372586409ee0a34f5cc9f3141dd3c44a056fd6c13d906ae3abe8bdb5c171d08bda5
7
+ data.tar.gz: 40b6edfe0146f00d27cb41ce14c5dc612892d59569fc1469355ce52245e560893c936ec43b0a159e0838435968720f4f6b00750259c6b940c9b7c998f7383dca
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.4.0 (released at 9/5/2015)
4
+
5
+ * Enable the verify SSL for enterprise mode by default, as security is more importent than speed, but still can switch off by configure
6
+ * Support scancode_push/scancode_waitmsg event.
7
+ * New API method can get wechat server IP list
8
+ * New API to query/create department/media/material
9
+ * Fix can not read token_file in mingw bug, which introduce at #43
10
+
3
11
  ## v0.3.0 (released at 8/30/2015)
4
12
 
5
13
  * New user group management API
data/README.md CHANGED
@@ -83,7 +83,6 @@ default: &default
83
83
  corpsecret: "corpsecret"
84
84
  agentid: "1"
85
85
  access_token: "C:/Users/[user_name]/wechat_access_token"
86
- encrypt_mode: true
87
86
  token: ""
88
87
  encoding_aes_key: ""
89
88
 
@@ -92,8 +91,8 @@ production:
92
91
  corpsecret: <%= ENV['WECHAT_CORPSECRET'] %>
93
92
  agentid: <%= ENV['WECHAT_AGENTID'] %>
94
93
  access_token: <%= ENV['WECHAT_ACCESS_TOKEN'] %>
95
- encrypt_mode: <%= ENV['WECHAT_ENCRYPT_MODE'] %>
96
94
  token: <%= ENV['WECHAT_TOKEN'] %>
95
+ skip_verify_ssl: false
97
96
  encoding_aes_key: <%= ENV['WECHAT_ENCODING_AES_KEY'] %>
98
97
 
99
98
  development:
@@ -103,8 +102,14 @@ test:
103
102
  <<: *default
104
103
  ```
105
104
 
105
+ ##### 配置优先级
106
+
106
107
  注意在Rails项目根目录下运行`wechat`命令行工具会优先使用`config/wechat.yml`中的`default`配置,如果失败则使用`~\.wechat.yml`中的配置,以便于在生产环境下管理多个微信账号应用。
107
108
 
109
+ ##### 配置跳过SSL认证
110
+
111
+ Wechat服务器有报道曾出现[RestClient::SSLCertificateNotVerified](http://qydev.weixin.qq.com/qa/index.php?qa=11037)错误,此时可以选择关闭SSL验证(skip_verify_ssl)。
112
+
108
113
  #### 为每个Responder配置不同的appid和secret
109
114
 
110
115
  在个别情况下,单个Rails应用可能需要处理来自多个账号的消息,此时可以配置多个responder controller。
@@ -134,16 +139,21 @@ wechat gems 内部不会检查权限。但因公众号类型不同,和微信
134
139
  ```
135
140
  $ wechat
136
141
  Wechat commands:
142
+ wechat callbackip # 获取微信服务器IP地址
137
143
  wechat custom_image [OPENID, IMAGE_PATH] # 发送图片客服消息
138
144
  wechat custom_music [OPENID, THUMBNAIL_PATH, MUSIC_URL] # 发送音乐客服消息
139
145
  wechat custom_news [OPENID, NEWS_YAML_PATH] # 发送图文客服消息
140
146
  wechat custom_text [OPENID, TEXT_MESSAGE] # 发送文字客服消息
141
147
  wechat custom_video [OPENID, VIDEO_PATH] # 发送视频客服消息
142
148
  wechat custom_voice [OPENID, VOICE_PATH] # 发送语音客服消息
149
+ wechat department [DEPARTMENT_ID] # 获取部门列表
143
150
  wechat group_create [GROUP_NAME] # 创建分组
144
151
  wechat group_delete [GROUP_ID] # 删除分组
145
152
  wechat group_update [GROUP_ID, NEW_GROUP_NAME] # 修改分组名
146
153
  wechat groups # 所有用户分组列表
154
+ wechat invite_user [USER_ID] # 邀请成员关注
155
+ wechat material [MEDIA_ID, PATH] # 永久媒体下载
156
+ wechat material_add [MEDIA_TYPE, PATH] # 永久媒体上传
147
157
  wechat media [MEDIA_ID, PATH] # 媒体下载
148
158
  wechat media_create [MEDIA_TYPE, PATH] # 媒体上传
149
159
  wechat menu # 当前菜单
@@ -151,8 +161,9 @@ Wechat commands:
151
161
  wechat menu_delete # 删除菜单
152
162
  wechat message_send [OPENID, TEXT_MESSAGE] # 发送文字消息(仅企业号)
153
163
  wechat template_message [OPENID, TEMPLATE_YAML_PATH] # 模板消息接口
154
- wechat user [OPEN_ID] # 查找关注者
164
+ wechat user [OPEN_ID] # 获取用户基本信息
155
165
  wechat user_change_group [OPEN_ID, TO_GROUP_ID] # 移动用户分组
166
+ wechat user_delete [USER_ID] # 删除成员
156
167
  wechat user_group [OPEN_ID] # 查询用户所在分组
157
168
  wechat users # 关注者列表
158
169
  ```
@@ -203,6 +214,12 @@ button:
203
214
  -
204
215
  name: "我要"
205
216
  sub_button:
217
+ -
218
+ type: "scancode_waitmsg"
219
+ name: "绑定用餐二维码"
220
+ key: "BINDING_QR_CODE"
221
+ sub_button:
222
+ -
206
223
  -
207
224
  type: "click"
208
225
  name: "预订午餐"
@@ -215,12 +232,6 @@ button:
215
232
  key: "BOOK_DINNER"
216
233
  sub_button:
217
234
  -
218
- -
219
- type: "click"
220
- name: "预订半夜餐"
221
- key: "BOOK_NIGHT_SNACK"
222
- sub_button:
223
- -
224
235
  -
225
236
  name: "查询"
226
237
  sub_button:
@@ -343,6 +354,11 @@ class WechatsController < ApplicationController
343
354
  request.reply.text "收到来自#{request[:FromUserName]} 的EventKey 为 #{key} 的事件"
344
355
  end
345
356
 
357
+ # 当收到EventKey 为二维码扫描结果事件时
358
+ on :event, with: 'BINDING_QR_CODE' do |request, scan_type, scan_result|
359
+ request.reply.text "User #{request[:FromUserName]} ScanType #{scan_type} ScanResult #{scan_result}"
360
+ end
361
+
346
362
  # 处理图片信息
347
363
  on :image do |request|
348
364
  request.reply.image(request[:MediaId]) #直接将图片返回给用户
@@ -406,7 +422,7 @@ end
406
422
  ```ruby
407
423
  class WechatsController < ApplicationController
408
424
  # 当无任何responder处理用户信息时,转发至客服处理。
409
- on :fallback, respond: nil do |message|
425
+ on :fallback do |message|
410
426
  message.reply.transfer_customer_service
411
427
  end
412
428
  end
data/Rakefile CHANGED
@@ -20,10 +20,8 @@ RDoc::Task.new(:rdoc) do |rdoc|
20
20
  rdoc.rdoc_files.include('lib/**/*.rb')
21
21
  end
22
22
 
23
-
24
23
  require File.join('bundler', 'gem_tasks')
25
24
  require File.join('rspec', 'core', 'rake_task')
26
25
  RSpec::Core::RakeTask.new(:spec)
27
26
 
28
-
29
- task :default => :spec
27
+ task default: :spec
data/bin/wechat CHANGED
@@ -23,11 +23,12 @@ class App < Thor
23
23
  corpsecret = config['corpsecret']
24
24
  token_file = options[:toke_file] || config['access_token'] || '/var/tmp/wechat_access_token'
25
25
  agentid = config['agentid']
26
+ skip_verify_ssl = config['skip_verify_ssl']
26
27
 
27
28
  if appid.present? && secret.present? && token_file.present?
28
- Wechat::Api.new(appid, secret, token_file)
29
+ Wechat::Api.new(appid, secret, token_file, skip_verify_ssl)
29
30
  elsif corpid.present? && corpsecret.present? && token_file.present?
30
- Wechat::CorpApi.new(corpid, corpsecret, token_file, agentid)
31
+ Wechat::CorpApi.new(corpid, corpsecret, token_file, agentid, skip_verify_ssl)
31
32
  else
32
33
  puts <<-HELP
33
34
  Need create ~/.wechat.yml with wechat appid and secret
@@ -62,6 +63,11 @@ HELP
62
63
  package_name 'Wechat'
63
64
  option :toke_file, aliases: '-t', desc: 'File to store access token'
64
65
 
66
+ desc 'callbackip', '获取微信服务器IP地址'
67
+ def callbackip
68
+ puts Helper.with(options).callbackip
69
+ end
70
+
65
71
  desc 'groups', '所有用户分组列表'
66
72
  def groups
67
73
  puts Helper.with(options).groups
@@ -82,16 +88,31 @@ HELP
82
88
  puts Helper.with(options).group_delete(groupid)
83
89
  end
84
90
 
91
+ desc 'department [DEPARTMENT_ID]', '获取部门列表'
92
+ def department(departmentid)
93
+ puts Helper.with(options).department(departmentid)
94
+ end
95
+
85
96
  desc 'users', '关注者列表'
86
97
  def users
87
98
  puts Helper.with(options).users
88
99
  end
89
100
 
90
- desc 'user [OPEN_ID]', '查找关注者'
101
+ desc 'user [OPEN_ID]', '获取用户基本信息'
91
102
  def user(open_id)
92
103
  puts Helper.with(options).user(open_id)
93
104
  end
94
105
 
106
+ desc 'invite_user [USER_ID]', '邀请成员关注'
107
+ def invite_user(userid)
108
+ puts Helper.with(options).invite_user(userid)
109
+ end
110
+
111
+ desc 'user_delete [USER_ID]', '删除成员'
112
+ def user_delete(userid)
113
+ puts Helper.with(options).user_delete(userid)
114
+ end
115
+
95
116
  desc 'user_group [OPEN_ID]', '查询用户所在分组'
96
117
  def user_group(openid)
97
118
  puts Helper.with(options).user_group(openid)
@@ -131,9 +152,22 @@ HELP
131
152
  puts Helper.with(options).media_create(type, file)
132
153
  end
133
154
 
155
+ desc 'material [MEDIA_ID, PATH]', '永久媒体下载'
156
+ def material(media_id, path)
157
+ tmp_file = Helper.with(options).material(media_id)
158
+ FileUtils.mv(tmp_file.path, path)
159
+ puts 'File downloaded'
160
+ end
161
+
162
+ desc 'material_add [MEDIA_TYPE, PATH]', '永久媒体上传'
163
+ def material_add(type, path)
164
+ file = File.new(path)
165
+ puts Helper.with(options).material_add(type, file)
166
+ end
167
+
134
168
  desc 'message_send [OPENID, TEXT_MESSAGE]', '发送文字消息(仅企业号)'
135
169
  def message_send(openid, text_message)
136
- puts Helper.with(options).message_send Wechat::Message.to(openid).text(text_message)
170
+ puts Helper.with(options).message_send openid, text_message
137
171
  end
138
172
 
139
173
  desc 'custom_text [OPENID, TEXT_MESSAGE]', '发送文字客服消息'
@@ -8,19 +8,18 @@ module Wechat
8
8
  included do
9
9
  skip_before_filter :verify_authenticity_token
10
10
  before_filter :verify_signature, only: [:show, :create]
11
- #delegate :wechat, to: :class
12
11
  end
13
12
 
14
13
  module ClassMethods
15
- attr_accessor :wechat, :token, :corpid, :agentid, :encrypt_mode, :encoding_aes_key
14
+ attr_accessor :wechat, :token, :corpid, :agentid, :encrypt_mode, :skip_verify_ssl, :encoding_aes_key
16
15
 
17
16
  def on(message_type, with: nil, respond: nil, &block)
18
- raise 'Unknow message type' unless message_type.in? [:text, :image, :voice, :video, :location, :link, :event, :fallback]
17
+ fail 'Unknow message type' unless message_type.in? [:text, :image, :voice, :video, :location, :link, :event, :fallback]
19
18
  config = respond.nil? ? {} : { respond: respond }
20
19
  config.merge!(proc: block) if block_given?
21
20
 
22
21
  if with.present? && !message_type.in?([:text, :event])
23
- raise 'Only text and event message can take :with parameters'
22
+ fail 'Only text and event message can take :with parameters'
24
23
  else
25
24
  config.merge!(with: with) if with.present?
26
25
  end
@@ -43,8 +42,12 @@ module Wechat
43
42
  yield(* match_responders(responders, message[:Content]))
44
43
 
45
44
  when :event
46
- if message[:Event] == 'click'
45
+ if 'click' == message[:Event]
47
46
  yield(* match_responders(responders, message[:EventKey]))
47
+ elsif %w(scancode_push scancode_waitmsg).include? message[:Event]
48
+ yield(* match_responders(responders, event_key: message[:EventKey],
49
+ scan_type: message[:ScanCodeInfo][:ScanType],
50
+ scan_result: message[:ScanCodeInfo][:ScanResult]))
48
51
  else
49
52
  yield(* match_responders(responders, message[:Event]))
50
53
  end
@@ -66,6 +69,8 @@ module Wechat
66
69
 
67
70
  if condition.is_a? Regexp
68
71
  memo[:scoped] ||= [responder] + $LAST_MATCH_INFO.captures if value =~ condition
72
+ elsif value.is_a? Hash
73
+ memo[:scoped] ||= [responder, value[:scan_type], value[:scan_result]] if value[:event_key] == condition
69
74
  else
70
75
  memo[:scoped] ||= [responder, value] if value == condition
71
76
  end
@@ -85,20 +90,7 @@ module Wechat
85
90
 
86
91
  def create
87
92
  request = Wechat::Message.from_hash(post_xml)
88
- response = self.class.responder_for(request) do |responder, *args|
89
- responder ||= self.class.responders(:fallback).first
90
-
91
- next if responder.nil?
92
- case
93
- when responder[:respond]
94
- request.reply.text responder[:respond]
95
- when responder[:proc]
96
- define_singleton_method :process, responder[:proc]
97
- send(:process, *args.unshift(request))
98
- else
99
- next
100
- end
101
- end
93
+ response = run_responder(request)
102
94
 
103
95
  if response.respond_to? :to_xml
104
96
  render xml: process_response(response)
@@ -110,33 +102,32 @@ module Wechat
110
102
  private
111
103
 
112
104
  def verify_signature
105
+ signature = params[:signature] || params[:msg_signature]
106
+
107
+ render text: 'Forbidden', status: 403 if signature != Digest::SHA1.hexdigest(content_to_verify)
108
+ end
109
+
110
+ def content_to_verify
113
111
  array = [self.class.token, params[:timestamp], params[:nonce]]
114
- signature = params[:signature]
115
112
 
116
113
  # 默认使用明文方式验证, 企业号验证加密签名
117
114
  if params[:signature].blank? && params[:msg_signature]
118
- signature = params[:msg_signature]
119
- if params[:echostr]
115
+ if params[:echostr].present?
120
116
  array << params[:echostr]
121
117
  else
122
- array << request_content['xml']['Encrypt']
118
+ array << request_encrypt_content
123
119
  end
124
120
  end
125
121
 
126
- str = array.compact.collect(&:to_s).sort.join
127
- render text: 'Forbidden', status: 403 if signature != Digest::SHA1.hexdigest(str)
122
+ array.compact.collect(&:to_s).sort.join
128
123
  end
129
124
 
130
125
  def post_xml
131
126
  data = request_content
132
127
 
133
- # 如果是加密模式解密
134
- if self.class.encrypt_mode || self.class.corpid.present?
135
- encrypt_msg = data['xml']['Encrypt']
136
- if encrypt_msg.present?
137
- content, @app_id = unpack(decrypt(Base64.decode64(encrypt_msg), self.class.encoding_aes_key))
138
- data = Hash.from_xml(content)
139
- end
128
+ if self.class.encrypt_mode && request_encrypt_content.present?
129
+ content, @app_id = unpack(decrypt(Base64.decode64(request_encrypt_content), self.class.encoding_aes_key))
130
+ data = Hash.from_xml(content)
140
131
  end
141
132
 
142
133
  HashWithIndifferentAccess.new_from_hash_copying_default(data.fetch('xml', {})).tap do |msg|
@@ -144,16 +135,29 @@ module Wechat
144
135
  end
145
136
  end
146
137
 
138
+ def run_responder(request)
139
+ self.class.responder_for(request) do |responder, *args|
140
+ responder ||= self.class.responders(:fallback).first
141
+
142
+ next if responder.nil?
143
+ case
144
+ when responder[:respond]
145
+ request.reply.text responder[:respond]
146
+ when responder[:proc]
147
+ define_singleton_method :process, responder[:proc]
148
+ send(:process, *args.unshift(request))
149
+ else
150
+ next
151
+ end
152
+ end
153
+ end
154
+
147
155
  def process_response(response)
148
156
  msg = response.to_xml
149
157
 
150
- # 返回加密消息
151
- if self.class.encrypt_mode || self.class.corpid.present?
152
- data = request_content
153
- if data['xml']['Encrypt']
154
- encrypt = Base64.strict_encode64 encrypt(pack(msg, @app_id), self.class.encoding_aes_key)
155
- msg = gen_msg(encrypt, params[:timestamp], params[:nonce])
156
- end
158
+ if self.class.encrypt_mode && request_encrypt_content.present?
159
+ encrypt = Base64.strict_encode64(encrypt(pack(msg, @app_id), self.class.encoding_aes_key))
160
+ msg = gen_msg(encrypt, params[:timestamp], params[:nonce])
157
161
  end
158
162
 
159
163
  msg
@@ -169,6 +173,10 @@ module Wechat
169
173
  }.to_xml(root: 'xml', children: 'item', skip_instruct: true, skip_types: true)
170
174
  end
171
175
 
176
+ def request_encrypt_content
177
+ request_content['xml']['Encrypt']
178
+ end
179
+
172
180
  def request_content
173
181
  params[:xml].nil? ? Hash.from_xml(request.raw_post) : { 'xml' => params[:xml] }
174
182
  end
@@ -0,0 +1,36 @@
1
+ module ActionController
2
+ module WechatResponder
3
+ def wechat_responder(opts = {})
4
+ include Wechat::Responder
5
+
6
+ self.corpid = opts[:corpid] || Wechat.config.corpid
7
+ self.agentid = opts[:agentid] || Wechat.config.agentid
8
+ self.encrypt_mode = opts[:encrypt_mode] || Wechat.config.encrypt_mode || corpid.present?
9
+ self.skip_verify_ssl = opts[:skip_verify_ssl]
10
+ self.token = opts[:token] || Wechat.config.token
11
+ self.encoding_aes_key = opts[:encoding_aes_key] || Wechat.config.encoding_aes_key
12
+
13
+ if opts.empty?
14
+ self.wechat = Wechat.api
15
+ else
16
+ if corpid.present?
17
+ self.wechat = Wechat::CorpApi.new(corpid, opts[:corpsecret], opts[:access_token], agentid, skip_verify_ssl)
18
+ else
19
+ self.wechat = Wechat::Api.new(opts[:appid], opts[:secret], opts[:access_token], skip_verify_ssl, opts[:jsapi_ticket])
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ if defined? Base
26
+ class << Base
27
+ include WechatResponder
28
+ end
29
+ end
30
+
31
+ if defined? API
32
+ class << API
33
+ include WechatResponder
34
+ end
35
+ end
36
+ end
@@ -1,9 +1,10 @@
1
1
  require 'wechat/api'
2
2
  require 'wechat/corp_api'
3
+ require 'action_controller/wechat_responder'
3
4
 
4
5
  module Wechat
5
6
  autoload :Message, 'wechat/message'
6
- autoload :Responder, 'wechat/responder'
7
+ autoload :Responder, 'action_controller/responder'
7
8
  autoload :Cipher, 'wechat/cipher'
8
9
 
9
10
  class AccessTokenExpiredError < StandardError; end
@@ -32,6 +33,7 @@ module Wechat
32
33
  token: ENV['WECHAT_TOKEN'],
33
34
  access_token: ENV['WECHAT_ACCESS_TOKEN'],
34
35
  encrypt_mode: ENV['WECHAT_ENCRYPT_MODE'],
36
+ skip_verify_ssl: ENV['WECHAT_SKIP_VERIFY_SSL'],
35
37
  encoding_aes_key: ENV['WECHAT_ENCODING_AES_KEY'] }
36
38
  config.symbolize_keys!
37
39
  config[:access_token] ||= Rails.root.join('tmp/access_token').to_s
@@ -42,63 +44,9 @@ module Wechat
42
44
 
43
45
  def self.api
44
46
  if config.corpid.present?
45
- @api ||= Wechat::CorpApi.new(config.corpid, config.corpsecret, config.access_token, config.agentid)
47
+ @api ||= CorpApi.new(config.corpid, config.corpsecret, config.access_token, config.agentid, config.skip_verify_ssl)
46
48
  else
47
- @api ||= Wechat::Api.new(config.appid, config.secret, config.access_token, config.jsapi_ticket)
48
- end
49
- end
50
- end
51
-
52
- if defined? ActionController::Base
53
- class ActionController::Base
54
- def self.wechat_responder(opts = {})
55
- send(:include, Wechat::Responder)
56
- if opts.empty?
57
- self.corpid = Wechat.config.corpid
58
- self.wechat = Wechat.api
59
- self.agentid = Wechat.config.agentid
60
- self.token = Wechat.config.token
61
- self.encrypt_mode = Wechat.config.encrypt_mode
62
- self.encoding_aes_key = Wechat.config.encoding_aes_key
63
- else
64
- self.corpid = opts[:corpid]
65
- if corpid.present?
66
- self.wechat = Wechat::CorpApi.new(opts[:corpid], opts[:corpsecret], opts[:access_token], opts[:agentid])
67
- else
68
- self.wechat = Wechat::Api.new(opts[:appid], opts[:secret], opts[:access_token], opts[:jsapi_ticket])
69
- end
70
- self.agentid = opts[:agentid]
71
- self.token = opts[:token]
72
- self.encrypt_mode = opts[:encrypt_mode]
73
- self.encoding_aes_key = opts[:encoding_aes_key]
74
- end
75
- end
76
- end
77
- end
78
-
79
- if defined? ActionController::API
80
- class ActionController::API
81
- def self.wechat_responder(opts = {})
82
- send(:include, Wechat::Responder)
83
- if opts.empty?
84
- self.corpid = Wechat.config.corpid
85
- self.wechat = Wechat.api
86
- self.agentid = Wechat.config.agentid
87
- self.token = Wechat.config.token
88
- self.encrypt_mode = Wechat.config.encrypt_mode
89
- self.encoding_aes_key = Wechat.config.encoding_aes_key
90
- else
91
- self.corpid = opts[:corpid]
92
- if corpid.present?
93
- self.wechat = Wechat::CorpApi.new(opts[:corpid], opts[:corpsecret], opts[:access_token], opts[:agentid])
94
- else
95
- self.wechat = Wechat::Api.new(opts[:appid], opts[:secret], opts[:access_token], opts[:jsapi_ticket])
96
- end
97
- self.agentid = opts[:agentid]
98
- self.token = opts[:token]
99
- self.encrypt_mode = opts[:encrypt_mode]
100
- self.encoding_aes_key = opts[:encoding_aes_key]
101
- end
49
+ @api ||= Api.new(config.appid, config.secret, config.access_token, config.jsapi_ticket, config.skip_verify_ssl)
102
50
  end
103
51
  end
104
52
  end
@@ -11,12 +11,10 @@ module Wechat
11
11
 
12
12
  def token
13
13
  begin
14
- @token_data ||= JSON.parse(File.read(token_file, open_args: File::LOCK_SH))
14
+ @token_data ||= JSON.parse(File.read(token_file))
15
15
  created_at = token_data['created_at'].to_i
16
16
  expires_in = token_data['expires_in'].to_i
17
- if Time.now.to_i - created_at >= expires_in - 3 * 60
18
- raise 'token_data may be expired'
19
- end
17
+ fail 'token_data may be expired' if Time.now.to_i - created_at >= expires_in - 3 * 60
20
18
  rescue
21
19
  refresh
22
20
  end
@@ -34,7 +32,7 @@ module Wechat
34
32
 
35
33
  def valid_token(token_data)
36
34
  access_token = token_data['access_token']
37
- raise "Response didn't have access_token" if access_token.blank?
35
+ fail "Response didn't have access_token" if access_token.blank?
38
36
  access_token
39
37
  end
40
38
  end
@@ -1,109 +1,102 @@
1
+ require 'wechat/api_base'
1
2
  require 'wechat/client'
2
3
  require 'wechat/access_token'
3
4
  require 'wechat/jsapi_ticket'
4
5
 
5
- class Wechat::Api
6
- attr_reader :access_token, :client, :jsapi_ticket
7
-
8
- API_BASE = 'https://api.weixin.qq.com/cgi-bin/'
9
- FILE_BASE = 'http://file.api.weixin.qq.com/cgi-bin/'
10
- OAUTH2_BASE = 'https://api.weixin.qq.com/sns/oauth2/'
11
-
12
- def initialize(appid, secret, token_file, jsapi_ticket_file = '/var/tmp/wechat_jsapi_ticket')
13
- @client = Wechat::Client.new(API_BASE)
14
- @access_token = Wechat::AccessToken.new(@client, appid, secret, token_file)
15
- @jsapi_ticket = Wechat::JsapiTicket.new(@client, @access_token, jsapi_ticket_file)
16
- end
17
-
18
- def groups
19
- get('groups/get')
20
- end
21
-
22
- def group_create(group_name)
23
- post 'groups/create', JSON.generate(group: { name: group_name })
24
- end
25
-
26
- def group_update(groupid, new_group_name)
27
- post 'groups/update', JSON.generate(group: { id: groupid, name: new_group_name })
28
- end
29
-
30
- def group_delete(groupid)
31
- post 'groups/delete', JSON.generate(group: { id: groupid })
32
- end
33
-
34
- def users(nextid = nil)
35
- params = { params: { next_openid: nextid } } if nextid.present?
36
- get('user/get', params || {})
37
- end
38
-
39
- def user(openid)
40
- get('user/info', params: { openid: openid })
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 menu
52
- get('menu/get')
53
- end
54
-
55
- def menu_delete
56
- get('menu/delete')
57
- end
58
-
59
- def menu_create(menu)
60
- # 微信不接受7bit escaped json(eg \uxxxx), 中文必须UTF-8编码, 这可能是个安全漏洞
61
- post('menu/create', JSON.generate(menu))
62
- end
63
-
64
- def media(media_id)
65
- get 'media/get', params: { media_id: media_id }, base: FILE_BASE, as: :file
66
- end
67
-
68
- def media_create(type, file)
69
- post 'media/upload', { upload: { media: file } }, params: { type: type }, base: FILE_BASE
70
- end
71
-
72
- def custom_message_send(message)
73
- post 'message/custom/send', message.to_json, content_type: :json
74
- end
75
-
76
- def template_message_send(message)
77
- post 'message/template/send', message.to_json, content_type: :json
78
- end
79
-
80
- # http://mp.weixin.qq.com/wiki/17/c0f37d5704f0b64713d5d2c37b468d75.html
81
- # 第二步:通过code换取网页授权access_token
82
- def web_access_token(code)
83
- params = {
84
- appid: access_token.appid,
85
- secret: access_token.secret,
86
- code: code,
87
- grant_type: 'authorization_code'
88
- }
89
- get 'access_token', params: params, base: OAUTH2_BASE
90
- end
91
-
92
- protected
93
-
94
- def get(path, headers = {})
95
- with_access_token(headers[:params]) { |params| client.get path, headers.merge(params: params) }
96
- end
97
-
98
- def post(path, payload, headers = {})
99
- with_access_token(headers[:params]) { |params| client.post path, payload, headers.merge(params: params) }
100
- end
101
-
102
- def with_access_token(params = {}, tries = 2)
103
- params ||= {}
104
- yield(params.merge(access_token: access_token.token))
105
- rescue Wechat::AccessTokenExpiredError
106
- access_token.refresh
107
- retry unless (tries -= 1).zero?
6
+ module Wechat
7
+ class Api < ApiBase
8
+ attr_reader :jsapi_ticket
9
+
10
+ API_BASE = 'https://api.weixin.qq.com/cgi-bin/'
11
+ FILE_BASE = 'http://file.api.weixin.qq.com/cgi-bin/'
12
+ OAUTH2_BASE = 'https://api.weixin.qq.com/sns/oauth2/'
13
+
14
+ def initialize(appid, secret, token_file, skip_verify_ssl, jsapi_ticket_file = '/var/tmp/wechat_jsapi_ticket')
15
+ @client = Client.new(API_BASE, skip_verify_ssl)
16
+ @access_token = AccessToken.new(@client, appid, secret, token_file)
17
+ @jsapi_ticket = JsapiTicket.new(@client, @access_token, jsapi_ticket_file)
18
+ end
19
+
20
+ def groups
21
+ get('groups/get')
22
+ end
23
+
24
+ def group_create(group_name)
25
+ post 'groups/create', JSON.generate(group: { name: group_name })
26
+ end
27
+
28
+ def group_update(groupid, new_group_name)
29
+ post 'groups/update', JSON.generate(group: { id: groupid, name: new_group_name })
30
+ end
31
+
32
+ def group_delete(groupid)
33
+ post 'groups/delete', JSON.generate(group: { id: groupid })
34
+ end
35
+
36
+ def users(nextid = nil)
37
+ params = { params: { next_openid: nextid } } if nextid.present?
38
+ get('user/get', params || {})
39
+ end
40
+
41
+ def user(openid)
42
+ get('user/info', params: { openid: openid })
43
+ end
44
+
45
+ def user_group(openid)
46
+ post 'groups/getid', JSON.generate(openid: openid)
47
+ end
48
+
49
+ def user_change_group(openid, to_groupid)
50
+ post 'groups/members/update', JSON.generate(openid: openid, to_groupid: to_groupid)
51
+ end
52
+
53
+ def menu
54
+ get('menu/get')
55
+ end
56
+
57
+ def menu_delete
58
+ get('menu/delete')
59
+ end
60
+
61
+ def menu_create(menu)
62
+ # 微信不接受7bit escaped json(eg \uxxxx), 中文必须UTF-8编码, 这可能是个安全漏洞
63
+ post('menu/create', JSON.generate(menu))
64
+ end
65
+
66
+ def media(media_id)
67
+ get 'media/get', params: { media_id: media_id }, base: FILE_BASE, as: :file
68
+ end
69
+
70
+ def media_create(type, file)
71
+ post 'media/upload', { upload: { media: file } }, params: { type: type }, base: FILE_BASE
72
+ end
73
+
74
+ def material(media_id)
75
+ get 'material/get', params: { media_id: media_id }, base: FILE_BASE, as: :file
76
+ end
77
+
78
+ def material_add(type, file)
79
+ post 'material/add_material', { upload: { media: file } }, params: { type: type }, base: FILE_BASE
80
+ end
81
+
82
+ def custom_message_send(message)
83
+ post 'message/custom/send', message.to_json, content_type: :json
84
+ end
85
+
86
+ def template_message_send(message)
87
+ post 'message/template/send', message.to_json, content_type: :json
88
+ end
89
+
90
+ # http://mp.weixin.qq.com/wiki/17/c0f37d5704f0b64713d5d2c37b468d75.html
91
+ # 第二步:通过code换取网页授权access_token
92
+ def web_access_token(code)
93
+ params = {
94
+ appid: access_token.appid,
95
+ secret: access_token.secret,
96
+ code: code,
97
+ grant_type: 'authorization_code'
98
+ }
99
+ get 'access_token', params: params, base: OAUTH2_BASE
100
+ end
108
101
  end
109
102
  end
@@ -0,0 +1,31 @@
1
+ module Wechat
2
+ class ApiBase
3
+ attr_reader :access_token, :client
4
+
5
+ def callbackip
6
+ get('getcallbackip')
7
+ end
8
+
9
+ protected
10
+
11
+ def get(path, headers = {})
12
+ with_access_token(headers[:params]) do |params|
13
+ client.get path, headers.merge(params: params)
14
+ end
15
+ end
16
+
17
+ def post(path, payload, headers = {})
18
+ with_access_token(headers[:params]) do |params|
19
+ client.post path, payload, headers.merge(params: params)
20
+ end
21
+ end
22
+
23
+ def with_access_token(params = {}, tries = 2)
24
+ params ||= {}
25
+ yield(params.merge(access_token: access_token.token))
26
+ rescue AccessTokenExpiredError
27
+ access_token.refresh
28
+ retry unless (tries -= 1).zero?
29
+ end
30
+ end
31
+ end
@@ -2,13 +2,14 @@ require 'rest_client'
2
2
 
3
3
  module Wechat
4
4
  class Client
5
- attr_reader :base
5
+ attr_reader :base, :verify_ssl
6
6
 
7
- def initialize(base)
7
+ def initialize(base, skip_verify_ssl)
8
8
  @base = base
9
+ @verify_ssl = !skip_verify_ssl
9
10
  end
10
11
 
11
- def get(path, header = {}, verify_ssl = true)
12
+ def get(path, header = {})
12
13
  request(path, header) do |url, header|
13
14
  if verify_ssl
14
15
  RestClient.get(url, header)
@@ -18,7 +19,7 @@ module Wechat
18
19
  end
19
20
  end
20
21
 
21
- def post(path, payload, header = {}, verify_ssl = true)
22
+ def post(path, payload, header = {})
22
23
  request(path, header) do |url, header|
23
24
  if verify_ssl
24
25
  RestClient.post(url, payload, header)
@@ -29,22 +30,22 @@ module Wechat
29
30
  end
30
31
 
31
32
  def request(path, header = {}, &block)
32
- url = "#{header.delete(:base) || self.base}#{path}"
33
+ url = "#{header.delete(:base) || base}#{path}"
33
34
  as = header.delete(:as)
34
- header.merge!(:accept => :json)
35
+ header.merge!(accept: :json)
35
36
  response = yield(url, header)
36
37
 
37
- raise "Request not OK, response code #{response.code}" if response.code != 200
38
+ fail "Request not OK, response code #{response.code}" if response.code != 200
38
39
  parse_response(response, as || :json) do |parse_as, data|
39
40
  break data unless parse_as == :json && data['errcode'].present?
40
41
 
41
42
  case data['errcode']
42
43
  when 0 # for request didn't expect results
43
44
  data
44
- when 42_001, 40_014 # 42001: access_token超时, 40014:不合法的access_token
45
- raise AccessTokenExpiredError
45
+ when 42001, 40014 # 42001: access_token超时, 40014:不合法的access_token
46
+ fail AccessTokenExpiredError
46
47
  else
47
- raise ResponseError.new(data['errcode'], data['errmsg'])
48
+ fail ResponseError.new(data['errcode'], data['errmsg'])
48
49
  end
49
50
  end
50
51
  end
@@ -54,9 +55,9 @@ module Wechat
54
55
  def parse_response(response, as)
55
56
  content_type = response.headers[:content_type]
56
57
  parse_as = {
57
- /^application\/json/ => :json,
58
- /^image\/.*/ => :file
59
- }.inject([]){ |memo, match| memo << match[1] if content_type =~ match[0]; memo }.first || as || :text
58
+ %r{^application\/json} => :json,
59
+ %r{^image\/.*} => :file
60
+ }.each_with_object([]) { |match, memo| memo << match[1] if content_type =~ match[0] }.first || as || :text
60
61
 
61
62
  case parse_as
62
63
  when :file
@@ -68,7 +69,6 @@ module Wechat
68
69
 
69
70
  when :json
70
71
  data = JSON.parse(response.body.gsub /[\u0000-\u001f]+/, '')
71
-
72
72
  else
73
73
  data = response.body
74
74
  end
@@ -1,66 +1,79 @@
1
+ require 'wechat/api_base'
1
2
  require 'wechat/client'
2
3
  require 'wechat/access_token'
3
4
 
4
- class Wechat::CorpAccessToken < Wechat::AccessToken
5
- def refresh
6
- data = client.get('gettoken', { params: { corpid: appid, corpsecret: secret }}, false)
7
- data.merge!(created_at: Time.now.to_i)
8
- File.open(token_file, 'w') { |f| f.write(data.to_json) } if valid_token(data)
9
- @token_data = data
5
+ module Wechat
6
+ class CorpAccessToken < AccessToken
7
+ def refresh
8
+ data = client.get('gettoken', params: { corpid: appid, corpsecret: secret })
9
+ data.merge!(created_at: Time.now.to_i)
10
+ File.write(token_file, data.to_json) if valid_token(data)
11
+ @token_data = data
12
+ end
10
13
  end
11
- end
12
14
 
13
- class Wechat::CorpApi
14
- attr_reader :access_token, :client, :agentid
15
+ class CorpApi < ApiBase
16
+ attr_reader :agentid
15
17
 
16
- API_BASE = 'https://qyapi.weixin.qq.com/cgi-bin/'
18
+ API_BASE = 'https://qyapi.weixin.qq.com/cgi-bin/'
17
19
 
18
- def initialize(appid, secret, token_file, agentid)
19
- @client = Wechat::Client.new(API_BASE)
20
- @access_token = Wechat::CorpAccessToken.new(@client, appid, secret, token_file)
21
- @agentid = agentid
22
- end
20
+ def initialize(appid, secret, token_file, agentid, skip_verify_ssl)
21
+ @client = Client.new(API_BASE, skip_verify_ssl)
22
+ @access_token = CorpAccessToken.new(@client, appid, secret, token_file)
23
+ @agentid = agentid
24
+ end
23
25
 
24
- def user(userid)
25
- get('user/get', params: { userid: userid })
26
- end
26
+ def user(userid)
27
+ get('user/get', params: { userid: userid })
28
+ end
27
29
 
28
- def menu
29
- get('menu/get', params: { agentid: agentid })
30
- end
30
+ def invite_user(userid)
31
+ post 'invite/send', JSON.generate(userid: userid)
32
+ end
31
33
 
32
- def menu_delete
33
- get('menu/delete', params: { agentid: agentid })
34
- end
34
+ def user_auth_success(userid)
35
+ get('user/authsucc', params: { userid: userid })
36
+ end
35
37
 
36
- def menu_create(menu)
37
- # 微信不接受7bit escaped json(eg \uxxxx), 中文必须UTF-8编码, 这可能是个安全漏洞
38
- post 'menu/create', JSON.generate(menu), { params: { agentid: agentid } }
39
- end
38
+ def user_delete(userid)
39
+ get('user/delete', params: { userid: userid })
40
+ end
40
41
 
41
- def message_send(message)
42
- post 'message/send', message.agent_id(agentid).to_json, content_type: :json
43
- end
42
+ def department(departmentid = 1)
43
+ get('department/list', params: { id: departmentid })
44
+ end
44
45
 
45
- protected
46
+ def menu
47
+ get('menu/get', params: { agentid: agentid })
48
+ end
46
49
 
47
- def get(path, headers = {})
48
- with_access_token(headers[:params]) do |params|
49
- client.get path, headers.merge(params: params), false
50
+ def menu_delete
51
+ get('menu/delete', params: { agentid: agentid })
50
52
  end
51
- end
52
53
 
53
- def post(path, payload, headers = {})
54
- with_access_token(headers[:params]) do |params|
55
- client.post path, payload, headers.merge(params: params), false
54
+ def menu_create(menu)
55
+ # 微信不接受7bit escaped json(eg \uxxxx), 中文必须UTF-8编码, 这可能是个安全漏洞
56
+ post 'menu/create', JSON.generate(menu), params: { agentid: agentid }
56
57
  end
57
- end
58
58
 
59
- def with_access_token(params = {}, tries = 2)
60
- params ||= {}
61
- yield(params.merge(access_token: access_token.token))
62
- rescue Wechat::AccessTokenExpiredError
63
- access_token.refresh
64
- retry unless (tries -= 1).zero?
59
+ def media(media_id)
60
+ get 'media/get', params: { media_id: media_id }, as: :file
61
+ end
62
+
63
+ def media_create(type, file)
64
+ post 'media/upload', { upload: { media: file } }, params: { type: type }
65
+ end
66
+
67
+ def material(media_id)
68
+ get 'material/get', params: { media_id: media_id, agentid: agentid }, as: :file
69
+ end
70
+
71
+ def material_add(type, file)
72
+ post 'material/add_material', { upload: { media: file } }, params: { type: type, agentid: agentid }
73
+ end
74
+
75
+ def message_send(openid, message)
76
+ post 'message/send', Message.to(openid).text(message).agent_id(agentid).to_json, content_type: :json
77
+ end
65
78
  end
66
79
  end
@@ -23,7 +23,7 @@ module Wechat
23
23
  created_at = jsapi_ticket_data['created_at'].to_i
24
24
  expires_in = jsapi_ticket_data['expires_in'].to_i
25
25
  if Time.now.to_i - created_at >= expires_in - 3 * 60
26
- raise 'jsapi_ticket may be expired'
26
+ fail 'jsapi_ticket may be expired'
27
27
  end
28
28
  rescue
29
29
  refresh
@@ -67,7 +67,7 @@ module Wechat
67
67
 
68
68
  def valid_ticket(jsapi_ticket_data)
69
69
  ticket = jsapi_ticket_data['ticket'] || jsapi_ticket_data[:ticket]
70
- raise "Response didn't have ticket" if ticket.blank?
70
+ fail "Response didn't have ticket" if ticket.blank?
71
71
  ticket
72
72
  end
73
73
  end
@@ -53,7 +53,7 @@ module Wechat
53
53
  results[value[0].to_s.underscore.to_sym] = value[1]
54
54
  end
55
55
  else
56
- raise "Don't know how to parse message as #{type}"
56
+ fail "Don't know how to parse message as #{type}"
57
57
  end
58
58
  end
59
59
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wechat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Skinnyworm
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-08-29 00:00:00.000000000 Z
12
+ date: 2015-09-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -79,15 +79,17 @@ files:
79
79
  - README.md
80
80
  - Rakefile
81
81
  - bin/wechat
82
+ - lib/action_controller/responder.rb
83
+ - lib/action_controller/wechat_responder.rb
82
84
  - lib/wechat.rb
83
85
  - lib/wechat/access_token.rb
84
86
  - lib/wechat/api.rb
87
+ - lib/wechat/api_base.rb
85
88
  - lib/wechat/cipher.rb
86
89
  - lib/wechat/client.rb
87
90
  - lib/wechat/corp_api.rb
88
91
  - lib/wechat/jsapi_ticket.rb
89
92
  - lib/wechat/message.rb
90
- - lib/wechat/responder.rb
91
93
  homepage: https://github.com/Eric-Guo/wechat
92
94
  licenses:
93
95
  - MIT