wechat-test 0.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ [![Build Status](https://travis-ci.org/skinnyworm/omniauth-wechat-oauth2.svg)](https://travis-ci.org/skinnyworm/wechat-rails) [![Code Climate](https://codeclimate.com/github/skinnyworm/wechat-rails.png)](https://codeclimate.com/github/skinnyworm/wechat-rails) [![Code Coverage](https://codeclimate.com/github/skinnyworm/wechat-rails/coverage.png)](https://codeclimate.com/github/skinnyworm/wechat-rails) [![Gem Version](https://badge.fury.io/rb/wechat-rails.png)](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
@@ -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: []