sinatra-wechat 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/examples/wechat_example.rb +1 -1
- data/lib/sinatra/version.rb +1 -1
- data/lib/sinatra/wechat.rb +37 -44
- data/sinatra-wechat.gemspec +1 -1
- data/spec/spec_helper.rb +4 -0
- data/spec/wechat_spec.rb +73 -106
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 86233c3037ef8bd2e6d48fa0d321aa6a4279e41f
|
4
|
+
data.tar.gz: 2d2eb697ffa61f47e149a4718c0b21b4dbdb7212
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 97f9ff1ed899844dd1aef81127b765b8b14e783b132e77e594facc8864fdd2a90f2261aaf50f39c9edf75ee147108ad62edfc45d0a2ddce63e73e3be97e47a26
|
7
|
+
data.tar.gz: fe24ff24774e0c9d15c3efc4eb7e284d9704ab6fa5e73f38a65ada3ec45cba63c18b3c6856990b71f654b3aea7ac3c02e9dc7f6212e42ea9fa044fbde3bf1776
|
data/README.md
CHANGED
@@ -12,14 +12,14 @@ This extension is used to support [Tencent Wechat](https://mp.weixin.qq.com/) ra
|
|
12
12
|
# Usage
|
13
13
|
|
14
14
|
Below code implement a simple wechat robot, reply text `你好` when message sent by end user contains number `%r{\d+}`.
|
15
|
-
> use `:
|
15
|
+
> use `:validate_msg => false` to disable wechat message validation, otherwise need to append signature to the URL. The default value of `:validate_msg` is `true`
|
16
16
|
|
17
17
|
```ruby
|
18
18
|
# app.rb
|
19
19
|
require 'sinatra'
|
20
20
|
require 'sinatra/wechat'
|
21
21
|
|
22
|
-
wechat('/', :wechat_token => 'test-token', :
|
22
|
+
wechat('/', :wechat_token => 'test-token', :validate_msg => false) {
|
23
23
|
text(:content => %r{\d+}) {
|
24
24
|
content_type 'application/xml'
|
25
25
|
erb :hello, :locals => request[:wechat_values]
|
data/examples/wechat_example.rb
CHANGED
@@ -17,7 +17,7 @@ location_event_reply = proc {
|
|
17
17
|
builder.to_xml
|
18
18
|
}
|
19
19
|
|
20
|
-
wechat('/wechat', :wechat_token => 'test-token', :
|
20
|
+
wechat('/wechat', :wechat_token => 'test-token', :validate_msg => false) {
|
21
21
|
location {
|
22
22
|
instance_eval &location_event_reply
|
23
23
|
}
|
data/lib/sinatra/version.rb
CHANGED
data/lib/sinatra/wechat.rb
CHANGED
@@ -1,65 +1,65 @@
|
|
1
|
-
|
2
|
-
require 'blankslate'
|
3
|
-
require 'nokogiri'
|
1
|
+
['sinatra/base', 'blankslate', 'nokogiri'].each { |m| require m }
|
4
2
|
|
5
3
|
module Sinatra
|
6
4
|
module Wechat
|
7
|
-
module
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
5
|
+
module Endpoint
|
6
|
+
# Work as a Ruby Builder, treat every Wechat 'MsgType' as method name
|
7
|
+
# the method arguments are the Wechat message it self
|
8
|
+
class DispatcherBuilder < ::BlankSlate
|
9
|
+
def initialize(&block)
|
12
10
|
@message_handlers = {}
|
11
|
+
instance_eval(&block) if block_given?
|
13
12
|
end
|
14
13
|
|
15
|
-
|
14
|
+
# resp_blk is used to generate HTTP response, need to eval in Sinatra context
|
15
|
+
def method_missing(sym, *args, &resp_blk)
|
16
16
|
@message_handlers[sym] ||= []
|
17
|
-
matchers = args.collect do |
|
18
|
-
if
|
17
|
+
matchers = args.collect do |arg|
|
18
|
+
if arg.respond_to?(:call) then lambda &arg
|
19
19
|
# for named parameters
|
20
|
-
elsif
|
20
|
+
elsif arg.respond_to?(:all?) then lambda { |values| arg.all? { |k,v| v === values[k]} }
|
21
21
|
else raise TypeError, "\"#{v} (#{v.class})\" is not an acceptable condition"
|
22
|
-
end
|
22
|
+
end
|
23
23
|
end
|
24
|
-
matcher = lambda
|
25
|
-
|
26
|
-
end
|
27
|
-
@message_handlers[sym] << [ matcher, block ]
|
24
|
+
matcher = lambda { |values| matchers.all? { |m| m.call(values) } }
|
25
|
+
@message_handlers[sym] << [ matcher, resp_blk ]
|
28
26
|
end
|
29
27
|
|
30
|
-
def
|
31
|
-
|
32
|
-
handlers = @message_handlers[
|
33
|
-
|
34
|
-
handler
|
28
|
+
def dispatch!(values)
|
29
|
+
return nil unless msg_type = values[:msg_type]
|
30
|
+
handlers = @message_handlers[msg_type.to_sym] || []
|
31
|
+
handlers.find { |m, _| m.call(values) }
|
35
32
|
end
|
36
33
|
end
|
37
34
|
|
38
|
-
def wechat(endpoint = '/', wechat_token: '',
|
39
|
-
|
40
|
-
|
35
|
+
def wechat(endpoint = '/', wechat_token: '', validate_msg: true, &block)
|
36
|
+
before endpoint do
|
37
|
+
if validate_msg then
|
38
|
+
raw = [wechat_token, params[:timestamp], params[:nonce]].compact.sort.join
|
39
|
+
halt 403 unless Digest::SHA1.hexdigest(raw) == params[:signature]
|
40
|
+
end
|
41
|
+
end
|
41
42
|
|
42
43
|
get endpoint do
|
43
|
-
halt 403 unless validate_messages(wechat_token) if message_validation
|
44
44
|
content_type 'text/plain'
|
45
45
|
params[:echostr]
|
46
46
|
end
|
47
47
|
|
48
|
-
|
49
|
-
halt 403 unless validate_messages(wechat_token) if message_validation
|
48
|
+
dispatcher = DispatcherBuilder.new(&block)
|
50
49
|
|
50
|
+
post endpoint do
|
51
51
|
body = request.body.read || ""
|
52
|
-
halt
|
52
|
+
halt 400 if body.empty? # bad request, cannot handle this kind of message
|
53
53
|
|
54
|
-
|
55
|
-
values =
|
54
|
+
xmldoc = Nokogiri::XML(body).root
|
55
|
+
values = xmldoc.element_children.each_with_object(Hash.new) do |e, v|
|
56
56
|
name = e.name.gsub(/(.)([A-Z])/,'\1_\2').downcase
|
57
57
|
# rename 'Location_X' to 'location__x' then to 'location_x'
|
58
58
|
name = name.gsub(/(_{2,})/,'_')
|
59
59
|
v[name.to_sym] = e.content
|
60
60
|
end
|
61
|
-
handler = dispatcher.
|
62
|
-
halt
|
61
|
+
_, handler = dispatcher.dispatch!(values)
|
62
|
+
halt 404 unless handler
|
63
63
|
|
64
64
|
request[:wechat_values] = values
|
65
65
|
instance_eval(&handler)
|
@@ -68,18 +68,11 @@ module Sinatra
|
|
68
68
|
end
|
69
69
|
end
|
70
70
|
|
71
|
-
def self.registered(
|
72
|
-
|
73
|
-
|
74
|
-
def validate_messages token
|
75
|
-
raw = [token, params[:timestamp], params[:nonce]].compact.sort.join
|
76
|
-
Digest::SHA1.hexdigest(raw) == params[:signature]
|
77
|
-
end
|
78
|
-
end
|
79
|
-
# expose to classic style
|
80
|
-
Delegator.delegate(:wechat)
|
71
|
+
def self.registered(application)
|
72
|
+
application.extend(Wechat::Endpoint)
|
73
|
+
Sinatra::Delegator.delegate(:wechat) # expose wechat method to classic style
|
81
74
|
end
|
82
75
|
end
|
83
76
|
|
84
|
-
register Wechat
|
77
|
+
register Sinatra::Wechat
|
85
78
|
end
|
data/sinatra-wechat.gemspec
CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.authors = ["Lu, Jun"]
|
10
10
|
spec.email = ["luj1985@gmail.com"]
|
11
11
|
spec.summary = "Sinatra extension for Tencent Wechat"
|
12
|
-
spec.description = "Provide
|
12
|
+
spec.description = "Provide extensible Sinatra API to support rapid Wechat development"
|
13
13
|
spec.homepage = "https://github.com/luj1985/sinatra-wechat"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
data/spec/spec_helper.rb
CHANGED
data/spec/wechat_spec.rb
CHANGED
@@ -1,48 +1,36 @@
|
|
1
|
-
|
1
|
+
require_relative 'spec_helper'
|
2
2
|
|
3
3
|
describe Sinatra::Wechat do
|
4
|
-
include Rack::Test::Methods
|
5
4
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
end
|
11
|
-
instance.wechat(:wechat_token => 'test-token') { }
|
12
|
-
end
|
5
|
+
let (:app) { Sinatra.new { register Sinatra::Wechat } }
|
6
|
+
|
7
|
+
it "should have message validation on GET method" do
|
8
|
+
app.wechat(:wechat_token => 'test-token')
|
13
9
|
|
14
10
|
get '/'
|
15
11
|
expect(last_response.status).to eq(403)
|
16
12
|
|
17
|
-
get '/', {
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
13
|
+
get '/', {
|
14
|
+
:timestamp => '201407191804',
|
15
|
+
:nonce => 'nonce',
|
16
|
+
:signature => '9a91a1cea1cb60b87a9abb29dae06dce14721258',
|
17
|
+
:echostr => 'echo string'
|
18
|
+
}
|
19
|
+
expect(last_response).to be_ok
|
22
20
|
expect(last_response.body).to eq('echo string')
|
23
21
|
end
|
24
22
|
|
25
23
|
it "Can disable message validation" do
|
26
|
-
|
27
|
-
instance = Sinatra.new do
|
28
|
-
register Sinatra::Wechat
|
29
|
-
end
|
30
|
-
instance.wechat(:message_validation => false) { }
|
31
|
-
end
|
24
|
+
app.wechat(:validate_msg => false)
|
32
25
|
|
33
|
-
get '/'
|
34
|
-
expect(last_response
|
26
|
+
get '/', { :echostr => 'echo' }
|
27
|
+
expect(last_response).to be_ok
|
28
|
+
expect(last_response.body).to eq('echo')
|
35
29
|
end
|
36
30
|
|
37
|
-
it "
|
38
|
-
|
39
|
-
|
40
|
-
register Sinatra::Wechat
|
41
|
-
end
|
42
|
-
instance.wechat(:wechat_token => 'test-token') {
|
43
|
-
text { 'text response' }
|
44
|
-
}
|
45
|
-
end
|
31
|
+
it "should have message validation on POST method" do
|
32
|
+
app.wechat(:wechat_token => 'test-token') { text { 'text response' } }
|
33
|
+
|
46
34
|
post '/'
|
47
35
|
expect(last_response.status).to eq(403)
|
48
36
|
|
@@ -58,19 +46,13 @@ describe Sinatra::Wechat do
|
|
58
46
|
EOF
|
59
47
|
|
60
48
|
post '/?timestamp=201407191804&nonce=nonce&signature=9a91a1cea1cb60b87a9abb29dae06dce14721258', body
|
61
|
-
expect(last_response
|
49
|
+
expect(last_response).to be_ok
|
62
50
|
expect(last_response.body).to eq('text response')
|
63
51
|
end
|
64
52
|
|
65
53
|
it "can switch wechat endpoint" do
|
66
|
-
|
67
|
-
|
68
|
-
register Sinatra::Wechat
|
69
|
-
end
|
70
|
-
instance.wechat('/wechat', :wechat_token => 'test-token') {
|
71
|
-
image { 'relocated response' }
|
72
|
-
}
|
73
|
-
end
|
54
|
+
app.wechat('/wechat', :wechat_token => 'test-token') { image { 'relocated response' } }
|
55
|
+
|
74
56
|
body = <<-EOF
|
75
57
|
<xml>
|
76
58
|
<ToUserName><![CDATA[toUser]]></ToUserName>
|
@@ -87,30 +69,25 @@ describe Sinatra::Wechat do
|
|
87
69
|
expect(last_response.status).to eq(404)
|
88
70
|
|
89
71
|
post '/wechat?timestamp=201407191804&nonce=nonce&signature=9a91a1cea1cb60b87a9abb29dae06dce14721258', body
|
90
|
-
expect(last_response
|
72
|
+
expect(last_response).to be_ok
|
91
73
|
expect(last_response.body).to eq('relocated response')
|
92
74
|
|
93
75
|
end
|
94
76
|
|
95
77
|
it "should accept wechat message push" do
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
}
|
108
|
-
location {
|
109
|
-
values = request[:wechat_values]
|
110
|
-
values[:label]
|
111
|
-
}
|
78
|
+
app.wechat(:wechat_token => 'test-token') {
|
79
|
+
text(:content => %r{regex match}) { 'regex match' }
|
80
|
+
text(lambda {|values| values[:content] == 'function match'}) { 'function match' }
|
81
|
+
text { 'default match' }
|
82
|
+
voice {
|
83
|
+
values = request[:wechat_values]
|
84
|
+
values[:msg_id]
|
85
|
+
}
|
86
|
+
location {
|
87
|
+
values = request[:wechat_values]
|
88
|
+
values[:label]
|
112
89
|
}
|
113
|
-
|
90
|
+
}
|
114
91
|
|
115
92
|
post '/?timestamp=201407191804&nonce=nonce&signature=9a91a1cea1cb60b87a9abb29dae06dce14721258', <<-EOF
|
116
93
|
<xml>
|
@@ -122,10 +99,9 @@ describe Sinatra::Wechat do
|
|
122
99
|
<MsgId>1234567890123456</MsgId>
|
123
100
|
</xml>
|
124
101
|
EOF
|
125
|
-
expect(last_response
|
102
|
+
expect(last_response).to be_ok
|
126
103
|
expect(last_response.body).to eq('regex match')
|
127
104
|
|
128
|
-
|
129
105
|
post '/?timestamp=201407191804&nonce=nonce&signature=9a91a1cea1cb60b87a9abb29dae06dce14721258', <<-EOF
|
130
106
|
<xml>
|
131
107
|
<ToUserName>tousername</ToUserName>
|
@@ -136,10 +112,9 @@ describe Sinatra::Wechat do
|
|
136
112
|
<MsgId>1234567890123456</MsgId>
|
137
113
|
</xml>
|
138
114
|
EOF
|
139
|
-
expect(last_response
|
115
|
+
expect(last_response).to be_ok
|
140
116
|
expect(last_response.body).to eq('function match')
|
141
117
|
|
142
|
-
|
143
118
|
post '/?timestamp=201407191804&nonce=nonce&signature=9a91a1cea1cb60b87a9abb29dae06dce14721258', <<-EOF
|
144
119
|
<xml>
|
145
120
|
<ToUserName>tousername</ToUserName>
|
@@ -150,10 +125,9 @@ describe Sinatra::Wechat do
|
|
150
125
|
<MsgId>1234567890123456</MsgId>
|
151
126
|
</xml>
|
152
127
|
EOF
|
153
|
-
expect(last_response
|
128
|
+
expect(last_response).to be_ok
|
154
129
|
expect(last_response.body).to eq('default match')
|
155
130
|
|
156
|
-
|
157
131
|
post '/?timestamp=201407191804&nonce=nonce&signature=9a91a1cea1cb60b87a9abb29dae06dce14721258', <<-EOF
|
158
132
|
<xml>
|
159
133
|
<ToUserName><![CDATA[toUser]]></ToUserName>
|
@@ -165,7 +139,7 @@ describe Sinatra::Wechat do
|
|
165
139
|
<MsgId>1234567890123456</MsgId>
|
166
140
|
</xml>
|
167
141
|
EOF
|
168
|
-
expect(last_response
|
142
|
+
expect(last_response).to be_ok
|
169
143
|
expect(last_response.body).to eq('1234567890123456')
|
170
144
|
|
171
145
|
post '/?timestamp=201407191804&nonce=nonce&signature=9a91a1cea1cb60b87a9abb29dae06dce14721258', <<-EOF
|
@@ -181,7 +155,7 @@ describe Sinatra::Wechat do
|
|
181
155
|
<MsgId>1234567890123456</MsgId>
|
182
156
|
</xml>
|
183
157
|
EOF
|
184
|
-
expect(last_response
|
158
|
+
expect(last_response).to be_ok
|
185
159
|
expect(last_response.body).to eq('位置信息')
|
186
160
|
|
187
161
|
post '/?timestamp=201407191804&nonce=nonce&signature=9a91a1cea1cb60b87a9abb29dae06dce14721258', <<-EOF
|
@@ -189,21 +163,16 @@ describe Sinatra::Wechat do
|
|
189
163
|
<MsgType>unknown</MsgType>
|
190
164
|
</xml>
|
191
165
|
EOF
|
192
|
-
expect(last_response.status).to eq(
|
166
|
+
expect(last_response.status).to eq(404)
|
193
167
|
end
|
194
168
|
|
195
169
|
it "should accept complex match" do
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
end
|
200
|
-
instance.wechat(:wechat_token => 'test-token') {
|
201
|
-
future(lambda {|vs| vs[:to_user_name] == 'test' }, :content => %r{future}, :create_time => '1348831860') {
|
202
|
-
'complex match'
|
203
|
-
}
|
204
|
-
range(:to_user_name => 'tesa'..'testz') { 'range match' }
|
170
|
+
app.wechat(:wechat_token => 'test-token') {
|
171
|
+
future(lambda {|vs| vs[:to_user_name] == 'test' }, :content => %r{future}, :create_time => '1348831860') {
|
172
|
+
'complex match'
|
205
173
|
}
|
206
|
-
|
174
|
+
range(:to_user_name => 'tesa'..'testz') { 'range match' }
|
175
|
+
}
|
207
176
|
|
208
177
|
post '/?timestamp=201407191804&nonce=nonce&signature=9a91a1cea1cb60b87a9abb29dae06dce14721258', <<-EOF
|
209
178
|
<xml>
|
@@ -215,51 +184,41 @@ describe Sinatra::Wechat do
|
|
215
184
|
<MsgId>1234567890123456</MsgId>
|
216
185
|
</xml>
|
217
186
|
EOF
|
218
|
-
expect(last_response
|
187
|
+
expect(last_response).to be_ok
|
219
188
|
expect(last_response.body).to eq('complex match')
|
220
189
|
|
221
|
-
|
222
|
-
|
223
190
|
post '/?timestamp=201407191804&nonce=nonce&signature=9a91a1cea1cb60b87a9abb29dae06dce14721258', <<-EOF
|
224
191
|
<xml>
|
225
192
|
<ToUserName>test</ToUserName>
|
226
193
|
<MsgType>range</MsgType>
|
227
194
|
</xml>
|
228
195
|
EOF
|
229
|
-
expect(last_response
|
196
|
+
expect(last_response).to be_ok
|
230
197
|
expect(last_response.body).to eq('range match')
|
231
198
|
end
|
232
199
|
|
233
200
|
it "should raise error when invalid condition set" do
|
234
|
-
instance = Sinatra.new do
|
235
|
-
register Sinatra::Wechat
|
236
|
-
end
|
237
201
|
expect {
|
238
|
-
|
202
|
+
app.wechat(:wechat_token => 'test-token') {
|
239
203
|
future('invalid condition') { 'complex match' }
|
240
204
|
}
|
241
205
|
}.to raise_exception
|
242
206
|
end
|
243
207
|
|
244
208
|
it "can have multiple endpoint" do
|
245
|
-
|
246
|
-
|
247
|
-
|
209
|
+
app.wechat('/wechat1', :wechat_token => 'test-token') {
|
210
|
+
selector = lambda do |values|
|
211
|
+
x = values[:location_x].to_f
|
212
|
+
20 < x && x < 30
|
248
213
|
end
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
}
|
256
|
-
|
257
|
-
text { 'this is another wechat endpoint' }
|
258
|
-
}
|
259
|
-
instance.wechat('/wechat3', :wechat_token => 'unknown', :message_validation => false) {
|
260
|
-
text { 'disable message validation' }
|
261
|
-
}
|
262
|
-
end
|
214
|
+
location(selector) { 'matched location range' }
|
215
|
+
}
|
216
|
+
app.wechat('/wechat2', :wechat_token => 'test') {
|
217
|
+
text { 'this is another wechat endpoint' }
|
218
|
+
}
|
219
|
+
app.wechat('/wechat3', :wechat_token => 'unknown', :validate_msg => false) {
|
220
|
+
text { 'disable message validation' }
|
221
|
+
}
|
263
222
|
|
264
223
|
post '/wechat1?timestamp=201407191804&nonce=nonce&signature=9a91a1cea1cb60b87a9abb29dae06dce14721258', <<-EOF
|
265
224
|
<xml>
|
@@ -269,16 +228,14 @@ describe Sinatra::Wechat do
|
|
269
228
|
<Scale>20</Scale>
|
270
229
|
</xml>
|
271
230
|
EOF
|
272
|
-
expect(last_response
|
231
|
+
expect(last_response).to be_ok
|
273
232
|
expect(last_response.body).to eq('matched location range')
|
274
233
|
|
275
|
-
|
276
234
|
post '/wechat2?timestamp=201407191804&nonce=nonce&signature=9a91a1cea1cb60b87a9abb29dae06dce14721258', '<xml><MsgType>text</MsgType></xml>'
|
277
235
|
expect(last_response.status).to eq(403)
|
278
236
|
|
279
|
-
|
280
237
|
post '/wechat2?timestamp=201407191804&nonce=nonce&signature=8149d14c72f418819b1eaab851aeab2c308f15cc', '<xml><MsgType>text</MsgType></xml>'
|
281
|
-
expect(last_response
|
238
|
+
expect(last_response).to be_ok
|
282
239
|
expect(last_response.body).to eq('this is another wechat endpoint')
|
283
240
|
|
284
241
|
get '/wechat3?echostr=return'
|
@@ -287,6 +244,16 @@ describe Sinatra::Wechat do
|
|
287
244
|
|
288
245
|
post '/wechat3', '<xml><MsgType>text</MsgType></xml>'
|
289
246
|
expect(last_response.body).to eq('disable message validation')
|
247
|
+
end
|
248
|
+
|
249
|
+
it "can handle bad formatted xml" do
|
250
|
+
app.wechat(:wechat_token => 'test-token') { text { 'bare' } }
|
290
251
|
|
252
|
+
post '/?timestamp=201407191804&nonce=nonce&signature=9a91a1cea1cb60b87a9abb29dae06dce14721258', <<-EOF
|
253
|
+
<xml>
|
254
|
+
<invalid>message</invalid>>
|
255
|
+
</xml>
|
256
|
+
EOF
|
257
|
+
expect(last_response.status).to eq(404)
|
291
258
|
end
|
292
259
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sinatra-wechat
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lu, Jun
|
@@ -122,7 +122,7 @@ dependencies:
|
|
122
122
|
- - ">="
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '0'
|
125
|
-
description: Provide
|
125
|
+
description: Provide extensible Sinatra API to support rapid Wechat development
|
126
126
|
email:
|
127
127
|
- luj1985@gmail.com
|
128
128
|
executables: []
|