wechat_public 0.1.1 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4568f46e962b94efd05b7410166f2d44cbc205ac
4
- data.tar.gz: 256927033a0075ad1ee68a0d0d0b18dfa2ac6cb5
3
+ metadata.gz: 744beadd7a9d6788a4ba9c10749824fd3f075fd2
4
+ data.tar.gz: 9185439b87d650df02b85728b84614ad070a0196
5
5
  SHA512:
6
- metadata.gz: 0c041608b86c40fdad5386d665c8a748fe4849ea610290b795b7b01a1c8338f102a34ee8ad2f15cd819ee407decb0c162589a78785246827992d5bfd6ab58b40
7
- data.tar.gz: a9f58047e3f916176ff7ea982eca9689b27675465e1289de6b1e150f1c931a2cc2bb2c5341232e965eda3d13ddc64f816f673317df8b0286f01cbc26ffac3da7
6
+ metadata.gz: d2b1b1d158d4b177363c2036de94987fe9d83851ae9ad7f08ac85a31ff38e1cc15cd83ae29c92707f3b234f252c1c5af73d7eb78ad2b4080fd3a824717fc3a5b
7
+ data.tar.gz: 7dbcff4a3af30a9ff00689db86fdabf3f5542a50e4f348fff1df285fb267912e798c5b9e63750252fb9b672c111739b2ceb91756a38beb3cad055df16cf39798
@@ -0,0 +1,35 @@
1
+ module WechatPublic
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 WechatPublic::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 = WechatPublic::Client.new(API_BASE)
12
+ @access_token = WechatPublic::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 WechatPublic::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 WechatPublic
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 WechatPublic
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
+ WechatPublic.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,124 @@
1
+ module WechatPublic
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, :login]
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
+ case message_type
39
+ when :text
40
+ yield(* match_responders(responders, message[:Content]))
41
+
42
+ when :event
43
+ if message[:Event] == 'CLICK'
44
+ yield(* match_responders(responders, message[:EventKey]))
45
+ else
46
+ yield(* match_responders(responders, message[:Event]))
47
+ end
48
+ else
49
+ yield(responders.first)
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def match_responders responders, value
56
+ matched = responders.inject({scoped:nil, general:nil}) do |matched, responder|
57
+ condition = responder[:with]
58
+
59
+ if condition.nil?
60
+ matched[:general] ||= [responder, value]
61
+ next matched
62
+ end
63
+
64
+ if condition.is_a? Regexp
65
+ matched[:scoped] ||= [responder] + $~.captures if(value =~ condition)
66
+ else
67
+ matched[:scoped] ||= [responder, value] if(value == condition)
68
+ end
69
+ matched
70
+ end
71
+ return matched[:scoped] || matched[:general]
72
+ end
73
+ end
74
+
75
+
76
+ # def show
77
+ # render :text => params[:echostr]
78
+ # end
79
+
80
+ # def create
81
+ # request = Wechat::Message.from_hash(params[:xml] || post_xml)
82
+ # response = self.class.responder_for(request) do |responder, *args|
83
+ # responder ||= self.class.responders(:fallback).first
84
+
85
+ # next if responder.nil?
86
+ # next request.reply.text responder[:respond] if (responder[:respond])
87
+ # next responder[:proc].call(*args.unshift(request)) if (responder[:proc])
88
+ # end
89
+
90
+ # if response.respond_to? :to_xml
91
+ # render xml: response.to_xml
92
+ # else
93
+ # render :nothing => true, :status => 200, :content_type => 'text/html'
94
+ # end
95
+ # end
96
+
97
+ def get_uid params
98
+ request = WechatPublic::Message.from_hash(params[:xml] || post_xml)
99
+ response = self.class.responder_for(request) do |responder, *args|
100
+ responder ||= self.class.responders(:login).first
101
+
102
+ next if responder.nil?
103
+ next request.reply.text responder[:respond] if (responder[:respond])
104
+ next responder[:proc].call(*args.unshift(request)) if (responder[:proc])
105
+ end
106
+ {
107
+ request: request,
108
+ response: response
109
+ }
110
+ end
111
+
112
+ private
113
+ def verify_signature
114
+ array = [self.class.token, params[:timestamp], params[:nonce]].compact.collect(&:to_s).sort
115
+ render :text => "Forbidden", :status => 403 if params[:signature] != Digest::SHA1.hexdigest(array.join)
116
+ end
117
+
118
+ private
119
+ def post_xml
120
+ data = Hash.from_xml(request.raw_post)
121
+ HashWithIndifferentAccess.new_from_hash_copying_default data.fetch('xml', {})
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,7 @@
1
+ module WechatPublic
2
+ module Test
3
+ def self.print
4
+ puts "我加载成功了啊......"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module WechatPublic
2
+ module Test
3
+ def self.print
4
+ puts "我加载成功了啊......"
5
+ end
6
+ end
7
+ end
@@ -1,3 +1,3 @@
1
1
  module WechatPublic
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.2"
3
3
  end
data/lib/wechat_public.rb CHANGED
@@ -1,11 +1,11 @@
1
1
  require "wechat_public/version"
2
- require "wechat/test"
2
+ require "wechat/api"
3
3
  module WechatPublic
4
4
  autoload :Message, "wechat/message"
5
5
  autoload :Responder, "wechat/responder"
6
6
  autoload :Response, "wechat/response"
7
- # autoload :Test, 'wechat/test'
8
-
7
+ autoload :Test, 'wechat/test'
8
+ #WechatPublic::Test.print
9
9
  class AccessTokenExpiredError < StandardError; end
10
10
  class ResponseError < StandardError
11
11
  attr_reader :error_code
@@ -31,7 +31,8 @@ module WechatPublic
31
31
  puts config
32
32
  OpenStruct.new(config)
33
33
  puts "判断 ActionController::Base...加载了么",ActionController::Base
34
- #Test.print
34
+ puts "判断 WechatPublic::Test 。。加载了么",WechatPublic::Test
35
+ WechatPublic::Test.print
35
36
  end
36
37
  end
37
38
 
@@ -2,6 +2,7 @@
2
2
  lib = File.expand_path('../lib', __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'wechat_public/version'
5
+ require 'wechat_public/test'
5
6
 
6
7
  Gem::Specification.new do |spec|
7
8
  spec.name = "wechat_public"
@@ -20,4 +21,8 @@ Gem::Specification.new do |spec|
20
21
 
21
22
  spec.add_development_dependency "bundler", "~> 1.6"
22
23
  spec.add_development_dependency 'rake', '~> 0'
24
+ # spec.add_development_dependency 'rest_client', '~> 1.8'
25
+ # spec.add_development_dependency 'rails', '~> 4.2'
26
+ spec.add_runtime_dependency 'rest_client', '>= 1.8'
27
+ spec.add_runtime_dependency 'rails', ">= 4.2"
23
28
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wechat_public
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Will
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-04-18 00:00:00.000000000 Z
11
+ date: 2015-04-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -38,6 +38,34 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '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: '1.8'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '1.8'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rails
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '4.2'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '4.2'
41
69
  description: one coding programmer.
42
70
  email:
43
71
  - yh@fir.im
@@ -50,7 +78,14 @@ files:
50
78
  - LICENSE.txt
51
79
  - README.md
52
80
  - Rakefile
81
+ - lib/wechat/access_token.rb
82
+ - lib/wechat/api.rb
83
+ - lib/wechat/client.rb
84
+ - lib/wechat/message.rb
85
+ - lib/wechat/responder.rb
86
+ - lib/wechat/test.rb
53
87
  - lib/wechat_public.rb
88
+ - lib/wechat_public/test.rb
54
89
  - lib/wechat_public/version.rb
55
90
  - wechat_public.gemspec
56
91
  homepage: ''