mp_weixin 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +22 -0
  3. data/.rspec +7 -0
  4. data/.travis.yml +20 -0
  5. data/CHANGELOG.md +17 -0
  6. data/Gemfile +5 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +308 -0
  9. data/Rakefile +31 -0
  10. data/lib/config/mp_weixin_error.yml +82 -0
  11. data/lib/mp_weixin.rb +59 -0
  12. data/lib/mp_weixin/access_token.rb +172 -0
  13. data/lib/mp_weixin/client.rb +199 -0
  14. data/lib/mp_weixin/config.rb +36 -0
  15. data/lib/mp_weixin/error.rb +27 -0
  16. data/lib/mp_weixin/interface/base.rb +43 -0
  17. data/lib/mp_weixin/interface/group.rb +92 -0
  18. data/lib/mp_weixin/interface/menu.rb +73 -0
  19. data/lib/mp_weixin/interface/message.rb +38 -0
  20. data/lib/mp_weixin/interface/promotion.rb +48 -0
  21. data/lib/mp_weixin/interface/user.rb +39 -0
  22. data/lib/mp_weixin/models/event.rb +123 -0
  23. data/lib/mp_weixin/models/message.rb +227 -0
  24. data/lib/mp_weixin/models/reply_message.rb +180 -0
  25. data/lib/mp_weixin/response.rb +93 -0
  26. data/lib/mp_weixin/response_rule.rb +46 -0
  27. data/lib/mp_weixin/server.rb +39 -0
  28. data/lib/mp_weixin/server_helper.rb +94 -0
  29. data/lib/mp_weixin/version.rb +3 -0
  30. data/lib/support/active_model.rb +3 -0
  31. data/lib/support/active_model/model.rb +99 -0
  32. data/mp_weixin.gemspec +44 -0
  33. data/spec/client_spec.rb +87 -0
  34. data/spec/mp_weixin/access_token_spec.rb +140 -0
  35. data/spec/mp_weixin/client_spec.rb +111 -0
  36. data/spec/mp_weixin/config_spec.rb +24 -0
  37. data/spec/mp_weixin/interface/base_spec.rb +16 -0
  38. data/spec/mp_weixin/interface/group_spec.rb +133 -0
  39. data/spec/mp_weixin/interface/menu_spec.rb +72 -0
  40. data/spec/mp_weixin/interface/message_spec.rb +36 -0
  41. data/spec/mp_weixin/interface/promotion_spec.rb +48 -0
  42. data/spec/mp_weixin/interface/user_spec.rb +76 -0
  43. data/spec/mp_weixin/models/event_spec.rb +94 -0
  44. data/spec/mp_weixin/models/message_spec.rb +300 -0
  45. data/spec/mp_weixin/models/reply_message_spec.rb +365 -0
  46. data/spec/mp_weixin/server_helper_spec.rb +165 -0
  47. data/spec/mp_weixin/server_spec.rb +56 -0
  48. data/spec/spec_helper.rb +51 -0
  49. data/spec/support/mp_weixin.rb +7 -0
  50. data/spec/support/rspec_mixin.rb +8 -0
  51. data/spec/support/weixin.yml +12 -0
  52. metadata +363 -0
@@ -0,0 +1,82 @@
1
+ mp_weixin_errors:
2
+ '-1': 系统繁忙
3
+ '0': 请求成功
4
+ '40001': 获取access_token时AppSecret错误,或者access_token无效
5
+ '40002': 不合法的凭证类型
6
+ '40003': 不合法的OpenID
7
+ '40004': 不合法的媒体文件类型
8
+ '40005': 不合法的文件类型
9
+ '40006': 不合法的文件大小
10
+ '40007': 不合法的媒体文件id
11
+ '40008': 不合法的消息类型
12
+ '40009': 不合法的图片文件大小
13
+ '40010': 不合法的语音文件大小
14
+ '40011': 不合法的视频文件大小
15
+ '40012': 不合法的缩略图文件大小
16
+ '40013': 不合法的APPID
17
+ '40014': 不合法的access_token
18
+ '40015': 不合法的菜单类型
19
+ '40016': 不合法的按钮个数
20
+ '40017': 不合法的按钮个数
21
+ '40018': 不合法的按钮名字长度
22
+ '40019': 不合法的按钮KEY长度
23
+ '40020': 不合法的按钮URL长度
24
+ '40021': 不合法的菜单版本号
25
+ '40022': 不合法的子菜单级数
26
+ '40023': 不合法的子菜单按钮个数
27
+ '40024': 不合法的子菜单按钮类型
28
+ '40025': 不合法的子菜单按钮名字长度
29
+ '40026': 不合法的子菜单按钮KEY长度
30
+ '40027': 不合法的子菜单按钮URL长度
31
+ '40028': 不合法的自定义菜单使用用户
32
+ '40029': 不合法的oauth_code
33
+ '40030': 不合法的refresh_token
34
+ '40031': 不合法的openid列表
35
+ '40032': 不合法的openid列表长度
36
+ '40033': 不合法的请求字符,不能包含\uxxxx格式的字符
37
+ '40035': 不合法的参数
38
+ '40038': 不合法的请求格式
39
+ '40039': 不合法的URL长度
40
+ '40050': 不合法的分组id
41
+ '40051': 分组名字不合法
42
+ '41001': 缺少access_token参数
43
+ '41002': 缺少appid参数
44
+ '41003': 缺少refresh_token参数
45
+ '41004': 缺少secret参数
46
+ '41005': 缺少多媒体文件数据
47
+ '41006': 缺少media_id参数
48
+ '41007': 缺少子菜单数据
49
+ '41009': 缺少openid
50
+ '42001': access_token超时
51
+ '42002': refresh_token超时
52
+ '42003': oauth_code超时
53
+ '43001': 需要GET请求
54
+ '43002': 需要POST请求
55
+ '43003': 需要HTTPS请求
56
+ '43004': 需要接收者关注
57
+ '43005': 需要好友关系
58
+ '44001': 多媒体文件为空
59
+ '44002': POST的数据包为空
60
+ '44003': 图文消息内容为空
61
+ '44004': 文本消息内容为空
62
+ '45001': 多媒体文件大小超过限制
63
+ '45002': 消息内容超过限制
64
+ '45003': 标题字段超过限制
65
+ '45004': 描述字段超过限制
66
+ '45005': 链接字段超过限制
67
+ '45006': 图片链接字段超过限制
68
+ '45007': 语音播放时间超过限制
69
+ '45008': 图文消息超过限制
70
+ '45009': 接口调用超过限制
71
+ '45010': 创建菜单个数超过限制
72
+ '45015': 回复时间超过限制
73
+ '45016': 系统分组,不允许修改
74
+ '45017': 分组名字过长
75
+ '45018': 分组数量超过上限
76
+ '46001': 不存在媒体数据
77
+ '46002': 不存在的菜单版本
78
+ '46003': 不存在的菜单数据
79
+ '46004': 不存在的用户
80
+ '47001': 解析JSON/XML内容错误
81
+ '48001': api功能未授权
82
+ '50001': 用户未授权该api
data/lib/mp_weixin.rb ADDED
@@ -0,0 +1,59 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler/setup'
5
+
6
+ if defined?(Bundler)
7
+ Bundler.require
8
+ end
9
+
10
+ require 'roxml'
11
+ require 'multi_xml'
12
+ require 'ostruct'
13
+
14
+ require 'faraday'
15
+ require 'active_model'
16
+ require 'active_support/all'
17
+
18
+ require 'support/active_model'
19
+
20
+ require "mp_weixin/version"
21
+ require "mp_weixin/config"
22
+ require "mp_weixin/error"
23
+
24
+ # require models
25
+
26
+ # require client
27
+
28
+ ## require client dependence
29
+ require "mp_weixin/response"
30
+ require "mp_weixin/access_token"
31
+
32
+ ## require interface
33
+ require "mp_weixin/interface/base"
34
+ require "mp_weixin/interface/message"
35
+ require "mp_weixin/interface/menu"
36
+ require "mp_weixin/interface/promotion"
37
+ require "mp_weixin/interface/group"
38
+ require "mp_weixin/interface/user"
39
+
40
+ require "mp_weixin/client"
41
+
42
+ # require server
43
+ require 'sinatra'
44
+ require 'digest/md5'
45
+ require 'rexml/document'
46
+
47
+ # include base class Message
48
+ # and some children class TextMessage, ImageMessage, LocationMessage,
49
+ # LinkMessage, VoiceMessage, VideoMessage
50
+ require 'mp_weixin/models/message'
51
+ require 'mp_weixin/models/event'
52
+ require 'mp_weixin/models/reply_message'
53
+ # require 'mp_weixin/models/location_message'
54
+
55
+ ## require helpers
56
+ require 'mp_weixin/server_helper'
57
+ require 'mp_weixin/response_rule'
58
+
59
+ require 'mp_weixin/server'
@@ -0,0 +1,172 @@
1
+ module MpWeixin
2
+ class AccessToken
3
+ attr_reader :client, :token, :expires_in, :expires_at, :params
4
+ attr_accessor :options, :refresh_token
5
+
6
+ class << self
7
+ # Initializes an AccessToken from a Hash
8
+ #
9
+ # @param [Client] the MpWeixin::Client instance
10
+ # @param [Hash] a hash of AccessToken property values
11
+ # @return [AccessToken] the initalized AccessToken
12
+ def from_hash(client, hash)
13
+ self.new(client, hash.delete('access_token') || hash.delete(:access_token), hash)
14
+ end
15
+
16
+ # Initializes an AccessToken from a key/value application/x-www-form-urlencoded string
17
+ #
18
+ # @param [Client] client the MpWeixin::Client instance
19
+ # @param [String] kvform the application/x-www-form-urlencoded string
20
+ # @return [AccessToken] the initalized AccessToken
21
+ def from_kvform(client, kvform)
22
+ from_hash(client, Rack::Utils.parse_query(kvform))
23
+ end
24
+ end
25
+
26
+ # Initalize an AccessToken
27
+ #
28
+ # @param [Client] client the MpWeixin::Client instance
29
+ # @param [String] token the Access Token value
30
+ # @param [Hash] opts the options to create the Access Token with
31
+ # @option opts [String] :refresh_token (nil) the refresh_token value
32
+ # @option opts [FixNum, String] :expires_in (nil) the number of seconds in which the AccessToken will expire
33
+ # @option opts [FixNum, String] :expires_at (nil) the epoch time in seconds in which AccessToken will expire
34
+ # @option opts [Symbol] :mode (:header) the transmission mode of the Access Token parameter value
35
+ # one of :header, :body or :query
36
+ # @option opts [String] :header_format ('Bearer %s') the string format to use for the Authorization header
37
+ # @option opts [String] :param_name ('access_token') the parameter name to use for transmission of the
38
+ # Access Token value in :body or :query transmission mode
39
+ def initialize(client, token, opts={})
40
+ @client = client
41
+ @token = token.to_s
42
+ [:refresh_token, :expires_in, :expires_at].each do |arg|
43
+ instance_variable_set("@#{arg}", opts.delete(arg) || opts.delete(arg.to_s))
44
+ end
45
+ @expires_in ||= opts.delete('expires')
46
+ @expires_in &&= @expires_in.to_i
47
+ @expires_at &&= @expires_at.to_i
48
+ @expires_at ||= Time.now.to_i + @expires_in if @expires_in
49
+ @options = {:mode => opts.delete(:mode) || :header,
50
+ :header_format => opts.delete(:header_format) || 'Bearer %s',
51
+ :param_name => opts.delete(:param_name) || 'access_token'}
52
+ @params = opts
53
+ end
54
+
55
+ # Indexer to additional params present in token response
56
+ #
57
+ # @param [String] key entry key to Hash
58
+ def [](key)
59
+ @params[key]
60
+ end
61
+
62
+ # Whether or not the token expires
63
+ #
64
+ # @return [Boolean]
65
+ def expires?
66
+ !!@expires_at
67
+ end
68
+
69
+ # Whether or not the token is expired
70
+ #
71
+ # @return [Boolean]
72
+ def expired?
73
+ expires? && (expires_at < Time.now.to_i)
74
+ end
75
+
76
+ # Refreshes the current Access Token
77
+ #
78
+ # @return [AccessToken] a new AccessToken
79
+ # @note options should be carried over to the new AccessToken
80
+ def refresh!(params={})
81
+ raise "A refresh_token is not available" unless refresh_token
82
+ params.merge!(:client_id => @client.id,
83
+ :client_secret => @client.secret,
84
+ :grant_type => 'client_credential'
85
+ )
86
+ new_token = @client.get_token(params)
87
+ new_token.options = options
88
+ new_token.refresh_token = refresh_token unless new_token.refresh_token
89
+ new_token
90
+ end
91
+
92
+ # Convert AccessToken to a hash which can be used to rebuild itself with AccessToken.from_hash
93
+ #
94
+ # @return [Hash] a hash of AccessToken property values
95
+ def to_hash
96
+ params.merge({:access_token => token, :refresh_token => refresh_token, :expires_at => expires_at})
97
+ end
98
+
99
+ # Make a request with the Access Token
100
+ #
101
+ # @param [Symbol] verb the HTTP request method
102
+ # @param [String] path the HTTP URL path of the request
103
+ # @param [Hash] opts the options to make the request with
104
+ # @see Client#request
105
+ def request(verb, path, opts={}, &block)
106
+ set_token(opts)
107
+ @client.request(verb, path, opts, &block)
108
+ end
109
+
110
+ # Make a GET request with the Access Token
111
+ #
112
+ # @see AccessToken#request
113
+ def get(path, opts={}, &block)
114
+ request(:get, path, opts, &block)
115
+ end
116
+
117
+ # Make a POST request with the Access Token
118
+ #
119
+ # @see AccessToken#request
120
+ def post(path, opts={}, &block)
121
+ request(:post, path, opts, &block)
122
+ end
123
+
124
+ # Make a PUT request with the Access Token
125
+ #
126
+ # @see AccessToken#request
127
+ def put(path, opts={}, &block)
128
+ request(:put, path, opts, &block)
129
+ end
130
+
131
+ # Make a PATCH request with the Access Token
132
+ #
133
+ # @see AccessToken#request
134
+ def patch(path, opts={}, &block)
135
+ request(:patch, path, opts, &block)
136
+ end
137
+
138
+ # Make a DELETE request with the Access Token
139
+ #
140
+ # @see AccessToken#request
141
+ def delete(path, opts={}, &block)
142
+ request(:delete, path, opts, &block)
143
+ end
144
+
145
+ # Get the headers hash (includes Authorization token)
146
+ def headers
147
+ { 'Authorization' => options[:header_format] % token }
148
+ end
149
+
150
+ private
151
+ def set_token(opts)
152
+ case options[:mode]
153
+ when :header
154
+ opts[:headers] ||= {}
155
+ opts[:headers].merge!(headers)
156
+ when :query
157
+ opts[:params] ||= {}
158
+ opts[:params][options[:param_name]] = token
159
+ when :body
160
+ opts[:body] ||= {}
161
+ if opts[:body].is_a?(Hash)
162
+ opts[:body][options[:param_name]] = token
163
+ else
164
+ opts[:body] << "&#{options[:param_name]}=#{token}"
165
+ end
166
+ # @todo support for multi-part (file uploads)
167
+ else
168
+ raise "invalid :mode option of #{options[:mode]}"
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,199 @@
1
+ # encoding: utf-8
2
+ module MpWeixin
3
+ # The MpWeixin::Client class
4
+ # reference to The OAuth2::Client class
5
+ class Client
6
+ attr_reader :id, :secret, :site
7
+ attr_accessor :options, :token
8
+ attr_writer :connection
9
+
10
+ # Instantiate a new client using the
11
+ # Client ID and Client Secret registered to your
12
+ # weixin mp account.
13
+ #
14
+ # @param [String] app_id the app_id value
15
+ # @param [String] app_secret the app_secret value
16
+ # @param [Hash] opts the options to create the client with
17
+ # @option opts [String] :site the site host provider to connection
18
+ # @option opts [String] :authorize_url ('/oauth/authorize') absolute or relative URL path to the Authorization endpoint
19
+ # @option opts [String] :token_url ('/oauth/token') absolute or relative URL path to the Token endpoint
20
+ # @option opts [Hash] :connection_opts ({}) Hash of connection options to pass to initialize Faraday with
21
+ # @option opts [Boolean] :raise_errors (true) whether or not to raise an MpWeixin::Error
22
+ # on responses with 400+ status codes
23
+ # @yield [builder] The Faraday connection builder
24
+ def initialize(app_id = nil, app_secret = nil, opts={}, &block)
25
+ _opts = opts.dup
26
+ @id = app_id || Config.app_id
27
+ @secret = app_secret || Config.app_secret
28
+ @site = _opts.delete(:site) || "https://api.weixin.qq.com/"
29
+ ssl = _opts.delete(:ssl)
30
+ @options = {:authorize_url => '/oauth/authorize',
31
+ :token_url => '/cgi-bin/token',
32
+ :token_method => :post,
33
+ :connection_opts => {},
34
+ :connection_build => block,
35
+ :raise_errors => true}.merge(_opts)
36
+ @options[:connection_opts][:ssl] = ssl if ssl
37
+ end
38
+
39
+ # Whether or not the client is authorized
40
+ #
41
+ # @return [Boolean]
42
+ def is_authorized?
43
+ !!token && !token.expired?
44
+ end
45
+
46
+ # Set the site host
47
+ #
48
+ # @param [String] the MpWeixin provider site host
49
+ def site=(value)
50
+ @connection = nil
51
+ @site = value
52
+ end
53
+
54
+ # The Faraday connection object
55
+ def connection
56
+ @connection ||= begin
57
+ conn = Faraday.new(site, options[:connection_opts])
58
+ conn.build do |b|
59
+ options[:connection_build].call(b)
60
+ end if options[:connection_build]
61
+ conn
62
+ end
63
+ end
64
+
65
+ # Makes a request relative to the specified site root.
66
+ #
67
+ # @param [Symbol] verb one of :get, :post, :put, :delete
68
+ # @param [String] url URL path of request
69
+ # @param [Hash] opts the options to make the request with
70
+ # @option opts [Hash] :params additional query parameters for the URL of the request
71
+ # @option opts [Hash, String] :body the body of the request
72
+ # @option opts [Hash] :headers http request headers
73
+ # @option opts [Boolean] :raise_errors whether or not to raise an MpWeixin::Error on 400+ status
74
+ def request(verb, url, opts={})
75
+ url = self.connection.build_url(url, opts[:params]).to_s
76
+
77
+ response = connection.run_request(verb, url, opts[:body], opts[:headers]) do |req|
78
+ yield(req) if block_given?
79
+ end
80
+ response = Response.new(response, :parse => opts[:parse])
81
+
82
+ case response.status
83
+ when 301, 302, 303, 307
84
+ opts[:redirect_count] ||= 0
85
+ opts[:redirect_count] += 1
86
+ return response if opts[:redirect_count] > options[:max_redirects]
87
+ if response.status == 303
88
+ verb = :get
89
+ opts.delete(:body)
90
+ end
91
+ request(verb, response.headers['location'], opts)
92
+ when 200..299, 300..399
93
+ # on non-redirecting 3xx statuses, just return the response
94
+ response
95
+ when 400..599
96
+ e = Error.new(response)
97
+ raise e if opts.fetch(:raise_errors, options[:raise_errors])
98
+ response.error = e
99
+ response
100
+ else
101
+ raise Error.new(response), "Unhandled status code value of #{response.status}"
102
+ end
103
+ end
104
+
105
+ # The authorize endpoint URL of the MpWeixin provider
106
+ #
107
+ # @param [Hash] params additional query parameters
108
+ def authorize_url(params=nil)
109
+ connection.build_url(options[:authorize_url], params).to_s
110
+ end
111
+
112
+ # The token endpoint URL of the MpWeixin provider
113
+ #
114
+ # @param [Hash] params additional query parameters
115
+ def token_url(params=nil)
116
+ connection.build_url(options[:token_url], params).to_s
117
+ end
118
+
119
+ # Initializes an AccessToken by making a request to the token endpoint
120
+ #
121
+ # @param [Hash] params a Hash of params for the token endpoint
122
+ # @param [Hash] access token options, to pass to the AccessToken object
123
+ # @param [Class] class of access token for easier subclassing MpWeixin::AccessToken
124
+ # @return [AccessToken] the initalized AccessToken
125
+ def get_token(params = {}, access_token_opts = {}, access_token_class = AccessToken)
126
+ params = ActiveSupport::HashWithIndifferentAccess.new(params)
127
+ params.reverse_merge!(grant_type: "client_credential", appid: id, secret: secret)
128
+
129
+ opts = {:raise_errors => options[:raise_errors], :parse => params.delete(:parse)}
130
+ if options[:token_method] == :post
131
+ headers = params.delete(:headers)
132
+ opts[:body] = params
133
+ opts[:headers] = {'Content-Type' => 'application/x-www-form-urlencoded'}
134
+ opts[:headers].merge!(headers) if headers
135
+ else
136
+ opts[:params] = params
137
+ end
138
+ response = request(options[:token_method], token_url, opts)
139
+ raise Error.new(response) if options[:raise_errors] && !(response.parsed.is_a?(Hash) && response.parsed['access_token'])
140
+ @token = access_token_class.from_hash(self, response.parsed.merge(access_token_opts))
141
+ end
142
+
143
+ # Initializes an AccessToken from a hash
144
+ #
145
+ # @param [Hash] hash a Hash contains access_token and expires
146
+ # @return [AccessToken] the initalized AccessToken
147
+ def get_token_from_hash(hash)
148
+ access_token = hash.delete('access_token') || hash.delete(:access_token) || hash.delete('oauth_token') || hash.delete(:oauth_token)
149
+ opts = {:expires_at => hash["expires"] || hash[:expires],
150
+ :header_format => "OAuth2 %s",
151
+ :param_name => "access_token"}
152
+
153
+ @token = AccessToken.new(self, access_token, opts)
154
+ end
155
+
156
+ # Initializes a new Client from a hash
157
+ #
158
+ # @param [Hash] a Hash contains access_token and expires
159
+ # @param [Hash] opts the options to create the client with
160
+ # @option opts [Hash] :connection_opts ({}) Hash of connection options to pass to initialize Faraday with
161
+ # @option opts [FixNum] :max_redirects (5) maximum number of redirects to follow
162
+ # @yield [builder] The Faraday connection builder
163
+ def self.from_hash(hash, opts={}, &block)
164
+ client = self.new(opts, &block)
165
+ client.get_token_from_hash(hash)
166
+
167
+ client
168
+ end
169
+
170
+ #
171
+ # APIs
172
+ #
173
+
174
+ # assocation an Interface::Message instance to client
175
+ def message
176
+ @message ||= Interface::Message.new(self)
177
+ end
178
+
179
+ # assocation an Interface::Menu instance to client
180
+ def menu
181
+ @menu ||= Interface::Menu.new(self)
182
+ end
183
+
184
+ # assocation an Interface::Promotion instance to client
185
+ def promotion
186
+ @promotion ||= Interface::Promotion.new(self)
187
+ end
188
+
189
+ # assocation an Interface::Group instance to client
190
+ def group
191
+ @group ||= Interface::Group.new(self)
192
+ end
193
+
194
+ # assocation an Interface::User instance to client
195
+ def user
196
+ @user ||= Interface::User.new(self)
197
+ end
198
+ end
199
+ end