mp-weixin 0.0.4.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +46 -0
- data/README.md +113 -0
- data/Rakefile +11 -0
- data/lib/rack-weixin.rb +53 -0
- data/lib/weixin/menu.rb +60 -0
- data/lib/weixin/middleware.rb +62 -0
- data/lib/weixin/model.rb +224 -0
- data/lib/weixin/version.rb +3 -0
- data/rack-weixin.gemspec +30 -0
- data/spec/model_spec.rb +104 -0
- data/spec/rack_middleware_spec.rb +55 -0
- metadata +199 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -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)
|
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
data/lib/rack-weixin.rb
ADDED
@@ -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
|
data/lib/weixin/menu.rb
ADDED
@@ -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
|
+
|
data/lib/weixin/model.rb
ADDED
@@ -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
|
data/rack-weixin.gemspec
ADDED
@@ -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
|
data/spec/model_spec.rb
ADDED
@@ -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}×tamp=#{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
|