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 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: []