qy_wechat 1.0.0.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/Rakefile +37 -0
- data/app/controllers/qy_wechat/qy_wechat_controller.rb +64 -0
- data/config/routes.rb +4 -0
- data/lib/generators/qy_wechat/install_generator.rb +35 -0
- data/lib/generators/qy_wechat/migration_generator.rb +35 -0
- data/lib/generators/templates/add_qy_wechat_columns.rb +20 -0
- data/lib/generators/templates/qy_wechat_config.rb +4 -0
- data/lib/generators/templates/qy_wechat_controller.rb +30 -0
- data/lib/qy_wechat/configuration.rb +31 -0
- data/lib/qy_wechat/engine.rb +6 -0
- data/lib/qy_wechat/helpers/pkcs7_encoder.rb +26 -0
- data/lib/qy_wechat/helpers/prpcrypt.rb +42 -0
- data/lib/qy_wechat/helpers/reply_message_helper.rb +117 -0
- data/lib/qy_wechat/message/encrypt_message.rb +24 -0
- data/lib/qy_wechat/message/message.rb +171 -0
- data/lib/qy_wechat/message/response_message.rb +152 -0
- data/lib/qy_wechat/version.rb +3 -0
- data/lib/qy_wechat.rb +16 -0
- metadata +134 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e0406f31fb56e074f7cf19633c0430a2a13dbedd
|
4
|
+
data.tar.gz: 44e9e8ae54ef833ab1ba3ba9c72ddb1f07d56d1b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1e1f81653a492b0b5d755a7e3b1e548e95313a38232568e4725127ac4c3ea395fae18d9c8f1b18cecc6c8580562376dabb77b70c4a4e007a6d8345f9a1f7fb3d
|
7
|
+
data.tar.gz: e95dddb6e3f94d51cd24cb086fdc6f7c1db1a8e8a199ad58291805752729e44b69a44bfd312be5de43e8b8bb6a5be55e5bee0368ee0c1852510bb2479ac4d5e8
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2014 lanrion
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'QyWechat'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
|
21
|
+
load 'rails/tasks/statistics.rake'
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
Bundler::GemHelper.install_tasks
|
26
|
+
|
27
|
+
require 'rake/testtask'
|
28
|
+
|
29
|
+
Rake::TestTask.new(:test) do |t|
|
30
|
+
t.libs << 'lib'
|
31
|
+
t.libs << 'test'
|
32
|
+
t.pattern = 'test/**/*_test.rb'
|
33
|
+
t.verbose = false
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
task default: :test
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module QyWechat
|
3
|
+
class QyWechatController < ActionController::Base
|
4
|
+
|
5
|
+
include ReplyMessageHelper
|
6
|
+
|
7
|
+
skip_before_filter :verify_authenticity_token, only: :reply
|
8
|
+
before_action :setup_qy_account, only: [:verify_url, :reply]
|
9
|
+
before_action :setup_wechat_message, only: :reply
|
10
|
+
|
11
|
+
# 验证URL有效性
|
12
|
+
def verify_url
|
13
|
+
raise "Not Match" if not valid_msg_signature(params)
|
14
|
+
params.delete(:qy_secret_key)
|
15
|
+
content = Prpcrypt.decrypt(aes_key, params[:echostr], corp_id)
|
16
|
+
render text: content
|
17
|
+
end
|
18
|
+
|
19
|
+
def reply;end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def setup_wechat_message
|
24
|
+
param_xml = request.body.read
|
25
|
+
hash = MultiXml.parse(param_xml)['xml']
|
26
|
+
@body_xml = OpenStruct.new(hash)
|
27
|
+
hash = MultiXml.parse(Prpcrypt.decrypt(aes_key, @body_xml.Encrypt, corp_id))["xml"]
|
28
|
+
@weixin_message = Message.factory(hash)
|
29
|
+
@keyword = @weixin_message.Content
|
30
|
+
end
|
31
|
+
|
32
|
+
def encoding_aes_key
|
33
|
+
@qy_account.encoding_aes_key
|
34
|
+
end
|
35
|
+
|
36
|
+
def qy_token
|
37
|
+
@qy_account.qy_token
|
38
|
+
end
|
39
|
+
|
40
|
+
def aes_key
|
41
|
+
Base64.decode64(@qy_account.encoding_aes_key + "=")
|
42
|
+
end
|
43
|
+
|
44
|
+
def corp_id
|
45
|
+
@qy_account.corp_id
|
46
|
+
end
|
47
|
+
|
48
|
+
# String signature = SHA1.getSHA1(token, timeStamp, nonce, echoStr);
|
49
|
+
def valid_msg_signature(params)
|
50
|
+
timestamp = params[:timestamp]
|
51
|
+
nonce = params[:nonce]
|
52
|
+
echo_str = params[:echostr]
|
53
|
+
msg_signature = params[:msg_signature]
|
54
|
+
sort_params = [qy_token, timestamp, nonce, echo_str].sort.join
|
55
|
+
current_signature = Digest::SHA1.hexdigest(sort_params)
|
56
|
+
Rails.logger.info("current_signature: #{current_signature} " )
|
57
|
+
current_signature == msg_signature
|
58
|
+
end
|
59
|
+
|
60
|
+
def setup_qy_account
|
61
|
+
@qy_account ||= QyWechat.qy_model.find_by(qy_secret_key: params[:qy_secret_key])
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# Rails::Generators::Base dont need a name
|
2
|
+
# Rails::Generators::NamedBase need a name
|
3
|
+
module QyWechat
|
4
|
+
module Generators
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
6
|
+
source_root File.expand_path('../../templates', __FILE__)
|
7
|
+
|
8
|
+
desc 'Creates a QyWechat initializer for your application.'
|
9
|
+
|
10
|
+
def install
|
11
|
+
route 'mount QyWechat::Engine, at: "/"'
|
12
|
+
end
|
13
|
+
|
14
|
+
def copy_initializer
|
15
|
+
template 'qy_wechat_config.rb', 'config/initializers/qy_wechat_config.rb'
|
16
|
+
end
|
17
|
+
|
18
|
+
def configure_application
|
19
|
+
application <<-APP
|
20
|
+
config.to_prepare do
|
21
|
+
# Load application's model / class decorators
|
22
|
+
Dir.glob(File.join(File.dirname(__FILE__), "../app/**/*_decorator*.rb")) do |c|
|
23
|
+
Rails.configuration.cache_classes ? require(c) : load(c)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
APP
|
27
|
+
end
|
28
|
+
|
29
|
+
def copy_decorators
|
30
|
+
template 'qy_wechat_controller.rb', 'app/decorators/controllers/qy_wechat/qy_wechat_controller_decorator.rb'
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rails/generators/active_record'
|
2
|
+
|
3
|
+
module QyWechat
|
4
|
+
module Generators
|
5
|
+
class MigrationGenerator < ActiveRecord::Generators::Base
|
6
|
+
source_root File.expand_path('../../templates', __FILE__)
|
7
|
+
|
8
|
+
desc 'Adds a qy qy_token, encoding_aes_key, corp_id for your application.'
|
9
|
+
def create_migration_file
|
10
|
+
if !migration_exists?(table_name)
|
11
|
+
migration_template "add_qy_wechat_columns.rb", "db/migrate/add_qy_wechat_columns_to_#{plural_name}.rb"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def model_exists?
|
18
|
+
File.exists?(File.join(destination_root, model_path))
|
19
|
+
end
|
20
|
+
|
21
|
+
def model_path
|
22
|
+
@model_path ||= File.join("app", "models", "#{file_path}.rb")
|
23
|
+
end
|
24
|
+
|
25
|
+
def migration_exists?(table_name)
|
26
|
+
Dir.glob("#{File.join(destination_root, migration_path)}/[0-9]*_*.rb").grep(/\d+_add_qy_wechat_columns_to_#{table_name}.rb/).first
|
27
|
+
end
|
28
|
+
|
29
|
+
def migration_path
|
30
|
+
@migration_path ||= File.join("db", "migrate")
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class AddQyWechatColumnsTo<%= table_name.camelize %> < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table(:<%= table_name %>) do |t|
|
4
|
+
t.string :qy_token
|
5
|
+
t.string :encoding_aes_key
|
6
|
+
t.string :corp_id
|
7
|
+
t.string :qy_secret_key
|
8
|
+
end
|
9
|
+
add_index :<%= table_name %>, :qy_token
|
10
|
+
add_index :<%= table_name %>, :encoding_aes_key
|
11
|
+
add_index :<%= table_name %>, :corp_id
|
12
|
+
add_index :<%= table_name %>, :qy_secret_key
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.down
|
16
|
+
# By default, we don't want to make any assumption about how to roll back a migration when your
|
17
|
+
# model already existed. Please edit below which fields you would like to remove in this migration.
|
18
|
+
raise ActiveRecord::IrreversibleMigration
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
QyWechat::QyWechatController.class_eval do
|
3
|
+
|
4
|
+
def reply
|
5
|
+
render xml: send("response_#{@weixin_message.MsgType}_message", {})
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
# text消息
|
11
|
+
def response_text_message(options={})
|
12
|
+
generate_text_message("Your Message: #{@keyword}")
|
13
|
+
end
|
14
|
+
|
15
|
+
# image消息
|
16
|
+
def response_image_message(options={})
|
17
|
+
generate_image_message(new_image(@weixin_message.MediaId))
|
18
|
+
end
|
19
|
+
|
20
|
+
# voice消息
|
21
|
+
def response_voice_message(options={})
|
22
|
+
generate_voice_message(new_voice(@weixin_message.MediaId))
|
23
|
+
end
|
24
|
+
|
25
|
+
# video消息
|
26
|
+
def response_video_message(options={})
|
27
|
+
generate_video_message(new_video(@weixin_message.MediaId, "desc", "title"))
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module QyWechat
|
2
|
+
|
3
|
+
class << self
|
4
|
+
|
5
|
+
attr_accessor :configuration
|
6
|
+
|
7
|
+
def config
|
8
|
+
self.configuration ||= Configuration.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def configure
|
12
|
+
yield config if block_given?
|
13
|
+
end
|
14
|
+
|
15
|
+
def qy_model_name
|
16
|
+
@qy_model_name ||= QyWechat.config.qy_account
|
17
|
+
end
|
18
|
+
|
19
|
+
def qy_model
|
20
|
+
if qy_model_name.blank?
|
21
|
+
raise "You need to config `qy_account` in 'config/initializers/qy_wechat_config.rb'"
|
22
|
+
end
|
23
|
+
@qy_model ||= qy_model_name.to_s.constantize
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
class Configuration
|
29
|
+
attr_accessor :qy_account
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module QyWechat
|
2
|
+
module PKCS7Encoder
|
3
|
+
extend self
|
4
|
+
|
5
|
+
BLOCK_SIZE = 32
|
6
|
+
|
7
|
+
def decode(text)
|
8
|
+
pad = text[-1].ord
|
9
|
+
pad = 0 if (pad < 1 || pad > BLOCK_SIZE)
|
10
|
+
size = text.size - pad
|
11
|
+
text[0...size]
|
12
|
+
end
|
13
|
+
|
14
|
+
# 对需要加密的明文进行填充补位
|
15
|
+
# 返回补齐明文字符串
|
16
|
+
def encode(text)
|
17
|
+
# 计算需要填充的位数
|
18
|
+
amount_to_pad = BLOCK_SIZE - (text.length % BLOCK_SIZE)
|
19
|
+
amount_to_pad = BLOCK_SIZE if amount_to_pad == 0
|
20
|
+
# 获得补位所用的字符
|
21
|
+
pad_chr = amount_to_pad.chr
|
22
|
+
"#{text}#{pad_chr * amount_to_pad}"
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module QyWechat
|
3
|
+
module Prpcrypt
|
4
|
+
extend self
|
5
|
+
|
6
|
+
# 对密文进行解密.
|
7
|
+
# text 需要解密的密文
|
8
|
+
def decrypt(aes_key, text, corpid)
|
9
|
+
text = Base64.decode64(text)
|
10
|
+
text = handle_cipher(:decrypt, aes_key, text)
|
11
|
+
result = PKCS7Encoder.decode(text)
|
12
|
+
content = result[16...result.length]
|
13
|
+
len_list = content[0...4].unpack("N")
|
14
|
+
xml_len = len_list[0]
|
15
|
+
xml_content = content[4...4 + xml_len]
|
16
|
+
from_corpid = content[xml_len+4...content.size]
|
17
|
+
raise "UnMatch corpid" if corpid != from_corpid
|
18
|
+
xml_content
|
19
|
+
end
|
20
|
+
|
21
|
+
# 加密
|
22
|
+
def encrypt(aes_key, text, corpid)
|
23
|
+
text = text.force_encoding("ASCII-8BIT")
|
24
|
+
random = SecureRandom.hex(8)
|
25
|
+
msg_len = [text.length].pack("N")
|
26
|
+
text = "#{random}#{msg_len}#{text}#{corpid}"
|
27
|
+
text = PKCS7Encoder.encode(text)
|
28
|
+
text = handle_cipher(:encrypt, aes_key, text)
|
29
|
+
Base64.encode64(text)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
def handle_cipher(action, aes_key, text)
|
34
|
+
cipher = OpenSSL::Cipher.new('AES-256-CBC')
|
35
|
+
cipher.send(action)
|
36
|
+
cipher.padding = 0
|
37
|
+
cipher.key = aes_key
|
38
|
+
cipher.iv = aes_key[0...16]
|
39
|
+
cipher.update(text) + cipher.final
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module QyWechat
|
2
|
+
module ReplyMessageHelper
|
3
|
+
|
4
|
+
# e.g.
|
5
|
+
# generate_text_message(@weixin_message.ToUserName, @weixin_message.FromUserName, "Your Message: #{@weixin_message.Content}")
|
6
|
+
# Or generate_text_message("Your Message: #{@weixin_message.Content}")
|
7
|
+
def generate_text_message(from=nil, to=nil, content)
|
8
|
+
message = TextResponseMessage.new
|
9
|
+
message.FromUserName = from || @weixin_message.ToUserName
|
10
|
+
message.ToUserName = to || @weixin_message.FromUserName
|
11
|
+
message.Content = content
|
12
|
+
encrypt_message(message.to_xml)
|
13
|
+
end
|
14
|
+
|
15
|
+
def new_article(title, desc, pic_url, link_url)
|
16
|
+
item = Article.new
|
17
|
+
item.Title = title
|
18
|
+
item.Description = desc
|
19
|
+
item.PicUrl = pic_url
|
20
|
+
item.Url = link_url
|
21
|
+
item
|
22
|
+
end
|
23
|
+
|
24
|
+
# articles = [new_article]
|
25
|
+
def generate_news_message(from=nil, to=nil, articles)
|
26
|
+
message = NewsResponseMessage.new
|
27
|
+
message.FromUserName = from || @weixin_message.ToUserName
|
28
|
+
message.ToUserName = to || @weixin_message.FromUserName
|
29
|
+
message.Articles = articles
|
30
|
+
message.ArticleCount = articles.count
|
31
|
+
encrypt_message message.to_xml
|
32
|
+
end
|
33
|
+
|
34
|
+
def new_video(media_id, desc, title)
|
35
|
+
video = Video.new
|
36
|
+
video.MediaId = media_id
|
37
|
+
video.Title = title
|
38
|
+
video.Description = desc
|
39
|
+
video
|
40
|
+
end
|
41
|
+
|
42
|
+
# <xml>
|
43
|
+
# <ToUserName><![CDATA[toUser]]></ToUserName>
|
44
|
+
# <FromUserName><![CDATA[fromUser]]></FromUserName>
|
45
|
+
# <CreateTime>12345678</CreateTime>
|
46
|
+
# <MsgType><![CDATA[video]]></MsgType>
|
47
|
+
# <Video>
|
48
|
+
# <MediaId><![CDATA[media_id]]></MediaId>
|
49
|
+
# <Title><![CDATA[title]]></Title>
|
50
|
+
# <Description><![CDATA[description]]></Description>
|
51
|
+
# </Video>
|
52
|
+
# </xml>
|
53
|
+
|
54
|
+
def generate_video_message(from=nil, to=nil, video)
|
55
|
+
message = VideoResponseMessage.new
|
56
|
+
message.FromUserName = from || @weixin_message.ToUserName
|
57
|
+
message.ToUserName = to || @weixin_message.FromUserName
|
58
|
+
message.Video = video
|
59
|
+
encrypt_message message.to_xml
|
60
|
+
end
|
61
|
+
|
62
|
+
def new_voice(media_id)
|
63
|
+
voice = Voice.new
|
64
|
+
voice.MediaId = media_id
|
65
|
+
voice
|
66
|
+
end
|
67
|
+
|
68
|
+
def generate_voice_message(from=nil, to=nil, voice)
|
69
|
+
message = VoiceResponseMessage.new
|
70
|
+
message.FromUserName = from || @weixin_message.ToUserName
|
71
|
+
message.ToUserName = to || @weixin_message.FromUserName
|
72
|
+
message.Voice = voice
|
73
|
+
encrypt_message message.to_xml
|
74
|
+
end
|
75
|
+
|
76
|
+
def new_image(media_id)
|
77
|
+
image = Image.new
|
78
|
+
image.MediaId = media_id
|
79
|
+
image
|
80
|
+
end
|
81
|
+
|
82
|
+
def generate_image_message(from=nil, to=nil, image)
|
83
|
+
message = ImageResponseMessage.new
|
84
|
+
message.FromUserName = from || @weixin_message.ToUserName
|
85
|
+
message.ToUserName = to || @weixin_message.FromUserName
|
86
|
+
message.Image = image
|
87
|
+
encrypt_message message.to_xml
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def encrypt_message(msg_xml)
|
93
|
+
# 加密回复的XML
|
94
|
+
encrypt_xml = Prpcrypt.encrypt(aes_key, msg_xml, corp_id).gsub("\n","")
|
95
|
+
# 标准的回包
|
96
|
+
generate_encrypt_message(encrypt_xml)
|
97
|
+
end
|
98
|
+
|
99
|
+
def generate_encrypt_message(encrypt_xml)
|
100
|
+
msg = EncryptMessage.new
|
101
|
+
msg.Encrypt = encrypt_xml
|
102
|
+
msg.TimeStamp = Time.now.to_i.to_s
|
103
|
+
msg.Nonce = "123"
|
104
|
+
msg.MsgSignature = generate_msg_signature(encrypt_xml, msg)
|
105
|
+
msg.to_xml
|
106
|
+
end
|
107
|
+
|
108
|
+
# dev_msg_signature=sha1(sort(token、timestamp、nonce、msg_encrypt))
|
109
|
+
# 生成企业签名
|
110
|
+
# $array = array($encrypt_msg, $token, $timestamp, $nonce);
|
111
|
+
def generate_msg_signature(encrypt_msg, msg)
|
112
|
+
sort_params = [encrypt_msg, qy_token, msg.TimeStamp, msg.Nonce].sort.join
|
113
|
+
Digest::SHA1.hexdigest(sort_params)
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# 标准的回包
|
3
|
+
# <xml>
|
4
|
+
# <Encrypt><![CDATA[msg_encrypt]]></Encrypt>
|
5
|
+
# <MsgSignature><![CDATA[msg_signature]]></MsgSignature>
|
6
|
+
# <TimeStamp>timestamp</TimeStamp>
|
7
|
+
# <Nonce><![CDATA[nonce]]></Nonce>
|
8
|
+
# </xml>
|
9
|
+
|
10
|
+
module QyWechat
|
11
|
+
class EncryptMessage
|
12
|
+
include ROXML
|
13
|
+
xml_name :xml
|
14
|
+
|
15
|
+
xml_accessor :Encrypt, :cdata => true
|
16
|
+
xml_accessor :Nonce, :cdata => true
|
17
|
+
xml_accessor :TimeStamp, :as => Integer
|
18
|
+
xml_accessor :MsgSignature, :cdata => true
|
19
|
+
|
20
|
+
def to_xml
|
21
|
+
super.to_xml(:encoding => 'UTF-8', :indent => 0, :save_with => 0)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# ref: https://github.com/wolfg1969/rack-weixin/lib/weixin/model.rb
|
3
|
+
require 'roxml'
|
4
|
+
require 'multi_xml'
|
5
|
+
require 'ostruct'
|
6
|
+
|
7
|
+
# multi_xml will use Nokogiri if it is available
|
8
|
+
MultiXml.parser = :nokogiri
|
9
|
+
|
10
|
+
module QyWechat
|
11
|
+
|
12
|
+
class Message
|
13
|
+
|
14
|
+
def initialize(hash)
|
15
|
+
@source = OpenStruct.new(hash)
|
16
|
+
end
|
17
|
+
|
18
|
+
def method_missing(method, *args, &block)
|
19
|
+
@source.send(method, *args, &block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def CreateTime
|
23
|
+
@source.CreateTime.to_i
|
24
|
+
end
|
25
|
+
|
26
|
+
def MsgId
|
27
|
+
@source.MsgId.to_i
|
28
|
+
end
|
29
|
+
|
30
|
+
def corp_id
|
31
|
+
@source.FromUserName
|
32
|
+
end
|
33
|
+
|
34
|
+
# 应用ID
|
35
|
+
def agent_id
|
36
|
+
@source.AgentID
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.factory(hash)
|
40
|
+
case hash['MsgType']
|
41
|
+
when 'text'
|
42
|
+
TextMessage.new(hash)
|
43
|
+
when 'image'
|
44
|
+
ImageMessage.new(hash)
|
45
|
+
when 'location'
|
46
|
+
LocationMessage.new(hash)
|
47
|
+
when 'link'
|
48
|
+
LinkMessage.new(hash)
|
49
|
+
when 'event'
|
50
|
+
EventMessage.new(hash)
|
51
|
+
when 'voice'
|
52
|
+
VoiceMessage.new(hash)
|
53
|
+
when 'video'
|
54
|
+
VideoMessage.new(hash)
|
55
|
+
else
|
56
|
+
raise ArgumentError, 'Unknown Message'
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
# <xml>
|
63
|
+
# <ToUserName><![CDATA[toUser]]></ToUserName>
|
64
|
+
# <FromUserName><![CDATA[fromUser]]></FromUserName>
|
65
|
+
# <CreateTime>1348831860</CreateTime>
|
66
|
+
# <MsgType><![CDATA[text]]></MsgType>
|
67
|
+
# <Content><![CDATA[this is a test]]></Content>
|
68
|
+
# <MsgId>1234567890123456</MsgId>
|
69
|
+
# </xml>
|
70
|
+
TextMessage = Class.new(Message)
|
71
|
+
|
72
|
+
# <xml>
|
73
|
+
# <ToUserName><![CDATA[toUser]]></ToUserName>
|
74
|
+
# <FromUserName><![CDATA[fromUser]]></FromUserName>
|
75
|
+
# <CreateTime>1348831860</CreateTime>
|
76
|
+
# <MsgType><![CDATA[image]]></MsgType>
|
77
|
+
# <PicUrl><![CDATA[this is a url]]></PicUrl>
|
78
|
+
# <MsgId>1234567890123456</MsgId>
|
79
|
+
# </xml>
|
80
|
+
ImageMessage = Class.new(Message)
|
81
|
+
|
82
|
+
# <xml>
|
83
|
+
# <ToUserName><![CDATA[toUser]]></ToUserName>
|
84
|
+
# <FromUserName><![CDATA[fromUser]]></FromUserName>
|
85
|
+
# <CreateTime>1351776360</CreateTime>
|
86
|
+
# <MsgType><![CDATA[link]]></MsgType>
|
87
|
+
# <Title><![CDATA[公众平台官网链接]]></Title>
|
88
|
+
# <Description><![CDATA[公众平台官网链接]]></Description>
|
89
|
+
# <Url><![CDATA[url]]></Url>
|
90
|
+
# <MsgId>1234567890123456</MsgId>
|
91
|
+
# </xml>
|
92
|
+
LinkMessage = Class.new(Message)
|
93
|
+
|
94
|
+
# <xml>
|
95
|
+
# <ToUserName><![CDATA[toUser]]></ToUserName>
|
96
|
+
# <FromUserName><![CDATA[FromUser]]></FromUserName>
|
97
|
+
# <CreateTime>123456789</CreateTime>
|
98
|
+
# <MsgType><![CDATA[event]]></MsgType>
|
99
|
+
# <Event><![CDATA[EVENT]]></Event>
|
100
|
+
# <EventKey><![CDATA[EVENTKEY]]></EventKey>
|
101
|
+
# </xml>
|
102
|
+
EventMessage = Class.new(Message)
|
103
|
+
|
104
|
+
# <xml>
|
105
|
+
# <ToUserName><![CDATA[toUser]]></ToUserName>
|
106
|
+
# <FromUserName><![CDATA[fromUser]]></FromUserName>
|
107
|
+
# <CreateTime>1351776360</CreateTime>
|
108
|
+
# <MsgType><![CDATA[location]]></MsgType>
|
109
|
+
# <Location_X>23.134521</Location_X>
|
110
|
+
# <Location_Y>113.358803</Location_Y>
|
111
|
+
# <Scale>20</Scale>
|
112
|
+
# <Label><![CDATA[位置信息]]></Label>
|
113
|
+
# <MsgId>1234567890123456</MsgId>
|
114
|
+
# </xml>
|
115
|
+
class LocationMessage < Message
|
116
|
+
|
117
|
+
def Location_X
|
118
|
+
@source.Location_X.to_f
|
119
|
+
end
|
120
|
+
|
121
|
+
def Location_Y
|
122
|
+
@source.Location_Y.to_f
|
123
|
+
end
|
124
|
+
|
125
|
+
def Scale
|
126
|
+
@source.Scale.to_i
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# <xml>
|
131
|
+
# <ToUserName><![CDATA[toUser]]></ToUserName>
|
132
|
+
# <FromUserName><![CDATA[fromUser]]></FromUserName>
|
133
|
+
# <CreateTime>1376632760</CreateTime>
|
134
|
+
# <MsgType><![CDATA[voice]]></MsgType>
|
135
|
+
# <MediaId><![CDATA[Qyb0tgux6QLjhL6ipvFZJ-kUt2tcQtkn0BU365Vt3wUAtqfGam4QpZU35RXVhv6G]]></MediaId>
|
136
|
+
# <Format><![CDATA[amr]]></Format>
|
137
|
+
# <MsgId>5912592682802219078</MsgId>
|
138
|
+
# <Recognition><![CDATA[]]></Recognition>
|
139
|
+
# </xml>
|
140
|
+
class VoiceMessage < Message
|
141
|
+
|
142
|
+
def MediaId
|
143
|
+
@source.MediaId
|
144
|
+
end
|
145
|
+
|
146
|
+
def Format
|
147
|
+
@source.Format
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# <xml>
|
152
|
+
# <ToUserName><![CDATA[toUser]]></ToUserName>
|
153
|
+
# <FromUserName><![CDATA[fromUser]]></FromUserName>
|
154
|
+
# <CreateTime>1376632994</CreateTime>
|
155
|
+
# <MsgType><![CDATA[video]]></MsgType>
|
156
|
+
# <MediaId><![CDATA[TAAGb6iS5LcZR1d5ICiZTWGWi6-Upic9tlWDpAKcNJA]]></MediaId>
|
157
|
+
# <ThumbMediaId><![CDATA[U-xulPW4kq6KKMWFNaBSPc65Bcgr7Qopwex0DfCeyQs]]></ThumbMediaId>
|
158
|
+
# <MsgId>5912593687824566343</MsgId>
|
159
|
+
# </xml>
|
160
|
+
class VideoMessage < Message
|
161
|
+
|
162
|
+
def MediaId
|
163
|
+
@source.MediaId
|
164
|
+
end
|
165
|
+
|
166
|
+
def ThumbMediaId
|
167
|
+
@source.ThumbMediaId
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# ref: https://github.com/wolfg1969/rack-weixin/lib/weixin/model.rb
|
3
|
+
require 'roxml'
|
4
|
+
|
5
|
+
module QyWechat
|
6
|
+
|
7
|
+
class ResponseMessage
|
8
|
+
include ROXML
|
9
|
+
xml_name :xml
|
10
|
+
|
11
|
+
xml_accessor :ToUserName, :cdata => true
|
12
|
+
xml_accessor :FromUserName, :cdata => true
|
13
|
+
xml_reader :CreateTime, :as => Integer
|
14
|
+
xml_reader :MsgType, :cdata => true
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@CreateTime = Time.now.to_i
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_xml
|
21
|
+
super.to_xml(:encoding => 'UTF-8', :indent => 0, :save_with => 0)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# <xml>
|
26
|
+
# <ToUserName><![CDATA[toUser]]></ToUserName>
|
27
|
+
# <FromUserName><![CDATA[fromUser]]></FromUserName>
|
28
|
+
# <CreateTime>12345678</CreateTime>
|
29
|
+
# <MsgType><![CDATA[text]]></MsgType>
|
30
|
+
# <Content><![CDATA[Hello]]></Content>
|
31
|
+
# </xml>
|
32
|
+
|
33
|
+
class TextResponseMessage < ResponseMessage
|
34
|
+
xml_accessor :Content, :cdata => true
|
35
|
+
def initialize
|
36
|
+
super
|
37
|
+
@MsgType = 'text'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Article
|
42
|
+
include ROXML
|
43
|
+
xml_accessor :Title, :cdata => true
|
44
|
+
xml_accessor :Description, :cdata => true
|
45
|
+
xml_accessor :PicUrl, :cdata => true
|
46
|
+
xml_accessor :Url, :cdata => true
|
47
|
+
end
|
48
|
+
|
49
|
+
# <xml>
|
50
|
+
# <ToUserName><![CDATA[toUser]]></ToUserName>
|
51
|
+
# <FromUserName><![CDATA[fromUser]]></FromUserName>
|
52
|
+
# <CreateTime>12345678</CreateTime>
|
53
|
+
# <MsgType><![CDATA[news]]></MsgType>
|
54
|
+
# <ArticleCount>2</ArticleCount>
|
55
|
+
# <Articles>
|
56
|
+
# <item>
|
57
|
+
# <Title><![CDATA[title1]]></Title>
|
58
|
+
# <Description><![CDATA[description1]]></Description>
|
59
|
+
# <PicUrl><![CDATA[picurl]]></PicUrl>
|
60
|
+
# <Url><![CDATA[url]]></Url>
|
61
|
+
# </item>
|
62
|
+
# <item>
|
63
|
+
# <Title><![CDATA[title]]></Title>
|
64
|
+
# <Description><![CDATA[description]]></Description>
|
65
|
+
# <PicUrl><![CDATA[picurl]]></PicUrl>
|
66
|
+
# <Url><![CDATA[url]]></Url>
|
67
|
+
# </item>
|
68
|
+
# </Articles>
|
69
|
+
# </xml>
|
70
|
+
|
71
|
+
class NewsResponseMessage < ResponseMessage
|
72
|
+
xml_accessor :ArticleCount, :as => Integer
|
73
|
+
xml_accessor :Articles, :as => [Article], :in => 'Articles', :from => 'item'
|
74
|
+
def initialize
|
75
|
+
super
|
76
|
+
@MsgType = 'news'
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# <xml>
|
81
|
+
# <ToUserName><![CDATA[toUser]]></ToUserName>
|
82
|
+
# <FromUserName><![CDATA[fromUser]]></FromUserName>
|
83
|
+
# <CreateTime>12345678</CreateTime>
|
84
|
+
# <MsgType><![CDATA[video]]></MsgType>
|
85
|
+
# <Video>
|
86
|
+
# <MediaId><![CDATA[media_id]]></MediaId>
|
87
|
+
# <Title><![CDATA[title]]></Title>
|
88
|
+
# <Description><![CDATA[description]]></Description>
|
89
|
+
# </Video>
|
90
|
+
# </xml>
|
91
|
+
|
92
|
+
class Video
|
93
|
+
include ROXML
|
94
|
+
xml_accessor :MediaId, :cdata => true
|
95
|
+
xml_accessor :Description, :cdata => true
|
96
|
+
xml_accessor :Title, :cdata => true
|
97
|
+
end
|
98
|
+
|
99
|
+
class VideoResponseMessage < ResponseMessage
|
100
|
+
xml_accessor :Video, :as => Video
|
101
|
+
def initialize
|
102
|
+
super
|
103
|
+
@MsgType = 'video'
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# <xml>
|
108
|
+
# <ToUserName><![CDATA[toUser]]></ToUserName>
|
109
|
+
# <FromUserName><![CDATA[fromUser]]></FromUserName>
|
110
|
+
# <CreateTime>12345678</CreateTime>
|
111
|
+
# <MsgType><![CDATA[voice]]></MsgType>
|
112
|
+
# <Voice>
|
113
|
+
# <MediaId><![CDATA[media_id]]></MediaId>
|
114
|
+
# </Voice>
|
115
|
+
# </xml>
|
116
|
+
class Voice
|
117
|
+
include ROXML
|
118
|
+
xml_accessor :MediaId, :cdata => true
|
119
|
+
end
|
120
|
+
|
121
|
+
class VoiceResponseMessage < ResponseMessage
|
122
|
+
xml_accessor :Voice, :as => Voice
|
123
|
+
def initialize
|
124
|
+
super
|
125
|
+
@MsgType = 'voice'
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# <xml>
|
130
|
+
# <ToUserName><![CDATA[toUser]]></ToUserName>
|
131
|
+
# <FromUserName><![CDATA[fromUser]]></FromUserName>
|
132
|
+
# <CreateTime>12345678</CreateTime>
|
133
|
+
# <MsgType><![CDATA[image]]></MsgType>
|
134
|
+
# <Image>
|
135
|
+
# <MediaId><![CDATA[media_id]]></MediaId>
|
136
|
+
# </Image>
|
137
|
+
# </xml>
|
138
|
+
|
139
|
+
class Image
|
140
|
+
include ROXML
|
141
|
+
xml_accessor :MediaId, :cdata => true
|
142
|
+
end
|
143
|
+
|
144
|
+
class ImageResponseMessage < ResponseMessage
|
145
|
+
xml_accessor :Image, :as => Image
|
146
|
+
def initialize
|
147
|
+
super
|
148
|
+
@MsgType = 'image'
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
data/lib/qy_wechat.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'roxml'
|
2
|
+
require 'multi_xml'
|
3
|
+
require 'ostruct'
|
4
|
+
|
5
|
+
require "qy_wechat/configuration"
|
6
|
+
require "qy_wechat/engine"
|
7
|
+
require "qy_wechat/message/encrypt_message"
|
8
|
+
require "qy_wechat/message/message"
|
9
|
+
require "qy_wechat/message/response_message"
|
10
|
+
|
11
|
+
require "qy_wechat/helpers/pkcs7_encoder"
|
12
|
+
require "qy_wechat/helpers/prpcrypt"
|
13
|
+
require "qy_wechat/helpers/reply_message_helper"
|
14
|
+
|
15
|
+
module QyWechat
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: qy_wechat
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0.beta1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- lanrion
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-11-04 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: railties
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.1'
|
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.1
|
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.1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rails
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.1'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.1'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: multi_xml
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.5.2
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.5.2
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: roxml
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: "微信企业版本,应答API集成"
|
84
|
+
email:
|
85
|
+
- huaitao-deng@foxmail.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- MIT-LICENSE
|
91
|
+
- Rakefile
|
92
|
+
- app/controllers/qy_wechat/qy_wechat_controller.rb
|
93
|
+
- config/routes.rb
|
94
|
+
- lib/generators/qy_wechat/install_generator.rb
|
95
|
+
- lib/generators/qy_wechat/migration_generator.rb
|
96
|
+
- lib/generators/templates/add_qy_wechat_columns.rb
|
97
|
+
- lib/generators/templates/qy_wechat_config.rb
|
98
|
+
- lib/generators/templates/qy_wechat_controller.rb
|
99
|
+
- lib/qy_wechat.rb
|
100
|
+
- lib/qy_wechat/configuration.rb
|
101
|
+
- lib/qy_wechat/engine.rb
|
102
|
+
- lib/qy_wechat/helpers/pkcs7_encoder.rb
|
103
|
+
- lib/qy_wechat/helpers/prpcrypt.rb
|
104
|
+
- lib/qy_wechat/helpers/reply_message_helper.rb
|
105
|
+
- lib/qy_wechat/message/encrypt_message.rb
|
106
|
+
- lib/qy_wechat/message/message.rb
|
107
|
+
- lib/qy_wechat/message/response_message.rb
|
108
|
+
- lib/qy_wechat/version.rb
|
109
|
+
homepage: https://github.com/lanrion/qy_wechat
|
110
|
+
licenses:
|
111
|
+
- MIT
|
112
|
+
metadata: {}
|
113
|
+
post_install_message:
|
114
|
+
rdoc_options: []
|
115
|
+
require_paths:
|
116
|
+
- lib
|
117
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - ">"
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: 1.3.1
|
127
|
+
requirements: []
|
128
|
+
rubyforge_project:
|
129
|
+
rubygems_version: 2.2.2
|
130
|
+
signing_key:
|
131
|
+
specification_version: 4
|
132
|
+
summary: "微信企业版本"
|
133
|
+
test_files: []
|
134
|
+
has_rdoc:
|