wechat 0.3.0 → 0.4.0

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 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