mp_weixin 0.1.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.
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,227 @@
1
+ # encoding: utf-8
2
+
3
+ module MpWeixin
4
+ # The MpWeixin::Message class
5
+ #
6
+ class Message
7
+ include ActiveModel::Model
8
+ attr_accessor :ToUserName, :FromUserName,
9
+ :CreateTime, :MsgType, :MsgId
10
+
11
+ # Instantiate a new Message with a hash of attributes
12
+ #
13
+ # @param [Hash] attributes the attributes value
14
+ def initialize(attributes = nil)
15
+ # Dynamic attr_accessible
16
+ # maybe cause secret problem
17
+ # singleton_class.class_eval do
18
+ # attr_accessor *attributes.keys
19
+ # end
20
+
21
+ super
22
+ @source = ActiveSupport::HashWithIndifferentAccess.new(attributes)
23
+ end
24
+
25
+ # same as @attributes CreateTime of an Message instance
26
+ #
27
+ # @return [Integer]
28
+ def create_time
29
+ self.CreateTime.to_i
30
+ end
31
+ # alias :CreateTime :create_time
32
+
33
+ # convert create_time to an Time instance
34
+ #
35
+ # @return [Time]
36
+ def created_at
37
+ Time.at create_time rescue nil
38
+ end
39
+
40
+ def msg_id
41
+ self.MsgId.to_i
42
+ end
43
+
44
+ # initialize an ReplyMessage
45
+ # @msg_type [string] the MsgType of ReplyMessage
46
+ # @attributes [Hash] the attributes of ReplyMessage
47
+ # @return an instance of #{MsgType}ReplyMessage
48
+ def reply(msg_type, attributes)
49
+ if attributes.is_a?(Hash)
50
+ attributes = attributes.deep_symbolize_keys
51
+ attributes.reverse_merge!({
52
+ ToUserName: self.FromUserName,
53
+ FromUserName: self.ToUserName
54
+ })
55
+ end
56
+
57
+ case msg_type
58
+ when 'text'
59
+ MpWeixin::TextReplyMessage.new(attributes)
60
+ when 'image'
61
+ MpWeixin::ImageReplyMessage.new(attributes)
62
+ when 'voice'
63
+ MpWeixin::VoiceReplyMessage.new(attributes)
64
+ when 'video'
65
+ MpWeixin::VideoReplyMessage.new(attributes)
66
+ when 'music'
67
+ MpWeixin::MusicReplyMessage.new(attributes)
68
+ when 'news'
69
+ MpWeixin::NewsReplyMessage.new(attributes)
70
+ else
71
+ # raise 'Unknown Message data'
72
+ end
73
+ end
74
+
75
+ # initialize an TextReplyMessage
76
+ # @attributes [Hash] the attributes of TextReplyMessage
77
+ def reply_text_message(attributes)
78
+ reply("text", attributes)
79
+ end
80
+
81
+ # initialize an ImageReplyMessage
82
+ # @attributes [Hash] the attributes of ImageReplyMessage
83
+ def reply_image_message(attributes)
84
+ reply("image", attributes)
85
+ end
86
+
87
+ # initialize an VoiceReplyMessage
88
+ # @attributes [Hash] the attributes of VoiceReplyMessage
89
+ def reply_voice_message(attributes)
90
+ reply("voice", attributes)
91
+ end
92
+
93
+ # initialize an VideoReplyMessage
94
+ # @attributes [Hash] the attributes of VideoReplyMessage
95
+ def reply_video_message(attributes)
96
+ reply("video", attributes)
97
+ end
98
+
99
+ # initialize an MusicReplyMessage
100
+ # @attributes [Hash] the attributes of MusicReplyMessage
101
+ def reply_music_message(attributes)
102
+ reply("music", attributes)
103
+ end
104
+
105
+ # initialize an NewsReplyMessage
106
+ # @attributes [Hash] the attributes of NewsReplyMessage
107
+ def reply_news_message(attributes)
108
+ reply("news", attributes)
109
+ end
110
+
111
+ class << self
112
+ def from_xml(xml)
113
+ begin
114
+ hash = MultiXml.parse(xml)['xml']
115
+ message = case hash['MsgType']
116
+ when 'text'
117
+ TextMessage.new(hash)
118
+ when 'image'
119
+ ImageMessage.new(hash)
120
+ when 'location'
121
+ LocationMessage.new(hash)
122
+ when 'link'
123
+ LinkMessage.new(hash)
124
+ when 'event'
125
+ # EventMessage.new(hash)
126
+ Event.from_xml(xml)
127
+ when 'voice'
128
+ VoiceMessage.new(hash)
129
+ when 'video'
130
+ VideoMessage.new(hash)
131
+ else
132
+ # raise 'Unknown Message data'
133
+ end
134
+ rescue
135
+ logger.info('Unknown Message data #{xml}') if self.respond_to?(:logger)
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ # <xml>
142
+ # <ToUserName><![CDATA[toUser]]></ToUserName>
143
+ # <FromUserName><![CDATA[fromUser]]></FromUserName>
144
+ # <CreateTime>1348831860</CreateTime>
145
+ # <MsgType><![CDATA[text]]></MsgType>
146
+ # <Content><![CDATA[this is a test]]></Content>
147
+ # <MsgId>1234567890123456</MsgId>
148
+ # </xml>
149
+ # TextMessage = Class.new(Message)
150
+ class TextMessage < Message
151
+ attr_accessor :Content
152
+ end
153
+
154
+ # <xml>
155
+ # <ToUserName><![CDATA[toUser]]></ToUserName>
156
+ # <FromUserName><![CDATA[fromUser]]></FromUserName>
157
+ # <CreateTime>1348831860</CreateTime>
158
+ # <MsgType><![CDATA[image]]></MsgType>
159
+ # <PicUrl><![CDATA[this is a url]]></PicUrl>
160
+ # <MsgId>1234567890123456</MsgId>
161
+ # </xml>
162
+ # ImageMessage = Class.new(Message)
163
+ class ImageMessage < Message
164
+ attr_accessor :PicUrl, :MediaId
165
+ end
166
+
167
+ # <xml>
168
+ # <ToUserName><![CDATA[toUser]]></ToUserName>
169
+ # <FromUserName><![CDATA[fromUser]]></FromUserName>
170
+ # <CreateTime>1376632760</CreateTime>
171
+ # <MsgType><![CDATA[voice]]></MsgType>
172
+ # <MediaId><![CDATA[Qyb0tgux6QLjhL6ipvFZJ-kUt2tcQtkn0BU365Vt3wUAtqfGam4QpZU35RXVhv6G]]></MediaId>
173
+ # <Format><![CDATA[amr]]></Format>
174
+ # <MsgId>5912592682802219078</MsgId>
175
+ # <Recognition><![CDATA[]]></Recognition>
176
+ # </xml>
177
+ # VoiceMessage = Class.new(Message)
178
+ class VoiceMessage < Message
179
+ attr_accessor :MediaId, :Format
180
+ end
181
+
182
+ # <xml>
183
+ # <ToUserName><![CDATA[toUser]]></ToUserName>
184
+ # <FromUserName><![CDATA[fromUser]]></FromUserName>
185
+ # <CreateTime>1376632994</CreateTime>
186
+ # <MsgType><![CDATA[video]]></MsgType>
187
+ # <MediaId><![CDATA[TAAGb6iS5LcZR1d5ICiZTWGWi6-Upic9tlWDpAKcNJA]]></MediaId>
188
+ # <ThumbMediaId><![CDATA[U-xulPW4kq6KKMWFNaBSPc65Bcgr7Qopwex0DfCeyQs]]></ThumbMediaId>
189
+ # <MsgId>5912593687824566343</MsgId>
190
+ # </xml>
191
+ # VideoMessage = Class.new(Message)
192
+ class VideoMessage < Message
193
+ attr_accessor :MediaId, :ThumbMediaId
194
+ end
195
+
196
+ # <xml>
197
+ # <ToUserName><![CDATA[toUser]]></ToUserName>
198
+ # <FromUserName><![CDATA[fromUser]]></FromUserName>
199
+ # <CreateTime>1351776360</CreateTime>
200
+ # <MsgType><![CDATA[location]]></MsgType>
201
+ # <Location_X>23.134521</Location_X>
202
+ # <Location_Y>113.358803</Location_Y>
203
+ # <Scale>20</Scale>
204
+ # <Label><![CDATA[位置信息]]></Label>
205
+ # <MsgId>1234567890123456</MsgId>
206
+ # </xml>
207
+ # LocationMessage = Class.new(Message)
208
+ class LocationMessage < Message
209
+ attr_accessor :Location_X , :Location_Y, :Scale, :Label
210
+ end
211
+
212
+ # <xml>
213
+ # <ToUserName><![CDATA[toUser]]></ToUserName>
214
+ # <FromUserName><![CDATA[fromUser]]></FromUserName>
215
+ # <CreateTime>1351776360</CreateTime>
216
+ # <MsgType><![CDATA[link]]></MsgType>
217
+ # <Title><![CDATA[公众平台官网链接]]></Title>
218
+ # <Description><![CDATA[公众平台官网链接]]></Description>
219
+ # <Url><![CDATA[url]]></Url>
220
+ # <MsgId>1234567890123456</MsgId>
221
+ # </xml>
222
+ #
223
+ # LinkMessage = Class.new(Message)
224
+ class LinkMessage < Message
225
+ attr_accessor :Title , :Description, :Url
226
+ end
227
+ end
@@ -0,0 +1,180 @@
1
+ # encoding: utf-8
2
+
3
+ module MpWeixin
4
+ class ReplyMessage
5
+ include ActiveModel::Model
6
+ include ROXML
7
+
8
+ xml_name :xml
9
+ #xml_convention :camelcase
10
+
11
+ attr_accessor :ToUserName, :FromUserName,
12
+ :CreateTime, :MsgType
13
+
14
+ xml_accessor :ToUserName, :cdata => true
15
+ xml_accessor :FromUserName, :cdata => true
16
+ xml_reader :CreateTime, :as => Integer
17
+ xml_reader :MsgType, :cdata => true
18
+
19
+ def initialize(attributes = {})
20
+ super
21
+ @CreateTime ||= Time.now.to_i
22
+ end
23
+
24
+ def to_xml
25
+ super.to_xml(:encoding => 'UTF-8', :indent => 0, :save_with => 0)
26
+ end
27
+
28
+ class << self
29
+ def set_nested_attr(nested_attr_name)
30
+ # define_method(:hi) { puts "Hello World!" }
31
+ class_eval <<-STR
32
+ def #{nested_attr_name}=(nested_attr = [])
33
+ @#{nested_attr_name} = nested_attr.is_a?(#{nested_attr_name}) ? nested_attr : #{nested_attr_name}.new(nested_attr)
34
+ end
35
+ STR
36
+ end
37
+ end
38
+ end
39
+
40
+ class TextReplyMessage < ReplyMessage
41
+ # include ActiveModel::Model
42
+
43
+ attr_accessor :Content
44
+ xml_accessor :Content, :cdata => true
45
+
46
+ def initialize(attributes = {})
47
+ super
48
+ @MsgType ||= 'text'
49
+ end
50
+ end
51
+
52
+ class Image
53
+ include ActiveModel::Model
54
+ include ROXML
55
+
56
+ attr_accessor :MediaId
57
+ xml_accessor :MediaId
58
+
59
+ # other important functionality
60
+ end
61
+
62
+ class ImageReplyMessage < ReplyMessage
63
+ attr_accessor :Image
64
+ xml_accessor :Image, as: Image, :cdata => true
65
+
66
+ set_nested_attr :Image
67
+ def initialize(attributes = {})
68
+ super
69
+ @MsgType ||= 'image'
70
+ end
71
+
72
+ end
73
+
74
+ class Voice
75
+ include ActiveModel::Model
76
+ include ROXML
77
+
78
+ xml_accessor :MediaId
79
+
80
+ # other important functionality
81
+ end
82
+
83
+ class VoiceReplyMessage < ReplyMessage
84
+ attr_accessor :Voice
85
+ xml_accessor :Voice, as: Voice, :cdata => true
86
+ set_nested_attr :Voice
87
+
88
+ def initialize(attributes = {})
89
+ super
90
+ @MsgType ||= 'voice'
91
+ end
92
+
93
+ end
94
+
95
+ class Video
96
+ include ActiveModel::Model
97
+ include ROXML
98
+
99
+ attr_accessor :MediaId, :Title, :Description
100
+
101
+ xml_accessor :MediaId, :cdata => true
102
+ xml_accessor :Title, :cdata => true
103
+ xml_accessor :Description, :cdata => true
104
+
105
+ end
106
+
107
+ class VideoReplyMessage < ReplyMessage
108
+ attr_accessor :Video
109
+ xml_accessor :Video, as: Video, :cdata => true
110
+ set_nested_attr :Video
111
+
112
+ def initialize(attributes = {})
113
+ super
114
+ @MsgType ||= 'video'
115
+ end
116
+
117
+ end
118
+
119
+ class Music
120
+ include ActiveModel::Model
121
+ include ROXML
122
+
123
+ attr_accessor :Title, :Description, :MusicUrl, :HQMusicUrl, :ThumbMediaId
124
+
125
+ xml_accessor :Title, :cdata => true
126
+ xml_accessor :Description, :cdata => true
127
+ xml_accessor :MusicUrl, :cdata => true
128
+ xml_accessor :HQMusicUrl, :cdata => true
129
+ xml_accessor :ThumbMediaId, :cdata => true
130
+ end
131
+
132
+ class MusicReplyMessage < ReplyMessage
133
+ attr_accessor :Music
134
+ xml_accessor :Music, :as => Music
135
+ set_nested_attr :Music
136
+
137
+ def initialize(attributes = {})
138
+ super
139
+ @MsgType ||= 'music'
140
+ end
141
+
142
+ end
143
+
144
+ class Item
145
+ include ActiveModel::Model
146
+ include ROXML
147
+
148
+ attr_accessor :Title, :Description, :PicUrl, :Url
149
+ xml_accessor :Title, :cdata => true
150
+ xml_accessor :Description, :cdata => true
151
+ xml_accessor :PicUrl, :cdata => true
152
+ xml_accessor :Url, :cdata => true
153
+ end
154
+
155
+ class NewsReplyMessage < ReplyMessage
156
+ attr_accessor :ArticleCount, :Articles
157
+ xml_accessor :ArticleCount, :as => Integer
158
+ xml_accessor :Articles, :as => [Item], :in => 'Articles', :from => 'item'
159
+ set_nested_attr :Articles
160
+
161
+ def initialize(attributes = {})
162
+ super
163
+ @MsgType ||= 'news'
164
+ end
165
+
166
+ def Articles=(attributes)
167
+ _attributes = attributes.is_a?(Hash) ? attributes.deep_symbolize_keys[:item] : attributes.dup
168
+
169
+ @Articles = _attributes.collect do |item_attr|
170
+
171
+ if item_attr.is_a?(Item)
172
+ item_attr
173
+ else
174
+ item_attr = item_attr.deep_symbolize_keys
175
+ Item.new(item_attr)
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,93 @@
1
+ require 'multi_json'
2
+ require 'rack'
3
+
4
+ module MpWeixin
5
+ # MpWeixin::Response class
6
+ class Response
7
+ attr_reader :response
8
+ attr_accessor :error, :options
9
+
10
+ # Adds a new content type parser.
11
+ #
12
+ # @param [Symbol] key A descriptive symbol key such as :json or :query.
13
+ # @param [Array] One or more mime types to which this parser applies.
14
+ # @yield [String] A block returning parsed content.
15
+ def self.register_parser(key, mime_types, &block)
16
+ key = key.to_sym
17
+ PARSERS[key] = block
18
+ Array(mime_types).each do |mime_type|
19
+ CONTENT_TYPES[mime_type] = key
20
+ end
21
+ end
22
+
23
+ # Initializes a Response instance
24
+ #
25
+ # @param [Faraday::Response] response The Faraday response instance
26
+ # @param [Hash] opts options in which to initialize the instance
27
+ # @option opts [Symbol] :parse (:automatic) how to parse the response body. one of :query (for x-www-form-urlencoded),
28
+ # :json, or :automatic (determined by Content-Type response header)
29
+ def initialize(response, opts={})
30
+ @response = response
31
+ @options = {:parse => :automatic}.merge(opts)
32
+ end
33
+
34
+ # The HTTP response headers
35
+ def headers
36
+ response.headers
37
+ end
38
+
39
+ # The HTTP response status code
40
+ def status
41
+ response.status
42
+ end
43
+
44
+ # The HTTP response body
45
+ def body
46
+ response.body || ''
47
+ end
48
+
49
+ # Procs that, when called, will parse a response body according
50
+ # to the specified format.
51
+ PARSERS = {
52
+ # Can't reliably detect whether MultiJson responds to load, since it's
53
+ # a reserved word. Use adapter as a proxy for new features.
54
+ :json => lambda{ |body| MultiJson.respond_to?(:adapter) ? MultiJson.load(body) : MultiJson.decode(body) rescue body },
55
+ :query => lambda{ |body| Rack::Utils.parse_query(body) },
56
+ :text => lambda{ |body| body }
57
+ }
58
+
59
+ # Content type assignments for various potential HTTP content types.
60
+ CONTENT_TYPES = {
61
+ 'application/json' => :json,
62
+ 'text/javascript' => :json,
63
+ 'application/x-www-form-urlencoded' => :query,
64
+ 'text/plain' => :text
65
+ }
66
+
67
+ # The parsed response body.
68
+ # Will attempt to parse application/x-www-form-urlencoded and
69
+ # application/json Content-Type response bodies
70
+ def parsed
71
+ return nil unless PARSERS.key?(parser)
72
+ @parsed ||= PARSERS[parser].call(body)
73
+ end
74
+
75
+ # Attempts to determine the content type of the response.
76
+ def content_type
77
+ ((response.headers.values_at('content-type', 'Content-Type').compact.first || '').split(';').first || '').strip
78
+ end
79
+
80
+ # Determines the parser that will be used to supply the content of #parsed
81
+ def parser
82
+ return options[:parse].to_sym if PARSERS.key?(options[:parse])
83
+ CONTENT_TYPES[content_type]
84
+ end
85
+ end
86
+ end
87
+
88
+ begin
89
+ require 'multi_xml'
90
+ MpWeixin::Response.register_parser(:xml, ['text/xml', 'application/rss+xml', 'application/rdf+xml', 'application/atom+xml']) do |body|
91
+ MultiXml.parse(body) rescue body
92
+ end
93
+ rescue LoadError; end