mp-weixin 0.0.4.2

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.
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+
15
+ # YARD artifacts
16
+ .yardoc
17
+ _yardoc
18
+ doc/
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ - 1.9.3
5
+ - 1.8.7
6
+
7
+ # uncomment this line if your project needs to run something other than `rake`:
8
+ # script: bundle exec rspec spec
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+ source 'http://ruby.taobao.org'
3
+
4
+ gemspec
@@ -0,0 +1,46 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rack-weixin (0.0.4.2)
5
+ multi_json (>= 1.7.9)
6
+ multi_xml (>= 0.5.2)
7
+ nestful
8
+ rack
9
+ roxml
10
+
11
+ GEM
12
+ remote: http://ruby.taobao.org/
13
+ specs:
14
+ activesupport (3.2.12)
15
+ i18n (~> 0.6)
16
+ multi_json (~> 1.0)
17
+ diff-lcs (1.1.3)
18
+ i18n (0.6.5)
19
+ multi_json (1.8.2)
20
+ multi_xml (0.5.3)
21
+ nestful (1.0.7)
22
+ nokogiri (1.5.6)
23
+ rack (1.5.2)
24
+ rack-test (0.6.2)
25
+ rack (>= 1.0)
26
+ rake (10.0.3)
27
+ roxml (3.3.1)
28
+ activesupport (>= 2.3.0)
29
+ nokogiri (>= 1.3.3)
30
+ rspec (2.12.0)
31
+ rspec-core (~> 2.12.0)
32
+ rspec-expectations (~> 2.12.0)
33
+ rspec-mocks (~> 2.12.0)
34
+ rspec-core (2.12.2)
35
+ rspec-expectations (2.12.1)
36
+ diff-lcs (~> 1.1.3)
37
+ rspec-mocks (2.12.2)
38
+
39
+ PLATFORMS
40
+ ruby
41
+
42
+ DEPENDENCIES
43
+ rack-test
44
+ rack-weixin!
45
+ rake
46
+ rspec (>= 2.0.0)
@@ -0,0 +1,113 @@
1
+ 微信公众平台 开放消息接口 Rack Middleware
2
+ ========================================
3
+
4
+ [![Build Status](https://travis-ci.org/wolfg1969/rack-weixin.png?branch=master)](https://travis-ci.org/wolfg1969/rack-weixin) [![Gem Version](https://badge.fury.io/rb/rack-weixin.png)](http://badge.fury.io/rb/rack-weixin)
5
+
6
+ * 验证微信请求 with 'weixin/middleware'
7
+ * 解析推送消息 with 'weixin/model'
8
+ * 生成回复消息 with 'weixin/model'
9
+
10
+
11
+ Installation
12
+ ------------
13
+ ```
14
+ $ gem install rack-weixin
15
+ ```
16
+
17
+
18
+ Usage
19
+ -----
20
+
21
+ A sinatra demo
22
+
23
+ ```ruby
24
+ # -*- encoding : utf-8 -*-
25
+ require 'sinatra'
26
+ require 'rack-weixin'
27
+
28
+ use Weixin::Middleware, 'your api token', '/your_app_root'
29
+
30
+ configure do
31
+ set :wx_id, 'your_weixin_account'
32
+ end
33
+
34
+ helpers do
35
+ def msg_router(msg)
36
+ case msg.MsgType
37
+ when 'text'
38
+ # text message handler
39
+ when 'image'
40
+ # image message handler
41
+ when 'location'
42
+ # location message handler
43
+ when 'link'
44
+ # link message handler
45
+ when 'event'
46
+ # event messge handler
47
+ when 'voice'
48
+ # voice message handler
49
+ when 'video'
50
+ # video message handler
51
+ else
52
+ Weixin.text_msg(msg.ToUserName, msg.FromUserName, '未知消息类型')
53
+ end
54
+ end
55
+ end
56
+
57
+ get '/your_app_root' do
58
+ params[:echostr]
59
+ end
60
+
61
+ post '/your_app_root' do
62
+ content_type :xml, 'charset' => 'utf-8'
63
+
64
+ message = request.env[Weixin::Middleware::WEIXIN_MSG]
65
+ logger.info "原始数据: #{request.env[Weixin::Middleware::WEIXIN_MSG_RAW]}"
66
+
67
+ # handle the message according to your business logic
68
+ msg_router(message) unless message.nil?
69
+ end
70
+ ```
71
+
72
+ ### Padrino下使用
73
+ 在`Gemfile`里加入:
74
+
75
+ gem 'rack-weixin'
76
+
77
+ 在`config/apps.rb`关闭以下两个:
78
+
79
+ set :protection, false
80
+ set :protect_from_csrf, false
81
+
82
+ 在`app.rb`里加入:
83
+
84
+ use Weixin::Middleware, 'your api token', '/your_app_root'
85
+
86
+ configure do
87
+ set :wx_id, 'your_weixin_account'
88
+ end
89
+
90
+
91
+
92
+ TODO
93
+ ----
94
+
95
+ Copyright
96
+ ---------
97
+
98
+ Permission is hereby granted, free of charge, to any person obtaining a copy
99
+ of this software and associated documentation files (the "Software"), to
100
+ deal in the Software without restriction, including without limitation the
101
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
102
+ sell copies of the Software, and to permit persons to whom the Software is
103
+ furnished to do so, subject to the following conditions:
104
+
105
+ The above copyright notice and this permission notice shall be included in
106
+ all copies or substantial portions of the Software.
107
+
108
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
109
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
110
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
111
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
112
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
113
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,11 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ desc 'Run all the tests'
5
+ task :default => [:test]
6
+
7
+ desc "Run all the tests"
8
+ task :test do
9
+ sh "rspec -Ilib/*:spec spec"
10
+ end
11
+
@@ -0,0 +1,53 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require 'weixin/version'
3
+ require 'weixin/middleware'
4
+ require 'weixin/menu'
5
+ require 'weixin/model'
6
+
7
+ module Weixin
8
+
9
+ extend self
10
+
11
+ def music(title, desc, music_url, hq_music_url)
12
+ item = Music.new
13
+ item.Title = title
14
+ item.Description = desc
15
+ item.MusicUrl = music_url
16
+ item.HQMusicUrl = hq_music_url
17
+ item
18
+ end
19
+
20
+ def item(title, desc, pic_url, link_url)
21
+ item = Item.new
22
+ item.Title = title
23
+ item.Description = desc
24
+ item.PicUrl = pic_url
25
+ item.Url = link_url
26
+ item
27
+ end
28
+
29
+ def text_msg(from, to, content)
30
+ msg = TextReplyMessage.new
31
+ msg.ToUserName = to
32
+ msg.FromUserName = from
33
+ msg.Content = content
34
+ msg.to_xml
35
+ end
36
+
37
+ def music_msg(from, to, music)
38
+ msg = MusicReplyMessage.new
39
+ msg.ToUserName = to
40
+ msg.FromUserName = from
41
+ msg.Music = music
42
+ msg.to_xml
43
+ end
44
+
45
+ def news_msg(from, to, items)
46
+ msg = NewsReplyMessage.new
47
+ msg.ToUserName = to
48
+ msg.FromUserName = from
49
+ msg.Articles = items
50
+ msg.ArticleCount = items.count
51
+ msg.to_xml
52
+ end
53
+ end
@@ -0,0 +1,60 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'multi_json'
3
+ require 'nestful'
4
+
5
+ module Weixin
6
+
7
+ class Menu
8
+ def initialize(api, key)
9
+ @api = api
10
+ @key = key
11
+
12
+ @access_token = nil
13
+ @expired_at = Time.now
14
+ @endpoint = 'https://api.weixin.qq.com/cgi-bin'
15
+ end
16
+
17
+ def access_token
18
+ if Time.now >= @expired_at
19
+ authenticate
20
+ end
21
+ @access_token
22
+ end
23
+
24
+ def gw_path(method)
25
+ "/menu/#{method}?access_token=#{access_token}"
26
+ end
27
+
28
+ def gw_url(method)
29
+ "#{@endpoint}" + gw_path(method)
30
+ end
31
+
32
+ def get
33
+ request = Nestful.get gw_url('get') rescue nil
34
+ MultiJson.load(request.body) unless request.nil?
35
+ end
36
+
37
+ def add(menu)
38
+ request = Nestful::Connection.new(gw_url('create')).post(gw_path('create'), MultiJson.dump(menu)) rescue nil
39
+ unless request.nil?
40
+ errcode = MultiJson.load(request.body)['errcode']
41
+ return true if errcode == 0
42
+ end
43
+ false
44
+ end
45
+
46
+ def authenticate
47
+ url = "#{@endpoint}/token"
48
+ request = Nestful.get url, { :grant_type => 'client_credential', :appid => @api, :secret => @key } rescue nil
49
+ unless request.nil?
50
+ auth = MultiJson.load(request.body)
51
+ unless auth.has_key?('errcode')
52
+ @access_token = auth['access_token']
53
+ @expired_at = Time.now + auth['expires_in'].to_i
54
+ end
55
+ @access_token
56
+ end
57
+ end
58
+ end
59
+
60
+ end
@@ -0,0 +1,62 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'digest/sha1'
3
+
4
+ module Weixin
5
+
6
+ class Middleware
7
+
8
+ POST_BODY = 'rack.input'.freeze
9
+ WEIXIN_MSG = 'weixin.msg'.freeze
10
+ WEIXIN_MSG_RAW = 'weixin.msg.raw'.freeze
11
+
12
+ def initialize(app, app_token, path)
13
+ @app = app
14
+ @app_token = app_token
15
+ @path = path
16
+ end
17
+
18
+ def call(env)
19
+ dup._call(env)
20
+ end
21
+
22
+ def _call(env)
23
+ if @path == env['PATH_INFO'].to_s && ['GET', 'POST'].include?(env['REQUEST_METHOD'])
24
+
25
+ @req = Rack::Request.new(env)
26
+ return invalid_request! unless request_is_valid?
27
+ return [
28
+ 200,
29
+ { 'Content-type' => 'text/plain', 'Content-length' => @req.params['echostr'].length.to_s },
30
+ [ @req.params['echostr'] ]
31
+ ] if @req.get?
32
+
33
+ raw_msg = env[POST_BODY].read
34
+ begin
35
+ env.update WEIXIN_MSG => Weixin::Message.factory(raw_msg), WEIXIN_MSG_RAW => raw_msg
36
+ @app.call(env)
37
+ rescue Exception => e
38
+ return [500, { 'Content-type' => 'text/html' }, ["Message parsing error: #{e.to_s}"]]
39
+ end
40
+ else
41
+ @app.call(env)
42
+ end
43
+
44
+ end
45
+
46
+ def invalid_request!
47
+ [401, { 'Content-type' => 'text/html', 'Content-Length' => '0'}, []]
48
+ end
49
+
50
+ def request_is_valid?
51
+ begin
52
+ param_array = [@app_token, @req.params['timestamp'], @req.params['nonce']]
53
+ sign = Digest::SHA1.hexdigest( param_array.sort.join )
54
+ sign == @req.params['signature'] ? true : false
55
+ rescue
56
+ false
57
+ end
58
+ end
59
+ end
60
+
61
+ end
62
+
@@ -0,0 +1,224 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'roxml'
3
+ require 'multi_xml'
4
+ require 'ostruct'
5
+
6
+ module Weixin
7
+
8
+ class Message
9
+
10
+ def initialize(hash)
11
+ @source = OpenStruct.new(hash)
12
+ end
13
+
14
+ def method_missing(method, *args, &block)
15
+ @source.send(method, *args, &block)
16
+ end
17
+
18
+ def CreateTime
19
+ @source.CreateTime.to_i
20
+ end
21
+
22
+ def MsgId
23
+ @source.MsgId.to_i
24
+ end
25
+
26
+ def Message.factory(xml)
27
+ hash = MultiXml.parse(xml)['xml']
28
+ case hash['MsgType']
29
+ when 'text'
30
+ TextMessage.new(hash)
31
+ when 'image'
32
+ ImageMessage.new(hash)
33
+ when 'location'
34
+ LocationMessage.new(hash)
35
+ when 'link'
36
+ LinkMessage.new(hash)
37
+ when 'event'
38
+ EventMessage.new(hash)
39
+ when 'voice'
40
+ VoiceMessage.new(hash)
41
+ when 'video'
42
+ VideoMessage.new(hash)
43
+ else
44
+ raise ArgumentError, 'Unknown Message'
45
+ end
46
+ end
47
+
48
+ end
49
+
50
+ # <xml>
51
+ # <ToUserName><![CDATA[toUser]]></ToUserName>
52
+ # <FromUserName><![CDATA[fromUser]]></FromUserName>
53
+ # <CreateTime>1348831860</CreateTime>
54
+ # <MsgType><![CDATA[text]]></MsgType>
55
+ # <Content><![CDATA[this is a test]]></Content>
56
+ # <MsgId>1234567890123456</MsgId>
57
+ # </xml>
58
+ TextMessage = Class.new(Message)
59
+
60
+ # <xml>
61
+ # <ToUserName><![CDATA[toUser]]></ToUserName>
62
+ # <FromUserName><![CDATA[fromUser]]></FromUserName>
63
+ # <CreateTime>1348831860</CreateTime>
64
+ # <MsgType><![CDATA[image]]></MsgType>
65
+ # <PicUrl><![CDATA[this is a url]]></PicUrl>
66
+ # <MsgId>1234567890123456</MsgId>
67
+ # </xml>
68
+ ImageMessage = Class.new(Message)
69
+
70
+ # <xml>
71
+ # <ToUserName><![CDATA[toUser]]></ToUserName>
72
+ # <FromUserName><![CDATA[fromUser]]></FromUserName>
73
+ # <CreateTime>1351776360</CreateTime>
74
+ # <MsgType><![CDATA[link]]></MsgType>
75
+ # <Title><![CDATA[公众平台官网链接]]></Title>
76
+ # <Description><![CDATA[公众平台官网链接]]></Description>
77
+ # <Url><![CDATA[url]]></Url>
78
+ # <MsgId>1234567890123456</MsgId>
79
+ # </xml>
80
+ LinkMessage = Class.new(Message)
81
+
82
+ # <xml>
83
+ # <ToUserName><![CDATA[toUser]]></ToUserName>
84
+ # <FromUserName><![CDATA[FromUser]]></FromUserName>
85
+ # <CreateTime>123456789</CreateTime>
86
+ # <MsgType><![CDATA[event]]></MsgType>
87
+ # <Event><![CDATA[EVENT]]></Event>
88
+ # <EventKey><![CDATA[EVENTKEY]]></EventKey>
89
+ # </xml>
90
+ EventMessage = Class.new(Message)
91
+
92
+ # <xml>
93
+ # <ToUserName><![CDATA[toUser]]></ToUserName>
94
+ # <FromUserName><![CDATA[fromUser]]></FromUserName>
95
+ # <CreateTime>1351776360</CreateTime>
96
+ # <MsgType><![CDATA[location]]></MsgType>
97
+ # <Location_X>23.134521</Location_X>
98
+ # <Location_Y>113.358803</Location_Y>
99
+ # <Scale>20</Scale>
100
+ # <Label><![CDATA[位置信息]]></Label>
101
+ # <MsgId>1234567890123456</MsgId>
102
+ # </xml>
103
+ class LocationMessage < Message
104
+
105
+ def Location_X
106
+ @source.Location_X.to_f
107
+ end
108
+
109
+ def Location_Y
110
+ @source.Location_Y.to_f
111
+ end
112
+
113
+ def Scale
114
+ @source.Scale.to_i
115
+ end
116
+ end
117
+
118
+ # <xml>
119
+ # <ToUserName><![CDATA[toUser]]></ToUserName>
120
+ # <FromUserName><![CDATA[fromUser]]></FromUserName>
121
+ # <CreateTime>1376632760</CreateTime>
122
+ # <MsgType><![CDATA[voice]]></MsgType>
123
+ # <MediaId><![CDATA[Qyb0tgux6QLjhL6ipvFZJ-kUt2tcQtkn0BU365Vt3wUAtqfGam4QpZU35RXVhv6G]]></MediaId>
124
+ # <Format><![CDATA[amr]]></Format>
125
+ # <MsgId>5912592682802219078</MsgId>
126
+ # <Recognition><![CDATA[]]></Recognition>
127
+ # </xml>
128
+ class VoiceMessage < Message
129
+
130
+ def MediaId
131
+ @source.MediaId
132
+ end
133
+
134
+ def Format
135
+ @source.Format
136
+ end
137
+ end
138
+
139
+
140
+ # <xml>
141
+ # <ToUserName><![CDATA[toUser]]></ToUserName>
142
+ # <FromUserName><![CDATA[fromUser]]></FromUserName>
143
+ # <CreateTime>1376632994</CreateTime>
144
+ # <MsgType><![CDATA[video]]></MsgType>
145
+ # <MediaId><![CDATA[TAAGb6iS5LcZR1d5ICiZTWGWi6-Upic9tlWDpAKcNJA]]></MediaId>
146
+ # <ThumbMediaId><![CDATA[U-xulPW4kq6KKMWFNaBSPc65Bcgr7Qopwex0DfCeyQs]]></ThumbMediaId>
147
+ # <MsgId>5912593687824566343</MsgId>
148
+ # </xml>
149
+ class VideoMessage < Message
150
+
151
+ def MediaId
152
+ @source.MediaId
153
+ end
154
+
155
+ def ThumbMediaId
156
+ @source.ThumbMediaId
157
+ end
158
+ end
159
+
160
+ class ReplyMessage
161
+ include ROXML
162
+ xml_name :xml
163
+ #xml_convention :camelcase
164
+
165
+ xml_accessor :ToUserName, :cdata => true
166
+ xml_accessor :FromUserName, :cdata => true
167
+ xml_reader :CreateTime, :as => Integer
168
+ xml_reader :MsgType, :cdata => true
169
+
170
+ def initialize
171
+ @CreateTime = Time.now.to_i
172
+ end
173
+
174
+ def to_xml
175
+ super.to_xml(:encoding => 'UTF-8', :indent => 0, :save_with => 0)
176
+ end
177
+ end
178
+
179
+ class TextReplyMessage < ReplyMessage
180
+ xml_accessor :Content, :cdata => true
181
+
182
+ def initialize
183
+ super
184
+ @MsgType = 'text'
185
+ end
186
+ end
187
+
188
+ class Music
189
+ include ROXML
190
+
191
+ xml_accessor :Title, :cdata => true
192
+ xml_accessor :Description, :cdata => true
193
+ xml_accessor :MusicUrl, :cdata => true
194
+ xml_accessor :HQMusicUrl, :cdata => true
195
+ end
196
+
197
+ class MusicReplyMessage < ReplyMessage
198
+ xml_accessor :Music, :as => Music
199
+
200
+ def initialize
201
+ super
202
+ @MsgType = 'music'
203
+ end
204
+ end
205
+
206
+ class Item
207
+ include ROXML
208
+
209
+ xml_accessor :Title, :cdata => true
210
+ xml_accessor :Description, :cdata => true
211
+ xml_accessor :PicUrl, :cdata => true
212
+ xml_accessor :Url, :cdata => true
213
+ end
214
+
215
+ class NewsReplyMessage < ReplyMessage
216
+ xml_accessor :ArticleCount, :as => Integer
217
+ xml_accessor :Articles, :as => [Item], :in => 'Articles', :from => 'item'
218
+
219
+ def initialize
220
+ super
221
+ @MsgType = 'news'
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,3 @@
1
+ module Weixin
2
+ VERSION = "0.0.4.2"
3
+ end
@@ -0,0 +1,30 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ require "weixin/version"
3
+
4
+ Gem::Specification.new do |s|
5
+
6
+ s.name = 'mp-weixin'
7
+ s.version = Weixin::VERSION
8
+
9
+ s.description = 'Rack middleware for Weixin apps: message validation/parser/generator'
10
+ s.summary = 'Rack middleware for Weixin apps'
11
+
12
+ s.authors = ['wolfg1969']
13
+ s.email = 'wolfg1969@gmail.com'
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = s.files.select {|path| path =~ /^spec\/.*_spec\.rb/}
17
+
18
+ s.add_dependency 'rack'
19
+ s.add_dependency 'multi_json', '>= 1.7.9'
20
+ s.add_dependency 'multi_xml', '>= 0.5.2'
21
+ s.add_dependency 'roxml'
22
+ s.add_dependency 'nestful'
23
+
24
+ s.add_development_dependency 'rake'
25
+ s.add_development_dependency 'rack-test'
26
+ s.add_development_dependency 'rspec', '>= 2.0.0'
27
+
28
+ s.homepage = 'https://github.com/wolfg1969/rack-weixin'
29
+ s.require_paths = %w[lib]
30
+ end
@@ -0,0 +1,104 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'rspec'
3
+ require 'weixin/model'
4
+
5
+ describe 'weixin/model' do
6
+
7
+ context 'Weixin:Message' do
8
+ it 'is a text message' do
9
+ msg = Weixin::Message.factory(%(
10
+ <xml>
11
+ <ToUserName><![CDATA[to]]></ToUserName>
12
+ <FromUserName><![CDATA[from]]></FromUserName>
13
+ <CreateTime>1360391199</CreateTime>
14
+ <MsgType><![CDATA[text]]></MsgType>
15
+ <Content><![CDATA[Hello2BizUser]]></Content>
16
+ <MsgId>5842835709471227904</MsgId>
17
+ </xml>
18
+ ))
19
+ msg.class.should == Weixin::TextMessage
20
+ msg.MsgType.should == 'text'
21
+ msg.ToUserName.should == 'to'
22
+ msg.FromUserName.should == 'from'
23
+ msg.CreateTime.should == 1360391199
24
+ msg.Content.should == 'Hello2BizUser'
25
+ msg.MsgId.should == 5842835709471227904
26
+ end
27
+
28
+ it 'is a image message' do
29
+ msg = Weixin::Message.factory(%(
30
+ <xml>
31
+ <ToUserName><![CDATA[to]]></ToUserName>
32
+ <FromUserName><![CDATA[from]]></FromUserName>
33
+ <CreateTime>1360391199</CreateTime>
34
+ <MsgType><![CDATA[image]]></MsgType>
35
+ <PicUrl><![CDATA[http://mmsns.qpic.cn/mmsns/Leiaa5NQF4FOTOSo3hXrEsGsodU2jHcWZiaInTxmTh6GaCIJ8hBHicIDA/0]]></PicUrl>
36
+ <MsgId>5842835709471227904</MsgId>
37
+ </xml>
38
+ ))
39
+ msg.class.should == Weixin::ImageMessage
40
+ msg.MsgType.should == 'image'
41
+ msg.ToUserName.should == 'to'
42
+ msg.FromUserName.should == 'from'
43
+ msg.CreateTime.should == 1360391199
44
+ msg.MsgId.should == 5842835709471227904
45
+ msg.PicUrl.should_not nil
46
+ end
47
+
48
+ it 'is a location message' do
49
+ msg = Weixin::Message.factory(%(
50
+ <xml>
51
+ <ToUserName><![CDATA[to]]></ToUserName>
52
+ <FromUserName><![CDATA[from]]></FromUserName>
53
+ <CreateTime>1360391199</CreateTime>
54
+ <MsgType><![CDATA[location]]></MsgType>
55
+ <Location_X>69.866013</Location_X>
56
+ <Location_Y>136.269449</Location_Y>
57
+ <Scale>15</Scale>
58
+ <Label><![CDATA[somewhere]]></Label>
59
+ <MsgId>5842835709471227904</MsgId>
60
+ </xml>
61
+ ))
62
+ msg.class.should == Weixin::LocationMessage
63
+ msg.MsgType.should == 'location'
64
+ msg.ToUserName.should == 'to'
65
+ msg.FromUserName.should == 'from'
66
+ msg.CreateTime.should == 1360391199
67
+ msg.MsgId.should == 5842835709471227904
68
+ msg.Label.should_not nil
69
+ msg.Scale.should == 15
70
+ msg.Location_X.should == 69.866013
71
+ msg.Location_Y.should == 136.269449
72
+ end
73
+
74
+ end
75
+
76
+ context 'Weixin::ReplyMessage' do
77
+
78
+ it 'is a text reply message' do
79
+ msg = Weixin::TextReplyMessage.new
80
+ msg.ToUserName = 'to'
81
+ msg.FromUserName = 'from'
82
+ msg.Content = 'blah'
83
+ msg.MsgType.should == 'text'
84
+ end
85
+
86
+ it 'is a news reply message' do
87
+ msg = Weixin::NewsReplyMessage.new
88
+ msg.ToUserName = 'to'
89
+ msg.FromUserName = 'from'
90
+ item1 = Weixin::Item.new
91
+ item1.Title = 'title1'
92
+ item1.Description = 'blah'
93
+ item2 = Weixin::Item.new
94
+ item2.Title = 'title2'
95
+ item2.Description = 'blah blah'
96
+ msg.Articles = [item1, item2]
97
+ msg.ArticleCount = 2
98
+ msg.MsgType.should == 'news'
99
+ #puts msg.to_xml
100
+ end
101
+
102
+ end
103
+
104
+ end
@@ -0,0 +1,55 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'rspec'
3
+ require 'rack'
4
+ require 'rack/test'
5
+ require 'rack/mock'
6
+ require 'digest/sha1'
7
+ require 'weixin/middleware'
8
+
9
+ describe "Weixin::Middleware" do
10
+
11
+ include Rack::Test::Methods
12
+
13
+ before(:all) do
14
+ @app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['hello']] }
15
+ @app_token = 'mytoken'
16
+ @context_path = '/'
17
+ end
18
+
19
+ def middleware()
20
+ Weixin::Middleware.new @app, @app_token, @context_path
21
+ end
22
+
23
+ def mock_env(echostr, token = @app_token, path = '/')
24
+ timestamp = Time.now.to_i.to_s
25
+ nonce = '123456'
26
+ param_array = [token, timestamp, nonce]
27
+ sign = Digest::SHA1.hexdigest( param_array.sort.join )
28
+ url = "#{path}?echostr=#{echostr}&timestamp=#{timestamp}&nonce=#{nonce}&signature=#{sign}"
29
+ Rack::MockRequest.env_for(url)
30
+ end
31
+
32
+ it 'does not match the path info' do
33
+ app = middleware
34
+ echostr = '123'
35
+ status, headers, body = app.call mock_env(echostr, @app_token, '/not_weixin_app')
36
+ status.should eq(200)
37
+ body.should_not == [echostr]
38
+ end
39
+
40
+ it 'is valid weixin request' do
41
+ app = middleware
42
+ echostr = '123'
43
+ status, headers, body = app.call mock_env(echostr)
44
+ status.should eq(200)
45
+ body.should == [echostr]
46
+ end
47
+
48
+ it 'is invalid weixin request' do
49
+ app = middleware
50
+ status, headers, body = app.call mock_env('123', 'wrong_token')
51
+ status.should eq(401)
52
+ body.should == []
53
+ end
54
+
55
+ end
metadata ADDED
@@ -0,0 +1,199 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mp-weixin
3
+ version: !ruby/object:Gem::Version
4
+ hash: 91
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 4
10
+ - 2
11
+ version: 0.0.4.2
12
+ platform: ruby
13
+ authors:
14
+ - wolfg1969
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2014-01-06 00:00:00 +08:00
20
+ default_executable:
21
+ dependencies:
22
+ - !ruby/object:Gem::Dependency
23
+ name: rack
24
+ prerelease: false
25
+ requirement: &id001 !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ hash: 3
31
+ segments:
32
+ - 0
33
+ version: "0"
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: multi_json
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ hash: 25
45
+ segments:
46
+ - 1
47
+ - 7
48
+ - 9
49
+ version: 1.7.9
50
+ type: :runtime
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ name: multi_xml
54
+ prerelease: false
55
+ requirement: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ hash: 15
61
+ segments:
62
+ - 0
63
+ - 5
64
+ - 2
65
+ version: 0.5.2
66
+ type: :runtime
67
+ version_requirements: *id003
68
+ - !ruby/object:Gem::Dependency
69
+ name: roxml
70
+ prerelease: false
71
+ requirement: &id004 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ hash: 3
77
+ segments:
78
+ - 0
79
+ version: "0"
80
+ type: :runtime
81
+ version_requirements: *id004
82
+ - !ruby/object:Gem::Dependency
83
+ name: nestful
84
+ prerelease: false
85
+ requirement: &id005 !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ hash: 3
91
+ segments:
92
+ - 0
93
+ version: "0"
94
+ type: :runtime
95
+ version_requirements: *id005
96
+ - !ruby/object:Gem::Dependency
97
+ name: rake
98
+ prerelease: false
99
+ requirement: &id006 !ruby/object:Gem::Requirement
100
+ none: false
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ hash: 3
105
+ segments:
106
+ - 0
107
+ version: "0"
108
+ type: :development
109
+ version_requirements: *id006
110
+ - !ruby/object:Gem::Dependency
111
+ name: rack-test
112
+ prerelease: false
113
+ requirement: &id007 !ruby/object:Gem::Requirement
114
+ none: false
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ hash: 3
119
+ segments:
120
+ - 0
121
+ version: "0"
122
+ type: :development
123
+ version_requirements: *id007
124
+ - !ruby/object:Gem::Dependency
125
+ name: rspec
126
+ prerelease: false
127
+ requirement: &id008 !ruby/object:Gem::Requirement
128
+ none: false
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ hash: 15
133
+ segments:
134
+ - 2
135
+ - 0
136
+ - 0
137
+ version: 2.0.0
138
+ type: :development
139
+ version_requirements: *id008
140
+ description: "Rack middleware for Weixin apps: message validation/parser/generator"
141
+ email: wolfg1969@gmail.com
142
+ executables: []
143
+
144
+ extensions: []
145
+
146
+ extra_rdoc_files: []
147
+
148
+ files:
149
+ - .gitignore
150
+ - .travis.yml
151
+ - Gemfile
152
+ - Gemfile.lock
153
+ - README.md
154
+ - Rakefile
155
+ - lib/rack-weixin.rb
156
+ - lib/weixin/menu.rb
157
+ - lib/weixin/middleware.rb
158
+ - lib/weixin/model.rb
159
+ - lib/weixin/version.rb
160
+ - rack-weixin.gemspec
161
+ - spec/model_spec.rb
162
+ - spec/rack_middleware_spec.rb
163
+ has_rdoc: true
164
+ homepage: https://github.com/wolfg1969/rack-weixin
165
+ licenses: []
166
+
167
+ post_install_message:
168
+ rdoc_options: []
169
+
170
+ require_paths:
171
+ - lib
172
+ required_ruby_version: !ruby/object:Gem::Requirement
173
+ none: false
174
+ requirements:
175
+ - - ">="
176
+ - !ruby/object:Gem::Version
177
+ hash: 3
178
+ segments:
179
+ - 0
180
+ version: "0"
181
+ required_rubygems_version: !ruby/object:Gem::Requirement
182
+ none: false
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ hash: 3
187
+ segments:
188
+ - 0
189
+ version: "0"
190
+ requirements: []
191
+
192
+ rubyforge_project:
193
+ rubygems_version: 1.3.7
194
+ signing_key:
195
+ specification_version: 3
196
+ summary: Rack middleware for Weixin apps
197
+ test_files:
198
+ - spec/model_spec.rb
199
+ - spec/rack_middleware_spec.rb