wechat-test 0.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +307 -0
- data/Rakefile +29 -0
- data/bin/wechat +144 -0
- data/lib/wechat-rails.rb +51 -0
- data/lib/wechat.rb +128 -0
- data/lib/wechat/access_token.rb +35 -0
- data/lib/wechat/api.rb +72 -0
- data/lib/wechat/client.rb +74 -0
- data/lib/wechat/message.rb +171 -0
- data/lib/wechat/responder.rb +110 -0
- metadata +110 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0550b9633270bc3cdd6d20e28c8cc543348c1618
|
4
|
+
data.tar.gz: 9b1095f0759ee293d7a88d6321e69ccfa0d075dd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 233d513958afed9c3842e9c2c1d0b339386e865f31461a2e9b1a5491f3448a301fbf8ac65f93f092e273e720e1cd98b5d089ed114d3bc6844faed734e7c30c58
|
7
|
+
data.tar.gz: 1e5e8d16b924c97d364d7dff1faa210f9203583b3066a66efdc98e144954c2c6d7fce2c0673a6e5d4d120a4cfbfc5ade69ade164d99e9e76dde35b643a6086fa
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2014 skinnyworm
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,307 @@
|
|
1
|
+
Wechat Rails
|
2
|
+
======================
|
3
|
+
|
4
|
+
[](https://travis-ci.org/skinnyworm/wechat-rails) [](https://codeclimate.com/github/skinnyworm/wechat-rails) [](https://codeclimate.com/github/skinnyworm/wechat-rails) [](http://badge.fury.io/rb/wechat-rails)
|
5
|
+
|
6
|
+
|
7
|
+
Wechat-rails 可以帮助开发者方便地在Rails环境中集成微信公众平台提供的所有服务,目前微信公众平台提供了以下几种类型的服务。
|
8
|
+
|
9
|
+
- ##### 微信公众平台基本API, 无需Web环境。
|
10
|
+
- ##### 消息处理机制, 需运行在Web环境中。
|
11
|
+
- ##### OAuth 2.0认证机制
|
12
|
+
|
13
|
+
Wechat-rails gem 包含了一个命令行程序可以调用各种无需web环境的API。同时它也提供了Rails Controller的responder DSL, 可以帮助开发者方便地在Rails应用中集成微信的消息处理机制。如果你的App还需要集成微信OAuth2.0, 你可以考虑[omniauth-wechat-oauth2](https://github.com/skinnyworm/omniauth-wechat-oauth2), 这个gem可以方便地和devise集成提供完整的用户认证.
|
14
|
+
|
15
|
+
在使用这个Gem前,你需要获得微信API的appid, secret, token。具体情况可以参见http://mp.weixin.qq.com
|
16
|
+
|
17
|
+
## 安装
|
18
|
+
|
19
|
+
Using `gem install` or add to your app's `Gemfile`:
|
20
|
+
|
21
|
+
```
|
22
|
+
gem install "wechat-rails"
|
23
|
+
```
|
24
|
+
|
25
|
+
```
|
26
|
+
gem "wechat-rails", git:"https://github.com/skinnyworm/wechat-rails"
|
27
|
+
```
|
28
|
+
|
29
|
+
|
30
|
+
## 配置
|
31
|
+
|
32
|
+
#### 命令行程序的配置
|
33
|
+
|
34
|
+
要使用命令行程序,你需要在你的home目录中创建一个`~/.wechat.yml`,包含以下内容。其中`access_token`是存放access_token的文件位置。
|
35
|
+
|
36
|
+
```
|
37
|
+
appid: "my_appid"
|
38
|
+
secret: "my_secret"
|
39
|
+
access_token: "/var/tmp/wechat_access_token"
|
40
|
+
```
|
41
|
+
|
42
|
+
#### Rails 全局配置
|
43
|
+
Rails环境中, 你可以在config中创建wechat.yml, 为每个rails environment创建不同的配置。
|
44
|
+
|
45
|
+
```
|
46
|
+
default: &default
|
47
|
+
appid: "app_id"
|
48
|
+
secret: "app_secret"
|
49
|
+
token: "app_token"
|
50
|
+
access_token: "/var/tmp/wechat_access_token"
|
51
|
+
|
52
|
+
production:
|
53
|
+
appid: <%= ENV['WECHAT_APPID'] %>
|
54
|
+
secret: <%= ENV['WECHAT_APP_SECRET'] %>
|
55
|
+
token: <%= ENV['WECHAT_TOKEN'] %>
|
56
|
+
access_token: <%= ENV['WECHAT_ACCESS_TOKEN'] %>
|
57
|
+
|
58
|
+
staging:
|
59
|
+
<<: *default
|
60
|
+
|
61
|
+
development:
|
62
|
+
<<: *default
|
63
|
+
|
64
|
+
test:
|
65
|
+
<<: *default
|
66
|
+
```
|
67
|
+
|
68
|
+
#### Rails 为每个Responder配置不同的appid和secret
|
69
|
+
在个别情况下,你的app可能需要处理来自多个公众账号的消息,这时你可以配置多个responder controller。
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
class WechatFirstController < ApplicationController
|
73
|
+
wechat_responder appid: "app1", secret: "secret1", token: "token1", access_token: Rails.root.join("tmp/access_token1")
|
74
|
+
|
75
|
+
on :text, with:"help", respond: "help content"
|
76
|
+
end
|
77
|
+
```
|
78
|
+
|
79
|
+
## 使用命令行
|
80
|
+
|
81
|
+
```
|
82
|
+
$ wechat
|
83
|
+
Wechat commands:
|
84
|
+
wechat custom_image [OPENID, IMAGE_PATH] # 发送图片客服消息
|
85
|
+
wechat custom_music [OPENID, THUMBNAIL_PATH, MUSIC_URL] # 发送音乐客服消息
|
86
|
+
wechat custom_news [OPENID, NEWS_YAML_FILE] # 发送图文客服消息
|
87
|
+
wechat custom_text [OPENID, TEXT_MESSAGE] # 发送文字客服消息
|
88
|
+
wechat custom_video [OPENID, VIDEO_PATH] # 发送视频客服消息
|
89
|
+
wechat custom_voice [OPENID, VOICE_PATH] # 发送语音客服消息
|
90
|
+
wechat template_message [OPENID, TEMPLATE_YAML_FILE] # 发送模板消息
|
91
|
+
wechat help [COMMAND] # Describe available commands or one specific command
|
92
|
+
wechat media [MEDIA_ID, PATH] # 媒体下载
|
93
|
+
wechat media_create [MEDIA_ID, PATH] # 媒体上传
|
94
|
+
wechat menu # 当前菜单
|
95
|
+
wechat menu_create [MENU_YAML] # 创建菜单
|
96
|
+
wechat menu_delete # 删除菜单
|
97
|
+
wechat user [OPEN_ID] # 查找关注者
|
98
|
+
wechat users # 关注者列表
|
99
|
+
|
100
|
+
```
|
101
|
+
|
102
|
+
### 使用场景
|
103
|
+
以下是几种典型场景的使用方法
|
104
|
+
|
105
|
+
#####获取所有用户的OPENID
|
106
|
+
|
107
|
+
```
|
108
|
+
$ wechat users
|
109
|
+
|
110
|
+
{"total"=>4, "count"=>4, "data"=>{"openid"=>["oCfEht9***********", "oCfEhtwqa***********", "oCfEht9oMCqGo***********", "oCfEht_81H5o2***********"]}, "next_openid"=>"oCfEht_81H5o2***********"}
|
111
|
+
|
112
|
+
```
|
113
|
+
|
114
|
+
#####获取用户的信息
|
115
|
+
|
116
|
+
```
|
117
|
+
$ wechat user "oCfEht9***********"
|
118
|
+
|
119
|
+
{"subscribe"=>1, "openid"=>"oCfEht9***********", "nickname"=>"Nickname", "sex"=>1, "language"=>"zh_CN", "city"=>"徐汇", "province"=>"上海", "country"=>"中国", "headimgurl"=>"http://wx.qlogo.cn/mmopen/ajNVdqHZLLBd0SG8NjV3UpXZuiaGGPDcaKHebTKiaTyof*********/0", "subscribe_time"=>1395715239}
|
120
|
+
|
121
|
+
```
|
122
|
+
|
123
|
+
#####获取用户的信息
|
124
|
+
|
125
|
+
```
|
126
|
+
$ wechat user "oCfEht9***********"
|
127
|
+
|
128
|
+
{"subscribe"=>1, "openid"=>"oCfEht9***********", "nickname"=>"Nickname", "sex"=>1, "language"=>"zh_CN", "city"=>"徐汇", "province"=>"上海", "country"=>"中国", "headimgurl"=>"http://wx.qlogo.cn/mmopen/ajNVdqHZLLBd0SG8NjV3UpXZuiaGGPDcaKHebTKiaTyof*********/0", "subscribe_time"=>1395715239}
|
129
|
+
```
|
130
|
+
|
131
|
+
##### 获取当前菜单
|
132
|
+
```
|
133
|
+
$ wechat menu
|
134
|
+
|
135
|
+
{"menu"=>{"button"=>[{"type"=>"view", "name"=>"保护的", "url"=>"http://***/protected", "sub_button"=>[]}, {"type"=>"view", "name"=>"公开的", "url"=>"http://***", "sub_button"=>[]}]}}
|
136
|
+
|
137
|
+
```
|
138
|
+
|
139
|
+
##### 创建菜单
|
140
|
+
创建菜单需要一个定义菜单内容的yaml文件,比如
|
141
|
+
menu.yaml
|
142
|
+
|
143
|
+
```
|
144
|
+
button:
|
145
|
+
-
|
146
|
+
type: "view"
|
147
|
+
name: "保护的"
|
148
|
+
url: "http://***/protected"
|
149
|
+
-
|
150
|
+
type: "view"
|
151
|
+
name: "公开的"
|
152
|
+
url: "http://***"
|
153
|
+
|
154
|
+
```
|
155
|
+
|
156
|
+
然后执行命令行
|
157
|
+
|
158
|
+
```
|
159
|
+
$ wechat menu_create menu.yaml
|
160
|
+
|
161
|
+
```
|
162
|
+
|
163
|
+
##### 发送客服图文消息
|
164
|
+
需定义一个图文消息内容的yaml文件,比如
|
165
|
+
articles.yaml
|
166
|
+
|
167
|
+
```
|
168
|
+
articles:
|
169
|
+
-
|
170
|
+
title: "习近平在布鲁日欧洲学院演讲"
|
171
|
+
description: "新华网比利时布鲁日4月1日电 国家主席习近平1日在比利时布鲁日欧洲学院发表重要演讲"
|
172
|
+
url: "http://news.sina.com.cn/c/2014-04-01/232629843387.shtml"
|
173
|
+
pic_url: "http://i3.sinaimg.cn/dy/c/2014-04-01/1396366518_bYays1.jpg"
|
174
|
+
```
|
175
|
+
|
176
|
+
然后执行命令行
|
177
|
+
|
178
|
+
```
|
179
|
+
$ wechat custom_news oCfEht9oM*********** articles.yml
|
180
|
+
|
181
|
+
```
|
182
|
+
|
183
|
+
##### 发送模板消息
|
184
|
+
需定义一个模板消息内容的yaml文件,比如
|
185
|
+
template.yml
|
186
|
+
|
187
|
+
```
|
188
|
+
template:
|
189
|
+
template_id: "o64KQ62_xxxxxxxxxxxxxxx-Qz-MlNcRKteq8"
|
190
|
+
url: "http://weixin.qq.com/download"
|
191
|
+
topcolor: "#FF0000"
|
192
|
+
data:
|
193
|
+
first:
|
194
|
+
value: "你好,你已报名成功"
|
195
|
+
color: "#0A0A0A"
|
196
|
+
keynote1:
|
197
|
+
value: "XX活动"
|
198
|
+
color: "#CCCCCC"
|
199
|
+
keynote2:
|
200
|
+
value: "2014年9月16日"
|
201
|
+
color: "#CCCCCC"
|
202
|
+
keynote3:
|
203
|
+
value: "上海徐家汇xxx城"
|
204
|
+
color: "#CCCCCC"
|
205
|
+
remark:
|
206
|
+
value: "欢迎再次使用。"
|
207
|
+
color: "#173177"
|
208
|
+
|
209
|
+
```
|
210
|
+
|
211
|
+
然后执行命令行
|
212
|
+
|
213
|
+
```
|
214
|
+
$ wechat template_message oCfEht9oM*********** template.yml
|
215
|
+
|
216
|
+
```
|
217
|
+
|
218
|
+
## Rails Responder Controller DSL
|
219
|
+
|
220
|
+
为了在Rails app中响应用户的消息,开发者需要创建一个wechat responder controller. 首先在router中定义
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
resource :wechat, only:[:show, :create]
|
224
|
+
|
225
|
+
```
|
226
|
+
|
227
|
+
然后创建Controller class, 例如
|
228
|
+
|
229
|
+
```ruby
|
230
|
+
|
231
|
+
class WechatsController < ApplicationController
|
232
|
+
wechat_responder
|
233
|
+
|
234
|
+
# 默认的文字信息responder
|
235
|
+
on :text do |request, content|
|
236
|
+
request.reply.text "echo: #{content}" #Just echo
|
237
|
+
end
|
238
|
+
|
239
|
+
# 当请求的文字信息内容为'help'时, 使用这个responder处理
|
240
|
+
on :text, with:"help" do |request, help|
|
241
|
+
request.reply.text "help content" #回复帮助信息
|
242
|
+
end
|
243
|
+
|
244
|
+
# 当请求的文字信息内容为'<n>条新闻'时, 使用这个responder处理, 并将n作为第二个参数
|
245
|
+
on :text, with: /^(\d+)条新闻$/ do |request, count|
|
246
|
+
articles_range = (0... [count.to_i, 10].min)
|
247
|
+
request.reply.news(articles_range) do |article, i| #回复"articles"
|
248
|
+
article.item title: "标题#{i}", description:"内容描述#{i}", pic_url: "http://www.baidu.com/img/bdlogo.gif", url:"http://www.baidu.com/"
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# 处理图片信息
|
253
|
+
on :image do |request|
|
254
|
+
request.reply.image(request[:MediaId]) #直接将图片返回给用户
|
255
|
+
end
|
256
|
+
|
257
|
+
# 处理语音信息
|
258
|
+
on :voice do |request|
|
259
|
+
request.reply.voice(request[:MediaId]) #直接语音音返回给用户
|
260
|
+
end
|
261
|
+
|
262
|
+
# 处理视频信息
|
263
|
+
on :video do |request|
|
264
|
+
nickname = wechat.user(request[:FromUserName])["nickname"] #调用 api 获得发送者的nickname
|
265
|
+
request.reply.video(request[:MediaId], title: "回声", description: "#{nickname}发来的视频请求") #直接视频返回给用户
|
266
|
+
end
|
267
|
+
|
268
|
+
# 处理地理位置信息
|
269
|
+
on :location do |request|
|
270
|
+
request.reply.text("#{request[:Location_X]}, #{request[:Location_Y]}") #回复地理位置
|
271
|
+
end
|
272
|
+
|
273
|
+
# 当无任何responder处理用户信息时,使用这个responder处理
|
274
|
+
on :fallback, respond: "fallback message"
|
275
|
+
end
|
276
|
+
|
277
|
+
```
|
278
|
+
|
279
|
+
在controller中使用`wechat_responder`引入Responder DSL, 之后可以用
|
280
|
+
|
281
|
+
```
|
282
|
+
on <message_type> do |message|
|
283
|
+
message.reply.text "some text"
|
284
|
+
end
|
285
|
+
|
286
|
+
```
|
287
|
+
来响应用户信息。
|
288
|
+
|
289
|
+
目前支持的message_type有如下几种
|
290
|
+
|
291
|
+
- :text 响应文字消息,可以用`:with`参数来匹配文本内容 `on(:text, with:'help'){|message, content| ...}`
|
292
|
+
- :image 响应图片消息
|
293
|
+
- :voice 响应语音消息
|
294
|
+
- :video 响应视频消息
|
295
|
+
- :location 响应地理位置消息
|
296
|
+
- :link 响应链接消息
|
297
|
+
- :event 响应事件消息, 可以用`:with`参数来匹配事件类型
|
298
|
+
- :fallback 默认响应,当收到的消息无法被其他responder响应时,会使用这个responder.
|
299
|
+
|
300
|
+
## Message DSL
|
301
|
+
|
302
|
+
Wechat-rails 的核心是一个Message DSL,帮助开发者构建各种类型的消息,包括主动推送的和被动响应的。
|
303
|
+
....
|
304
|
+
|
305
|
+
|
306
|
+
|
307
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'rdoc/task'
|
9
|
+
rescue LoadError
|
10
|
+
require 'rdoc/rdoc'
|
11
|
+
require 'rake/rdoctask'
|
12
|
+
RDoc::Task = Rake::RDocTask
|
13
|
+
end
|
14
|
+
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
+
rdoc.rdoc_dir = 'rdoc'
|
17
|
+
rdoc.title = 'WechatRails'
|
18
|
+
rdoc.options << '--line-numbers'
|
19
|
+
rdoc.rdoc_files.include('README.rdoc')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
require File.join('bundler', 'gem_tasks')
|
25
|
+
require File.join('rspec', 'core', 'rake_task')
|
26
|
+
RSpec::Core::RakeTask.new(:spec)
|
27
|
+
|
28
|
+
|
29
|
+
task :default => :spec
|
data/bin/wechat
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
lib = File.expand_path(File.dirname(__FILE__) + '/../lib')
|
4
|
+
$LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
|
5
|
+
|
6
|
+
require 'thor'
|
7
|
+
require "wechat-rails"
|
8
|
+
require 'json'
|
9
|
+
require "active_support/core_ext"
|
10
|
+
require 'fileutils'
|
11
|
+
require 'yaml'
|
12
|
+
|
13
|
+
|
14
|
+
class App < Thor
|
15
|
+
class Helper
|
16
|
+
def self.with(options)
|
17
|
+
config_file = File.join(Dir.home, ".wechat.yml")
|
18
|
+
config = YAML.load(File.new(config_file).read) if File.exist?(config_file)
|
19
|
+
|
20
|
+
config ||= {}
|
21
|
+
appid = config["appid"]
|
22
|
+
secret = config["secret"]
|
23
|
+
token_file = options[:toke_file] || config["access_token"] || "/var/tmp/wechat_access_token"
|
24
|
+
|
25
|
+
if (appid.nil? || secret.nil? || token_file.nil?)
|
26
|
+
puts <<-HELP
|
27
|
+
You need create ~/.wechat.yml with wechat appid and secret. For example:
|
28
|
+
|
29
|
+
appid: <wechat appid>
|
30
|
+
secret: <wechat secret>
|
31
|
+
access_toke: "/var/tmp/wechat_access_token"
|
32
|
+
|
33
|
+
HELP
|
34
|
+
exit 1
|
35
|
+
end
|
36
|
+
Wechat::Api.new(appid, secret, token_file)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
package_name "Wechat"
|
41
|
+
option :toke_file, :aliases=>"-t", :desc => "File to store access token"
|
42
|
+
|
43
|
+
desc "users", "关注者列表"
|
44
|
+
def users
|
45
|
+
puts Helper.with(options).users
|
46
|
+
end
|
47
|
+
|
48
|
+
desc "user [OPEN_ID]", "查找关注者"
|
49
|
+
def user(open_id)
|
50
|
+
puts Helper.with(options).user(open_id)
|
51
|
+
end
|
52
|
+
|
53
|
+
desc "menu", "当前菜单"
|
54
|
+
def menu
|
55
|
+
puts Helper.with(options).menu
|
56
|
+
end
|
57
|
+
|
58
|
+
desc "menu_delete", "删除菜单"
|
59
|
+
def menu_delete
|
60
|
+
puts "Menu deleted" if Helper.with(options).menu_delete
|
61
|
+
end
|
62
|
+
|
63
|
+
desc "menu_create [MENU_YAML]", "创建菜单"
|
64
|
+
def menu_create(menu_yaml)
|
65
|
+
menu = YAML.load(File.new(menu_yaml).read)
|
66
|
+
puts "Menu created" if Helper.with(options).menu_create(menu)
|
67
|
+
end
|
68
|
+
|
69
|
+
desc "media [MEDIA_ID, PATH]", "媒体下载"
|
70
|
+
def media(media_id, path)
|
71
|
+
tmp_file = Helper.with(options).media(media_id)
|
72
|
+
FileUtils.mv(tmp_file.path, path)
|
73
|
+
puts "File downloaded"
|
74
|
+
end
|
75
|
+
|
76
|
+
desc "media_create [MEDIA_ID, PATH]", "媒体上传"
|
77
|
+
def media_create(type, path)
|
78
|
+
file = File.new(path)
|
79
|
+
puts Helper.with(options).media_create(type, file)
|
80
|
+
end
|
81
|
+
|
82
|
+
desc "custom_text [OPENID, TEXT_MESSAGE]", "发送文字客服消息"
|
83
|
+
def custom_text openid, text_message
|
84
|
+
puts Helper.with(options).custom_message_send Wechat::Message.to(openid).text(text_message)
|
85
|
+
end
|
86
|
+
|
87
|
+
desc "custom_image [OPENID, IMAGE_PATH]", "发送图片客服消息"
|
88
|
+
def custom_image openid, image_path
|
89
|
+
file = File.new(image_path)
|
90
|
+
api = Helper.with(options)
|
91
|
+
|
92
|
+
media_id = api.media_create("image", file)["media_id"]
|
93
|
+
puts api.custom_message_send Wechat::Message.to(openid).image(media_id)
|
94
|
+
end
|
95
|
+
|
96
|
+
desc "custom_voice [OPENID, VOICE_PATH]", "发送语音客服消息"
|
97
|
+
def custom_voice openid, voice_path
|
98
|
+
file = File.new(voice_path)
|
99
|
+
api = Helper.with(options)
|
100
|
+
|
101
|
+
media_id = api.media_create("voice", file)["media_id"]
|
102
|
+
puts api.custom_message_send Wechat::Message.to(openid).voice(media_id)
|
103
|
+
end
|
104
|
+
|
105
|
+
desc "custom_video [OPENID, VIDEO_PATH]", "发送视频客服消息"
|
106
|
+
method_option :title, :aliases => "-h", :desc => "视频标题"
|
107
|
+
method_option :description, :aliases => "-d", :desc => "视频描述"
|
108
|
+
def custom_video openid, video_path
|
109
|
+
file = File.new(video_path)
|
110
|
+
api = Helper.with(options)
|
111
|
+
|
112
|
+
api_opts = options.slice(:title, :description)
|
113
|
+
media_id = api.media_create("video", file)["media_id"]
|
114
|
+
puts api.custom_message_send Wechat::Message.to(openid).video(media_id, api_opts)
|
115
|
+
end
|
116
|
+
|
117
|
+
desc "custom_music [OPENID, THUMBNAIL_PATH, MUSIC_URL]", "发送音乐客服消息"
|
118
|
+
method_option :title, :aliases => "-h", :desc => "音乐标题"
|
119
|
+
method_option :description, :aliases => "-d", :desc => "音乐描述"
|
120
|
+
method_option :HQ_music_url, :aliases => "-u", :desc => "高质量音乐URL链接"
|
121
|
+
def custom_music openid, thumbnail_path, music_url
|
122
|
+
file = File.new(thumbnail_path)
|
123
|
+
api = Helper.with(options)
|
124
|
+
|
125
|
+
api_opts = options.slice(:title, :description, :HQ_music_url)
|
126
|
+
thumb_media_id = api.media_create("thumb", file)["thumb_media_id"]
|
127
|
+
puts api.custom_message_send Wechat::Message.to(openid).music(thumb_media_id, music_url, api_opts)
|
128
|
+
end
|
129
|
+
|
130
|
+
desc "custom_news [OPENID, NEWS_YAML_FILE]", "发送图文客服消息"
|
131
|
+
def custom_news openid, news_yaml
|
132
|
+
articles = YAML.load(File.new(news_yaml).read)
|
133
|
+
puts Helper.with(options).custom_message_send Wechat::Message.to(openid).news(articles["articles"])
|
134
|
+
end
|
135
|
+
|
136
|
+
desc "template_message [OPENID, TEMPLATE_YAML_FILE]", "模板消息接口"
|
137
|
+
def template_message openid, template_yaml
|
138
|
+
template = YAML.load(File.new(template_yaml).read)
|
139
|
+
puts Helper.with(options).template_message_send Wechat::Message.to(openid).template(template["template"])
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
|
144
|
+
App.start
|
data/lib/wechat-rails.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require "wechat/api"
|
2
|
+
|
3
|
+
module Wechat
|
4
|
+
autoload :Message, "wechat/message"
|
5
|
+
autoload :Responder, "wechat/responder"
|
6
|
+
autoload :Response, "wechat/response"
|
7
|
+
|
8
|
+
class AccessTokenExpiredError < StandardError; end
|
9
|
+
class ResponseError < StandardError
|
10
|
+
attr_reader :error_code
|
11
|
+
def initialize(errcode, errmsg)
|
12
|
+
error_code = errcode
|
13
|
+
super "#{errmsg}(#{error_code})"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :config
|
18
|
+
|
19
|
+
def self.config
|
20
|
+
@config ||= begin
|
21
|
+
if defined? Rails
|
22
|
+
config_file = Rails.root.join("config/wechat.yml")
|
23
|
+
config = YAML.load(ERB.new(File.new(config_file).read).result)[Rails.env] if (File.exist?(config_file))
|
24
|
+
end
|
25
|
+
|
26
|
+
config ||= {appid: ENV["WECHAT_APPID"], secret: ENV["WECHAT_SECRET"], token: ENV["WECHAT_TOKEN"], access_token: ENV["WECHAT_ACCESS_TOKEN"]}
|
27
|
+
config.symbolize_keys!
|
28
|
+
config[:access_token] ||= Rails.root.join("tmp/access_token").to_s
|
29
|
+
OpenStruct.new(config)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.api
|
34
|
+
@api ||= Wechat::Api.new(self.config.appid, self.config.secret, self.config.access_token)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
if defined? ActionController::Base
|
39
|
+
class ActionController::Base
|
40
|
+
def self.wechat_responder opts={}
|
41
|
+
self.send(:include, Wechat::Responder)
|
42
|
+
if (opts.empty?)
|
43
|
+
self.wechat = Wechat.api
|
44
|
+
self.token = Wechat.config.token
|
45
|
+
else
|
46
|
+
self.wechat = Wechat::Api.new(opts[:appid], opts[:secret], opts[:access_token])
|
47
|
+
self.token = opts[:token]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/wechat.rb
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
|
2
|
+
require 'thor'
|
3
|
+
require 'json'
|
4
|
+
require "active_support/core_ext"
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
require_relative "wechat-rails"
|
8
|
+
|
9
|
+
|
10
|
+
class App < Thor
|
11
|
+
class Helper
|
12
|
+
def self.with(options)
|
13
|
+
appid = Wechat.config.appid
|
14
|
+
secret = Wechat.config.secret
|
15
|
+
token_file = options[:toke_file] || Wechat.config.access_token
|
16
|
+
|
17
|
+
if (appid.nil? || secret.nil? || token_file.nil?)
|
18
|
+
puts <<-HELP
|
19
|
+
You need create ~/.wechat.yml with wechat appid and secret. For example:
|
20
|
+
|
21
|
+
appid: <wechat appid>
|
22
|
+
secret: <wechat secret>
|
23
|
+
access_toke: "/var/tmp/wechat_access_token"
|
24
|
+
|
25
|
+
HELP
|
26
|
+
exit 1
|
27
|
+
end
|
28
|
+
Wechat::Api.new(appid, secret, token_file)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
package_name "Wechat"
|
33
|
+
option :toke_file, :aliases=>"-t", :desc => "File to store access token"
|
34
|
+
|
35
|
+
desc "users", "关注者列表"
|
36
|
+
def users
|
37
|
+
puts Helper.with(options).users
|
38
|
+
end
|
39
|
+
|
40
|
+
desc "user [OPEN_ID]", "查找关注者"
|
41
|
+
def user(open_id)
|
42
|
+
puts Helper.with(options).user(open_id)
|
43
|
+
end
|
44
|
+
|
45
|
+
desc "menu", "当前菜单"
|
46
|
+
def menu
|
47
|
+
puts Helper.with(options).menu
|
48
|
+
end
|
49
|
+
|
50
|
+
desc "menu_delete", "删除菜单"
|
51
|
+
def menu_delete
|
52
|
+
puts "Menu deleted" if Helper.with(options).menu_delete
|
53
|
+
end
|
54
|
+
|
55
|
+
desc "menu_create [MENU_YAML]", "创建菜单"
|
56
|
+
def menu_create(menu_yaml)
|
57
|
+
menu = YAML.load(File.new(menu_yaml).read)
|
58
|
+
puts "Menu created" if Helper.with(options).menu_create(menu)
|
59
|
+
end
|
60
|
+
|
61
|
+
desc "media [MEDIA_ID, PATH]", "媒体下载"
|
62
|
+
def media(media_id, path)
|
63
|
+
tmp_file = Helper.with(options).media(media_id)
|
64
|
+
FileUtils.mv(tmp_file.path, path)
|
65
|
+
puts "File downloaded"
|
66
|
+
end
|
67
|
+
|
68
|
+
desc "media_create [MEDIA_ID, PATH]", "媒体上传"
|
69
|
+
def media_create(type, path)
|
70
|
+
file = File.new(path)
|
71
|
+
puts Helper.with(options).media_create(type, file)
|
72
|
+
end
|
73
|
+
|
74
|
+
desc "custom_text [OPENID, TEXT_MESSAGE]", "发送文字客服消息"
|
75
|
+
def custom_text openid, text_message
|
76
|
+
puts Helper.with(options).custom_message_send Wechat::Message.to(openid).text(text_message)
|
77
|
+
end
|
78
|
+
|
79
|
+
desc "custom_image [OPENID, IMAGE_PATH]", "发送图片客服消息"
|
80
|
+
def custom_image openid, image_path
|
81
|
+
file = File.new(image_path)
|
82
|
+
api = Helper.with(options)
|
83
|
+
|
84
|
+
media_id = api.media_create("image", file)["media_id"]
|
85
|
+
puts api.custom_message_send Wechat::Message.to(openid).image(media_id)
|
86
|
+
end
|
87
|
+
|
88
|
+
desc "custom_voice [OPENID, VOICE_PATH]", "发送语音客服消息"
|
89
|
+
def custom_voice openid, voice_path
|
90
|
+
file = File.new(voice_path)
|
91
|
+
api = Helper.with(options)
|
92
|
+
|
93
|
+
media_id = api.media_create("voice", file)["media_id"]
|
94
|
+
puts api.custom_message_send Wechat::Message.to(openid).voice(media_id)
|
95
|
+
end
|
96
|
+
|
97
|
+
desc "custom_video [OPENID, VIDEO_PATH]", "发送视频客服消息"
|
98
|
+
method_option :title, :aliases => "-h", :desc => "视频标题"
|
99
|
+
method_option :description, :aliases => "-d", :desc => "视频描述"
|
100
|
+
def custom_video openid, video_path
|
101
|
+
file = File.new(video_path)
|
102
|
+
api = Helper.with(options)
|
103
|
+
|
104
|
+
api_opts = options.slice(:title, :description)
|
105
|
+
media_id = api.media_create("video", file)["media_id"]
|
106
|
+
puts api.custom_message_send Wechat::Message.to(openid).video(media_id, api_opts)
|
107
|
+
end
|
108
|
+
|
109
|
+
desc "custom_music [OPENID, THUMBNAIL_PATH, MUSIC_URL]", "发送音乐客服消息"
|
110
|
+
method_option :title, :aliases => "-h", :desc => "音乐标题"
|
111
|
+
method_option :description, :aliases => "-d", :desc => "音乐描述"
|
112
|
+
method_option :HQ_music_url, :aliases => "-u", :desc => "高质量音乐URL链接"
|
113
|
+
def custom_music openid, thumbnail_path, music_url
|
114
|
+
file = File.new(thumbnail_path)
|
115
|
+
api = Helper.with(options)
|
116
|
+
|
117
|
+
api_opts = options.slice(:title, :description, :HQ_music_url)
|
118
|
+
thumb_media_id = api.media_create("thumb", file)["thumb_media_id"]
|
119
|
+
puts api.custom_message_send Wechat::Message.to(openid).music(thumb_media_id, music_url, api_opts)
|
120
|
+
end
|
121
|
+
|
122
|
+
desc "custom_news [OPENID, NEWS_YAML_FILE]", "发送图文客服消息"
|
123
|
+
def custom_news openid, news_yaml
|
124
|
+
articles = YAML.load(File.new(news_yaml).read)
|
125
|
+
puts Helper.with(options).custom_message_send Wechat::Message.to(openid).news(articles["articles"])
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Wechat
|
2
|
+
class AccessToken
|
3
|
+
attr_reader :client, :appid, :secret, :token_file, :token_data
|
4
|
+
|
5
|
+
def initialize(client, appid, secret, token_file)
|
6
|
+
@appid = appid
|
7
|
+
@secret = secret
|
8
|
+
@client = client
|
9
|
+
@token_file = token_file
|
10
|
+
end
|
11
|
+
|
12
|
+
def token
|
13
|
+
begin
|
14
|
+
@token_data ||= JSON.parse(File.read(token_file))
|
15
|
+
rescue
|
16
|
+
self.refresh
|
17
|
+
end
|
18
|
+
return valid_token(@token_data)
|
19
|
+
end
|
20
|
+
|
21
|
+
def refresh
|
22
|
+
data = client.get("token", params:{grant_type: "client_credential", appid: appid, secret: secret})
|
23
|
+
File.open(token_file, 'w'){|f| f.write(data.to_s)} if valid_token(data)
|
24
|
+
return @token_data = data
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def valid_token token_data
|
29
|
+
access_token = token_data["access_token"]
|
30
|
+
raise "Response didn't have access_token" if access_token.blank?
|
31
|
+
return access_token
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
data/lib/wechat/api.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'wechat/client'
|
2
|
+
require 'wechat/access_token'
|
3
|
+
|
4
|
+
class Wechat::Api
|
5
|
+
attr_reader :access_token, :client
|
6
|
+
|
7
|
+
API_BASE = "https://api.weixin.qq.com/cgi-bin/"
|
8
|
+
FILE_BASE = "http://file.api.weixin.qq.com/cgi-bin/"
|
9
|
+
|
10
|
+
def initialize appid, secret, token_file
|
11
|
+
@client = Wechat::Client.new(API_BASE)
|
12
|
+
@access_token = Wechat::AccessToken.new(@client, appid, secret, token_file)
|
13
|
+
end
|
14
|
+
|
15
|
+
def users nextid = nil
|
16
|
+
params = {params: {next_openid: nextid}} if nextid.present?
|
17
|
+
get('user/get', params||{})
|
18
|
+
end
|
19
|
+
|
20
|
+
def user openid
|
21
|
+
get("user/info", params:{openid: openid})
|
22
|
+
end
|
23
|
+
|
24
|
+
def menu
|
25
|
+
get("menu/get")
|
26
|
+
end
|
27
|
+
|
28
|
+
def menu_delete
|
29
|
+
get("menu/delete")
|
30
|
+
end
|
31
|
+
|
32
|
+
def menu_create menu
|
33
|
+
# 微信不接受7bit escaped json(eg \uxxxx), 中文必须UTF-8编码, 这可能是个安全漏洞
|
34
|
+
post("menu/create", JSON.generate(menu))
|
35
|
+
end
|
36
|
+
|
37
|
+
def media media_id
|
38
|
+
response = get "media/get", params:{media_id: media_id}, base: FILE_BASE, as: :file
|
39
|
+
end
|
40
|
+
|
41
|
+
def media_create type, file
|
42
|
+
post "media/upload", {upload:{media: file}}, params:{type: type}, base: FILE_BASE
|
43
|
+
end
|
44
|
+
|
45
|
+
def custom_message_send message
|
46
|
+
post "message/custom/send", message.to_json, content_type: :json
|
47
|
+
end
|
48
|
+
|
49
|
+
def template_message_send message
|
50
|
+
post "message/template/send", message.to_json, content_type: :json
|
51
|
+
end
|
52
|
+
|
53
|
+
protected
|
54
|
+
def get path, headers={}
|
55
|
+
with_access_token(headers[:params]){|params| client.get path, headers.merge(params: params)}
|
56
|
+
end
|
57
|
+
|
58
|
+
def post path, payload, headers = {}
|
59
|
+
with_access_token(headers[:params]){|params| client.post path, payload, headers.merge(params: params)}
|
60
|
+
end
|
61
|
+
|
62
|
+
def with_access_token params={}, tries=2
|
63
|
+
begin
|
64
|
+
params ||= {}
|
65
|
+
yield(params.merge(access_token: access_token.token))
|
66
|
+
rescue Wechat::AccessTokenExpiredError => ex
|
67
|
+
access_token.refresh
|
68
|
+
retry unless (tries -= 1).zero?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'rest_client'
|
2
|
+
|
3
|
+
module Wechat
|
4
|
+
class Client
|
5
|
+
|
6
|
+
attr_reader :base
|
7
|
+
|
8
|
+
def initialize(base)
|
9
|
+
@base = base
|
10
|
+
end
|
11
|
+
|
12
|
+
def get path, header={}
|
13
|
+
request(path, header) do |url, header|
|
14
|
+
RestClient.get(url, header)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def post path, payload, header = {}
|
19
|
+
request(path, header) do |url, header|
|
20
|
+
RestClient.post(url, payload, header)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def request path, header={}, &block
|
25
|
+
url = "#{header.delete(:base) || self.base}#{path}"
|
26
|
+
as = header.delete(:as)
|
27
|
+
header.merge!(:accept => :json)
|
28
|
+
response = yield(url, header)
|
29
|
+
|
30
|
+
raise "Request not OK, response code #{response.code}" if response.code != 200
|
31
|
+
parse_response(response, as || :json) do |parse_as, data|
|
32
|
+
break data unless (parse_as == :json && data["errcode"].present?)
|
33
|
+
|
34
|
+
case data["errcode"]
|
35
|
+
when 0 # for request didn't expect results
|
36
|
+
true
|
37
|
+
|
38
|
+
when 42001, 40014 #42001: access_token超时, 40014:不合法的access_token
|
39
|
+
raise AccessTokenExpiredError
|
40
|
+
|
41
|
+
else
|
42
|
+
raise ResponseError.new(data['errcode'], data['errmsg'])
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
def parse_response response, as
|
49
|
+
content_type = response.headers[:content_type]
|
50
|
+
parse_as = {
|
51
|
+
/^application\/json/ => :json,
|
52
|
+
/^image\/.*/ => :file
|
53
|
+
}.inject([]){|memo, match| memo<<match[1] if content_type =~ match[0]; memo}.first || as || :text
|
54
|
+
|
55
|
+
case parse_as
|
56
|
+
when :file
|
57
|
+
file = Tempfile.new("tmp")
|
58
|
+
file.binmode
|
59
|
+
file.write(response.body)
|
60
|
+
file.close
|
61
|
+
data = file
|
62
|
+
|
63
|
+
when :json
|
64
|
+
data = JSON.parse(response.body.gsub /[\u0000-\u001f]+/, '')
|
65
|
+
|
66
|
+
else
|
67
|
+
data = response.body
|
68
|
+
end
|
69
|
+
|
70
|
+
return yield(parse_as, data)
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
module Wechat
|
2
|
+
class Message
|
3
|
+
|
4
|
+
JSON_KEY_MAP = {
|
5
|
+
"ToUserName" => "touser",
|
6
|
+
"MediaId" => "media_id",
|
7
|
+
"ThumbMediaId" => "thumb_media_id",
|
8
|
+
"TemplateId"=>"template_id"
|
9
|
+
}
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def from_hash message_hash
|
13
|
+
self.new(message_hash)
|
14
|
+
end
|
15
|
+
|
16
|
+
def to to_user
|
17
|
+
self.new(:ToUserName=>to_user, :CreateTime=>Time.now.to_i)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class ArticleBuilder
|
22
|
+
attr_reader :items
|
23
|
+
delegate :count, to: :items
|
24
|
+
def initialize
|
25
|
+
@items=Array.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def item title: "title", description: nil, pic_url: nil, url: nil
|
29
|
+
items << {:Title=> title, :Description=> description, :PicUrl=> pic_url, :Url=> url}.reject{|k,v| v.nil? }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_reader :message_hash
|
34
|
+
|
35
|
+
def initialize(message_hash)
|
36
|
+
@message_hash = message_hash || {}
|
37
|
+
end
|
38
|
+
|
39
|
+
def [](key)
|
40
|
+
message_hash[key]
|
41
|
+
end
|
42
|
+
|
43
|
+
def reply
|
44
|
+
Message.new(
|
45
|
+
:ToUserName=>message_hash[:FromUserName],
|
46
|
+
:FromUserName=>message_hash[:ToUserName],
|
47
|
+
:CreateTime=>Time.now.to_i
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
def as type
|
52
|
+
case type
|
53
|
+
when :text
|
54
|
+
message_hash[:Content]
|
55
|
+
|
56
|
+
when :image, :voice, :video
|
57
|
+
Wechat.api.media(message_hash[:MediaId])
|
58
|
+
|
59
|
+
when :location
|
60
|
+
message_hash.slice(:Location_X, :Location_Y, :Scale, :Label).inject({}){|results, value|
|
61
|
+
results[value[0].to_s.underscore.to_sym] = value[1]; results}
|
62
|
+
else
|
63
|
+
raise "Don't know how to parse message as #{type}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def to openid
|
68
|
+
update(:ToUserName=>openid)
|
69
|
+
end
|
70
|
+
|
71
|
+
def text content
|
72
|
+
update(:MsgType=>"text", :Content=>content)
|
73
|
+
end
|
74
|
+
|
75
|
+
def image media_id
|
76
|
+
update(:MsgType=>"image", :Image=>{:MediaId=>media_id})
|
77
|
+
end
|
78
|
+
|
79
|
+
def voice media_id
|
80
|
+
update(:MsgType=>"voice", :Voice=>{:MediaId=>media_id})
|
81
|
+
end
|
82
|
+
|
83
|
+
def video media_id, opts={}
|
84
|
+
video_fields = camelize_hash_keys({media_id: media_id}.merge(opts.slice(:title, :description)))
|
85
|
+
update(:MsgType=>"video", :Video=>video_fields)
|
86
|
+
end
|
87
|
+
|
88
|
+
def music thumb_media_id, music_url, opts={}
|
89
|
+
music_fields = camelize_hash_keys(opts.slice(:title, :description, :HQ_music_url).merge(music_url: music_url, thumb_media_id: thumb_media_id))
|
90
|
+
update(:MsgType=>"music", :Music=>music_fields)
|
91
|
+
end
|
92
|
+
|
93
|
+
def news collection, &block
|
94
|
+
if block_given?
|
95
|
+
article = ArticleBuilder.new
|
96
|
+
collection.each{|item| yield(article, item)}
|
97
|
+
items = article.items
|
98
|
+
else
|
99
|
+
items = collection.collect do |item|
|
100
|
+
camelize_hash_keys(item.symbolize_keys.slice(:title, :description, :pic_url, :url).reject{|k,v| v.nil? })
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
update(:MsgType=>"news", :ArticleCount=> items.count,
|
105
|
+
:Articles=> items.collect{|item| camelize_hash_keys(item)})
|
106
|
+
end
|
107
|
+
|
108
|
+
def template opts={}
|
109
|
+
template_fields = camelize_hash_keys(opts.symbolize_keys.slice(:template_id, :topcolor, :url, :data))
|
110
|
+
update(:MsgType=>"template",:Template=> template_fields)
|
111
|
+
end
|
112
|
+
|
113
|
+
def to_xml
|
114
|
+
message_hash.to_xml(root: "xml", children: "item", skip_instruct: true, skip_types: true)
|
115
|
+
end
|
116
|
+
|
117
|
+
def to_json
|
118
|
+
json_hash = deep_recursive(message_hash) do |key, value|
|
119
|
+
key = key.to_s
|
120
|
+
[(JSON_KEY_MAP[key] || key.downcase), value]
|
121
|
+
end
|
122
|
+
|
123
|
+
json_hash.slice!("touser", "msgtype", "content", "image", "voice", "video", "music", "news", "articles","template").to_hash
|
124
|
+
case json_hash["msgtype"]
|
125
|
+
when "text"
|
126
|
+
json_hash["text"] = {"content" => json_hash.delete("content")}
|
127
|
+
when "news"
|
128
|
+
json_hash["news"] = {"articles" => json_hash.delete("articles")}
|
129
|
+
when "template"
|
130
|
+
json_hash.merge! json_hash['template']
|
131
|
+
end
|
132
|
+
JSON.generate(json_hash)
|
133
|
+
end
|
134
|
+
|
135
|
+
def save_to! model_class
|
136
|
+
model = model_class.new(underscore_hash_keys(message_hash))
|
137
|
+
model.save!
|
138
|
+
return self
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
def camelize_hash_keys hash
|
143
|
+
deep_recursive(hash){|key, value| [key.to_s.camelize.to_sym, value]}
|
144
|
+
end
|
145
|
+
|
146
|
+
def underscore_hash_keys hash
|
147
|
+
deep_recursive(hash){|key, value| [key.to_s.underscore.to_sym, value]}
|
148
|
+
end
|
149
|
+
|
150
|
+
def update fields={}
|
151
|
+
message_hash.merge!(fields)
|
152
|
+
return self
|
153
|
+
end
|
154
|
+
|
155
|
+
def deep_recursive hash, &block
|
156
|
+
hash.inject({}) do |memo, val|
|
157
|
+
key,value = *val
|
158
|
+
case value.class.name
|
159
|
+
when "Hash"
|
160
|
+
value = deep_recursive(value, &block)
|
161
|
+
when "Array"
|
162
|
+
value = value.collect{|item| item.is_a?(Hash) ? deep_recursive(item, &block) : item}
|
163
|
+
end
|
164
|
+
|
165
|
+
key,value = yield(key, value)
|
166
|
+
memo.merge!(key => value)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module Wechat
|
2
|
+
module Responder
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
self.skip_before_filter :verify_authenticity_token
|
7
|
+
self.before_filter :verify_signature, only: [:show, :create]
|
8
|
+
#delegate :wehcat, to: :class
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
|
13
|
+
attr_accessor :wechat, :token
|
14
|
+
|
15
|
+
def on message_type, with: nil, respond: nil, &block
|
16
|
+
raise "Unknow message type" unless message_type.in? [:text, :image, :voice, :video, :location, :link, :event, :fallback]
|
17
|
+
config=respond.nil? ? {} : {:respond=>respond}
|
18
|
+
config.merge!(:proc=>block) if block_given?
|
19
|
+
|
20
|
+
if (with.present? && !message_type.in?([:text, :event]))
|
21
|
+
raise "Only text and event message can take :with parameters"
|
22
|
+
else
|
23
|
+
config.merge!(:with=>with) if with.present?
|
24
|
+
end
|
25
|
+
|
26
|
+
responders(message_type) << config
|
27
|
+
return config
|
28
|
+
end
|
29
|
+
|
30
|
+
def responders type
|
31
|
+
@responders ||= Hash.new
|
32
|
+
@responders[type] ||= Array.new
|
33
|
+
end
|
34
|
+
|
35
|
+
def responder_for message, &block
|
36
|
+
message_type = message[:MsgType].to_sym
|
37
|
+
responders = responders(message_type)
|
38
|
+
|
39
|
+
case message_type
|
40
|
+
when :text
|
41
|
+
yield(* match_responders(responders, message[:Content]))
|
42
|
+
|
43
|
+
when :event
|
44
|
+
if message[:Event] == 'CLICK'
|
45
|
+
yield(* match_responders(responders, message[:EventKey]))
|
46
|
+
else
|
47
|
+
yield(* match_responders(responders, message[:Event]))
|
48
|
+
end
|
49
|
+
else
|
50
|
+
yield(responders.first)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def match_responders responders, value
|
57
|
+
matched = responders.inject({scoped:nil, general:nil}) do |matched, responder|
|
58
|
+
condition = responder[:with]
|
59
|
+
|
60
|
+
if condition.nil?
|
61
|
+
matched[:general] ||= [responder, value]
|
62
|
+
next matched
|
63
|
+
end
|
64
|
+
|
65
|
+
if condition.is_a? Regexp
|
66
|
+
matched[:scoped] ||= [responder] + $~.captures if(value =~ condition)
|
67
|
+
else
|
68
|
+
matched[:scoped] ||= [responder, value] if(value == condition)
|
69
|
+
end
|
70
|
+
matched
|
71
|
+
end
|
72
|
+
return matched[:scoped] || matched[:general]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
def show
|
78
|
+
render :text => params[:echostr]
|
79
|
+
end
|
80
|
+
|
81
|
+
def create
|
82
|
+
request = Wechat::Message.from_hash(params[:xml] || post_xml)
|
83
|
+
response = self.class.responder_for(request) do |responder, *args|
|
84
|
+
responder ||= self.class.responders(:fallback).first
|
85
|
+
|
86
|
+
next if responder.nil?
|
87
|
+
next request.reply.text responder[:respond] if (responder[:respond])
|
88
|
+
next responder[:proc].call(*args.unshift(request)) if (responder[:proc])
|
89
|
+
end
|
90
|
+
|
91
|
+
if response.respond_to? :to_xml
|
92
|
+
render xml: response.to_xml
|
93
|
+
else
|
94
|
+
render :nothing => true, :status => 200, :content_type => 'text/html'
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
def verify_signature
|
100
|
+
array = [self.class.token, params[:timestamp], params[:nonce]].compact.collect(&:to_s).sort
|
101
|
+
render :text => "Forbidden", :status => 403 if params[:signature] != Digest::SHA1.hexdigest(array.join)
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
def post_xml
|
106
|
+
data = Hash.from_xml(request.raw_post)
|
107
|
+
HashWithIndifferentAccess.new_from_hash_copying_default data.fetch('xml', {})
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
metadata
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: wechat-test
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.0'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Skinnyworm
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-03-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 4.2.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 4.2.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: nokogiri
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.6.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.6.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rest-client
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec-rails
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: API and message handling for wechat in rails environment
|
70
|
+
email: askinnyworm@gmail.com
|
71
|
+
executables:
|
72
|
+
- wechat
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- LICENSE
|
77
|
+
- README.md
|
78
|
+
- Rakefile
|
79
|
+
- bin/wechat
|
80
|
+
- lib/wechat-rails.rb
|
81
|
+
- lib/wechat.rb
|
82
|
+
- lib/wechat/access_token.rb
|
83
|
+
- lib/wechat/api.rb
|
84
|
+
- lib/wechat/client.rb
|
85
|
+
- lib/wechat/message.rb
|
86
|
+
- lib/wechat/responder.rb
|
87
|
+
homepage: https://github.com/skinnyworm/wechat-rails
|
88
|
+
licenses: []
|
89
|
+
metadata: {}
|
90
|
+
post_install_message:
|
91
|
+
rdoc_options: []
|
92
|
+
require_paths:
|
93
|
+
- lib
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
requirements: []
|
105
|
+
rubyforge_project:
|
106
|
+
rubygems_version: 2.4.6
|
107
|
+
signing_key:
|
108
|
+
specification_version: 4
|
109
|
+
summary: DSL for wechat message handling and api
|
110
|
+
test_files: []
|