gfd_wechat 0.0.1

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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +321 -0
  3. data/LICENSE +21 -0
  4. data/README-CN.md +815 -0
  5. data/README.md +844 -0
  6. data/bin/wechat +520 -0
  7. data/lib/action_controller/wechat_responder.rb +72 -0
  8. data/lib/generators/wechat/config_generator.rb +36 -0
  9. data/lib/generators/wechat/install_generator.rb +20 -0
  10. data/lib/generators/wechat/menu_generator.rb +21 -0
  11. data/lib/generators/wechat/redis_store_generator.rb +16 -0
  12. data/lib/generators/wechat/session_generator.rb +36 -0
  13. data/lib/generators/wechat/templates/MENU_README +3 -0
  14. data/lib/generators/wechat/templates/app/controllers/wechats_controller.rb +12 -0
  15. data/lib/generators/wechat/templates/app/models/wechat_config.rb +46 -0
  16. data/lib/generators/wechat/templates/app/models/wechat_session.rb +17 -0
  17. data/lib/generators/wechat/templates/config/initializers/wechat_redis_store.rb +42 -0
  18. data/lib/generators/wechat/templates/config/wechat.yml +72 -0
  19. data/lib/generators/wechat/templates/config/wechat_menu.yml +6 -0
  20. data/lib/generators/wechat/templates/config/wechat_menu_android.yml +15 -0
  21. data/lib/generators/wechat/templates/db/config_migration.rb.erb +40 -0
  22. data/lib/generators/wechat/templates/db/session_migration.rb.erb +10 -0
  23. data/lib/wechat/api.rb +54 -0
  24. data/lib/wechat/api_base.rb +63 -0
  25. data/lib/wechat/api_loader.rb +145 -0
  26. data/lib/wechat/cipher.rb +66 -0
  27. data/lib/wechat/concern/common.rb +217 -0
  28. data/lib/wechat/controller_api.rb +96 -0
  29. data/lib/wechat/corp_api.rb +168 -0
  30. data/lib/wechat/helpers.rb +47 -0
  31. data/lib/wechat/http_client.rb +112 -0
  32. data/lib/wechat/message.rb +265 -0
  33. data/lib/wechat/mp_api.rb +46 -0
  34. data/lib/wechat/responder.rb +308 -0
  35. data/lib/wechat/signature.rb +10 -0
  36. data/lib/wechat/ticket/corp_jsapi_ticket.rb +14 -0
  37. data/lib/wechat/ticket/jsapi_base.rb +84 -0
  38. data/lib/wechat/ticket/public_jsapi_ticket.rb +14 -0
  39. data/lib/wechat/token/access_token_base.rb +53 -0
  40. data/lib/wechat/token/corp_access_token.rb +13 -0
  41. data/lib/wechat/token/public_access_token.rb +13 -0
  42. data/lib/wechat.rb +52 -0
  43. metadata +195 -0
@@ -0,0 +1,36 @@
1
+ require 'rails/generators/active_record'
2
+
3
+ module Wechat
4
+ module Generators
5
+ class SessionGenerator < Rails::Generators::Base
6
+ include ::Rails::Generators::Migration
7
+
8
+ desc 'Enable wechat session support'
9
+ source_root File.expand_path('../templates', __FILE__)
10
+
11
+ def copy_wechat_sessions_migration
12
+ migration_template(
13
+ 'db/session_migration.rb.erb',
14
+ 'db/migrate/create_wechat_sessions.rb',
15
+ {migration_version: migration_version}
16
+ )
17
+ end
18
+
19
+ def copy_wechat_session_model
20
+ template 'app/models/wechat_session.rb'
21
+ end
22
+
23
+ private
24
+
25
+ def self.next_migration_number(dirname)
26
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
27
+ end
28
+
29
+ def migration_version
30
+ if Rails.version >= '5.0.0'
31
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,3 @@
1
+ Run `wechat menu_create config\wechat_menu.yml` to uploading the default menu.
2
+ Run `rails g wechat:menu --conditional` to generate one example of conditional menu.
3
+ Run `wechat menu_addconditional config\wechat_menu_conditional.yml` to uploading the conditional menu.
@@ -0,0 +1,12 @@
1
+ <% if defined? ActionController::API -%>
2
+ class WechatsController < ApplicationController
3
+ <% else -%>
4
+ class WechatsController < ActionController::Base
5
+ <% end -%>
6
+ # For details on the DSL available within this file, see https://github.com/Eric-Guo/wechat#wechat_responder---rails-responder-controller-dsl
7
+ wechat_responder
8
+
9
+ on :text do |request, content|
10
+ request.reply.text "echo: #{content}" # Just echo
11
+ end
12
+ end
@@ -0,0 +1,46 @@
1
+ # Used by wechat gems, do not rename WechatConfig to other name,
2
+ # Feel free to inherit from other class like ActiveModel::Model
3
+ class WechatConfig < ActiveRecord::Base
4
+ validates :environment, presence: true
5
+ validates :account, presence: true, uniqueness: { scope: [:environment] }
6
+ validates :token, presence: true
7
+ validates :access_token, presence: true
8
+ validates :jsapi_ticket, presence: true
9
+ validates :encoding_aes_key, presence: { if: :encrypt_mode? }
10
+
11
+ validate :app_config_is_valid
12
+
13
+ ATTRIBUTES_TO_REMOVE = %w(environment account created_at updated_at enabled)
14
+
15
+ def self.get_all_configs(environment)
16
+ WechatConfig.where(environment: environment, enabled: true).inject({}) do |hash, config|
17
+ hash[config.account] = config.build_config_hash
18
+ hash
19
+ end
20
+ end
21
+
22
+ def build_config_hash
23
+ self.as_json(except: ATTRIBUTES_TO_REMOVE)
24
+ end
25
+
26
+ private
27
+
28
+ def app_config_is_valid
29
+ if self[:appid].present?
30
+ # public account
31
+ if self[:secret].blank?
32
+ errors.add(:secret, 'cannot be nil when appid is set')
33
+ end
34
+ elsif self[:corpid].present?
35
+ # corp account
36
+ if self[:corpsecret].blank?
37
+ errors.add(:corpsecret, 'cannot be nil when corpid is set')
38
+ end
39
+ if self[:agentid].blank?
40
+ errors.add(:agentid, 'cannot be nil when corpid is set')
41
+ end
42
+ else
43
+ errors[:base] << 'Either appid or corpid must be set'
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,17 @@
1
+ # Used by wechat gems, do not rename WechatSession to other name,
2
+ # Feel free to inherit from other class like ActiveModel::Model
3
+ class WechatSession < ActiveRecord::Base
4
+ validates :openid, presence: true, uniqueness: true
5
+ serialize :hash_store, Hash
6
+
7
+ # called by wechat gems when user request session
8
+ def self.find_or_initialize_session(request_message)
9
+ find_or_initialize_by(openid: request_message[:from_user_name])
10
+ end
11
+
12
+ # called by wechat gems after response Techent server at controller#create
13
+ def save_session(_response_message)
14
+ touch unless new_record? # Always refresh updated_at even no change
15
+ save!
16
+ end
17
+ end
@@ -0,0 +1,42 @@
1
+ module Wechat
2
+ def self.redis
3
+ # You can reuse existing redis connection and remove this method if require
4
+ @redis ||= Redis.new # more options see https://github.com/redis/redis-rb#getting-started
5
+ end
6
+
7
+ module Token
8
+ class AccessTokenBase
9
+ def read_token
10
+ JSON.parse(Wechat.redis.get(redis_key)) || {}
11
+ end
12
+
13
+ def write_token(token_hash)
14
+ Wechat.redis.set redis_key, token_hash.to_json
15
+ end
16
+
17
+ private
18
+
19
+ def redis_key
20
+ "my_app_wechat_token_#{self.appid}"
21
+ end
22
+ end
23
+ end
24
+
25
+ module Ticket
26
+ class JsapiBase
27
+ def read_ticket
28
+ JSON.parse(Wechat.redis.get(redis_key)) || {}
29
+ end
30
+
31
+ def write_ticket(ticket_hash)
32
+ Wechat.redis.set redis_key, ticket_hash.to_json
33
+ end
34
+
35
+ private
36
+
37
+ def redis_key
38
+ "my_app_wechat_ticket_#{self.access_token.appid}"
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,72 @@
1
+ default: &default
2
+ corpid: "corpid"
3
+ corpsecret: "corpsecret"
4
+ agentid: 1
5
+ # Or if using public account, only need above two line
6
+ # appid: "my_appid"
7
+ # secret: "my_secret"
8
+ token: "my_token"
9
+ access_token: "C:/Users/[username]/wechat_access_token"
10
+ encrypt_mode: false # if true must fill encoding_aes_key
11
+ encoding_aes_key: "my_encoding_aes_key"
12
+ jsapi_ticket: "C:/Users/[user_name]/wechat_jsapi_ticket"
13
+
14
+ production:
15
+ corpid: <%%= ENV['WECHAT_CORPID'] %>
16
+ corpsecret: <%%= ENV['WECHAT_CORPSECRET'] %>
17
+ agentid: <%%= ENV['WECHAT_AGENTID'] %>
18
+ # Or if using public account, only need above two line
19
+ # appid: <%= ENV['WECHAT_APPID'] %>
20
+ # secret: <%= ENV['WECHAT_APP_SECRET'] %>
21
+ token: <%%= ENV['WECHAT_TOKEN'] %>
22
+ timeout: 30,
23
+ skip_verify_ssl: true
24
+ access_token: <%%= ENV['WECHAT_ACCESS_TOKEN'] %>
25
+ encrypt_mode: false # if true must fill encoding_aes_key
26
+ encoding_aes_key: <%%= ENV['WECHAT_ENCODING_AES_KEY'] %>
27
+ jsapi_ticket: <%%= ENV['WECHAT_JSAPI_TICKET'] %>
28
+ oauth2_cookie_duration: <%%= ENV['WECHAT_OAUTH2_COOKIE_DURATION'] %> # seconds
29
+
30
+ development:
31
+ <<: *default
32
+ trusted_domain_fullname: "http://your_dev.proxy.qqbrowser.cc"
33
+
34
+ test:
35
+ <<: *default
36
+
37
+ # Multiple Accounts
38
+ #
39
+ # wx2_development:
40
+ # <<: *default
41
+ # appid: "my_appid"
42
+ # secret: "my_secret"
43
+ # access_token: "tmp/wechat_access_token2"
44
+ # jsapi_ticket: "tmp/wechat_jsapi_ticket2"
45
+ #
46
+ # wx2_test:
47
+ # <<: *default
48
+ # appid: "my_appid"
49
+ # secret: "my_secret"
50
+ #
51
+ # wx2_production:
52
+ # <<: *default
53
+ # appid: "my_appid"
54
+ # secret: "my_secret"
55
+ #
56
+ # wx3_development:
57
+ # <<: *default
58
+ # appid: "my_appid"
59
+ # secret: "my_secret"
60
+ # access_token: "tmp/wechat_access_token3"
61
+ # jsapi_ticket: "tmp/wechat_jsapi_ticket3"
62
+ #
63
+ # wx3_test:
64
+ # <<: *default
65
+ # appid: "my_appid"
66
+ # secret: "my_secret"
67
+ #
68
+ # wx3_production:
69
+ # <<: *default
70
+ # appid: "my_appid"
71
+ # secret: "my_secret"
72
+ #
@@ -0,0 +1,6 @@
1
+ # More option see https://github.com/Eric-Guo/wechat#menu-create
2
+ button:
3
+ -
4
+ type: "view"
5
+ name: "Testing"
6
+ url: "http://xxxxx.proxy.qqbrowser.cc"
@@ -0,0 +1,15 @@
1
+ button:
2
+ -
3
+ type: "view"
4
+ name: "Testing Android"
5
+ url: "http://xxxxx.proxy.qqbrowser.cc"
6
+
7
+ # More match rule see http://mp.weixin.qq.com/wiki/0/c48ccd12b69ae023159b4bfaa7c39c20.html
8
+ matchrule:
9
+ # group_id: 2
10
+ # sex: 1
11
+ # country: 中国
12
+ # province: 上海
13
+ # city: 杨浦
14
+ client_platform_type: 1
15
+ # language: zh_CN
@@ -0,0 +1,40 @@
1
+ class CreateWechatConfigs < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :wechat_configs do |t|
4
+ # config environment, typical values: production, development or test
5
+ t.string :environment, null: false, default: 'development'
6
+ # account name
7
+ t.string :account, null: false
8
+ # whether this config is activated
9
+ t.boolean :enabled, default: true
10
+
11
+ # public account
12
+ t.string :appid
13
+ t.string :secret
14
+
15
+ # corp account
16
+ t.string :corpid
17
+ t.string :corpsecret
18
+ t.integer :agentid
19
+
20
+ # when encrypt_mode is true, encoding_aes_key must be specified
21
+ t.boolean :encrypt_mode
22
+ t.string :encoding_aes_key
23
+
24
+ # app token
25
+ t.string :token, null: false
26
+ # path to access token storage file
27
+ t.string :access_token, null: false
28
+ # path to jsapi ticket storage file
29
+ t.string :jsapi_ticket, null: false
30
+ # set to false if RestClient::SSLCertificateNotVerified is thrown
31
+ t.boolean :skip_verify_ssl, default: true
32
+ t.integer :timeout, default: 20
33
+ t.string :trusted_domain_fullname
34
+
35
+ t.timestamps null: false
36
+ end
37
+
38
+ add_index :wechat_configs, [:environment, :account], unique: true, length: {environment: 20, account: 100}
39
+ end
40
+ end
@@ -0,0 +1,10 @@
1
+ class CreateWechatSessions < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :wechat_sessions do |t|
4
+ t.string :openid, null: false
5
+ t.string :hash_store
6
+ t.timestamps null: false
7
+ end
8
+ add_index :wechat_sessions, :openid, unique: true
9
+ end
10
+ end
data/lib/wechat/api.rb ADDED
@@ -0,0 +1,54 @@
1
+ require 'wechat/api_base'
2
+ require 'wechat/http_client'
3
+ require 'wechat/token/public_access_token'
4
+ require 'wechat/ticket/public_jsapi_ticket'
5
+ require 'wechat/concern/common'
6
+
7
+ module Wechat
8
+ class Api < ApiBase
9
+ include Concern::Common
10
+ DEVICE_BASE = 'https://api.weixin.qq.com/device/'.freeze
11
+
12
+ def qrcode_create_scene_str(scene_id, expire_seconds = 604800)
13
+ post 'qrcode/create', JSON.generate(expire_seconds: expire_seconds,
14
+ action_name: 'QR_STR_SCENE',
15
+ action_info: { scene: { scene_str: scene_id } })
16
+ end
17
+
18
+ def template_message_send(message)
19
+ post 'message/template/send', message.to_json, content_type: :json
20
+ end
21
+
22
+ def list_message_template
23
+ get 'template/get_all_private_template'
24
+ end
25
+
26
+ def add_message_template(template_id_short)
27
+ post 'template/api_add_template', JSON.generate(template_id_short: template_id_short)
28
+ end
29
+
30
+ def del_message_template(template_id)
31
+ post 'template/del_private_template', JSON.generate(template_id: template_id)
32
+ end
33
+
34
+ def device_bind(open_id, device_id)
35
+ client.post 'compel_bind', JSON.generate(device_id: device_id,
36
+ openid:open_id), base: DEVICE_BASE, access_token: access_token.token
37
+ end
38
+
39
+ def device_unbind(open_id, device_id)
40
+ client.post 'compel_unbind', JSON.generate(device_id: device_id,
41
+ openid:open_id), base: DEVICE_BASE, access_token: access_token.token
42
+ end
43
+
44
+ def device_transmsg(device_type, open_id, device_id, content)
45
+ client.post 'transmsg', JSON.generate(device_type: device_type, open_id: open_id,
46
+ device_id: device_id, content: content), base: DEVICE_BASE, access_token: access_token.token
47
+ end
48
+
49
+ def device_authorize(device_num, device_list, op_type, product_id)
50
+ client.post 'authorize_device', JSON.generate(device_num: device_num, device_list: device_list,
51
+ op_type: op_type, product_id: product_id), base: DEVICE_BASE, access_token: access_token.token
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,63 @@
1
+ module Wechat
2
+ class ApiBase
3
+ attr_reader :access_token, :client, :jsapi_ticket
4
+
5
+ MP_BASE = 'https://mp.weixin.qq.com/cgi-bin/'.freeze
6
+
7
+ def callbackip
8
+ get 'getcallbackip'
9
+ end
10
+
11
+ def qrcode(ticket)
12
+ client.get 'showqrcode', params: { ticket: ticket }, base: MP_BASE, as: :file
13
+ end
14
+
15
+ def media(media_id)
16
+ get 'media/get', params: { media_id: media_id }, as: :file
17
+ end
18
+
19
+ def media_hq(media_id)
20
+ get 'media/get/jssdk', params: { media_id: media_id }, as: :file
21
+ end
22
+
23
+ def media_create(type, file)
24
+ post_file 'media/upload', file, params: { type: type }
25
+ end
26
+
27
+ def media_uploadimg(file)
28
+ post_file 'media/uploadimg', file
29
+ end
30
+
31
+ def media_uploadnews(mpnews_message)
32
+ post 'media/uploadnews', mpnews_message.to_json
33
+ end
34
+
35
+ protected
36
+
37
+ def get(path, headers = {})
38
+ with_access_token(headers[:params]) do |params|
39
+ client.get path, headers.merge(params: params)
40
+ end
41
+ end
42
+
43
+ def post(path, payload, headers = {})
44
+ with_access_token(headers[:params]) do |params|
45
+ client.post path, payload, headers.merge(params: params)
46
+ end
47
+ end
48
+
49
+ def post_file(path, file, headers = {})
50
+ with_access_token(headers[:params]) do |params|
51
+ client.post_file path, file, headers.merge(params: params)
52
+ end
53
+ end
54
+
55
+ def with_access_token(params = {}, tries = 2)
56
+ params ||= {}
57
+ yield(params.merge(access_token: access_token.token))
58
+ rescue AccessTokenExpiredError
59
+ access_token.refresh
60
+ retry unless (tries -= 1).zero?
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,145 @@
1
+ module Wechat
2
+ module ApiLoader
3
+ def self.with(options)
4
+ account = options[:account] || :default
5
+ c = ApiLoader.config(account)
6
+
7
+ token_file = options[:token_file] || c.access_token.presence || '/var/tmp/wechat_access_token'
8
+ js_token_file = options[:js_token_file] || c.jsapi_ticket.presence || '/var/tmp/wechat_jsapi_ticket'
9
+ type = options[:type] || c.type
10
+ if c.appid && c.secret && token_file.present?
11
+ wx_class = (type == 'mp') ? Wechat::MpApi : Wechat::Api
12
+ wx_class.new(c.appid, c.secret, token_file, c.timeout, c.skip_verify_ssl, js_token_file)
13
+ elsif c.corpid && c.corpsecret && token_file.present?
14
+ Wechat::CorpApi.new(c.corpid, c.corpsecret, token_file, c.agentid, c.timeout, c.skip_verify_ssl, js_token_file)
15
+ else
16
+ raise "Need create ~/.wechat.yml with wechat appid and secret or running at rails root folder so wechat can read config/wechat.yml"
17
+ end
18
+ end
19
+
20
+ @configs = nil
21
+
22
+ def self.config(account = :default)
23
+ account = :default if account.nil?
24
+ @configs ||= loading_config!
25
+ @configs[account.to_sym] || raise("Wechat configuration for #{account} is missing.")
26
+ end
27
+
28
+ def self.reload_config!
29
+ @configs = loading_config!
30
+ end
31
+
32
+ private_class_method def self.loading_config!
33
+ configs = config_from_file || config_from_environment
34
+ configs.merge!(config_from_db)
35
+
36
+ configs.symbolize_keys!
37
+ configs.each do |key, cfg|
38
+ if cfg.is_a?(Hash)
39
+ cfg.symbolize_keys!
40
+ else
41
+ raise "wrong wechat configuration format for #{key}"
42
+ end
43
+ end
44
+
45
+ if defined?(::Rails)
46
+ configs.each do |_, cfg|
47
+ cfg[:access_token] ||= Rails.root.try(:join, 'tmp/access_token').try(:to_path)
48
+ cfg[:jsapi_ticket] ||= Rails.root.try(:join, 'tmp/jsapi_ticket').try(:to_path)
49
+ end
50
+ end
51
+
52
+ configs.each do |_, cfg|
53
+ cfg[:timeout] ||= 20
54
+ cfg[:have_session_class] = class_exists?('WechatSession')
55
+ cfg[:oauth2_cookie_duration] ||= 1.hour
56
+ end
57
+
58
+ # create config object using raw config data
59
+ cfg_objs = {}
60
+ configs.each do |account, cfg|
61
+ cfg_objs[account] = OpenStruct.new(cfg)
62
+ end
63
+ cfg_objs
64
+ end
65
+
66
+ private_class_method def self.config_from_db
67
+ unless class_exists?('WechatConfig')
68
+ return {}
69
+ end
70
+
71
+ environment = defined?(::Rails) ? Rails.env.to_s : ENV['RAILS_ENV'] || 'development'
72
+ WechatConfig.get_all_configs(environment)
73
+ end
74
+
75
+ private_class_method def self.config_from_file
76
+ if defined?(::Rails)
77
+ config_file = ENV['WECHAT_CONF_FILE'] || Rails.root.join('config/wechat.yml')
78
+ return resovle_config_file(config_file, Rails.env.to_s)
79
+ else
80
+ rails_config_file = ENV['WECHAT_CONF_FILE'] || File.join(Dir.getwd, 'config/wechat.yml')
81
+ application_config_file = File.join(Dir.getwd, 'config/application.yml')
82
+ home_config_file = File.join(Dir.home, '.wechat.yml')
83
+ if File.exist?(rails_config_file)
84
+ rails_env = ENV['RAILS_ENV'] || 'development'
85
+ if File.exist?(application_config_file) && !defined?(::Figaro)
86
+ require 'figaro'
87
+ Figaro::Application.new(path: application_config_file, environment: rails_env).load
88
+ end
89
+ config = resovle_config_file(rails_config_file, rails_env)
90
+ if config.present? && (default = config[:default]) && (default['appid'] || default['corpid'])
91
+ puts "Using rails project #{ENV['WECHAT_CONF_FILE'] || "config/wechat.yml"} #{rails_env} setting..."
92
+ return config
93
+ end
94
+ end
95
+ if File.exist?(home_config_file)
96
+ return resovle_config_file(home_config_file, nil)
97
+ end
98
+ end
99
+ end
100
+
101
+ private_class_method def self.resovle_config_file(config_file, env)
102
+ if File.exist?(config_file)
103
+ raw_data = YAML.load(ERB.new(File.read(config_file)).result)
104
+ configs = {}
105
+ if env
106
+ # Process multiple accounts when env is given
107
+ raw_data.each do |key, value|
108
+ if key == env
109
+ configs[:default] = value
110
+ elsif m = /(.*?)_#{env}$/.match(key)
111
+ configs[m[1].to_sym] = value
112
+ end
113
+ end
114
+ else
115
+ # Treat is as one account when env is omitted
116
+ configs[:default] = raw_data
117
+ end
118
+ configs
119
+ end
120
+ end
121
+
122
+ private_class_method def self.config_from_environment
123
+ value = { appid: ENV['WECHAT_APPID'],
124
+ secret: ENV['WECHAT_SECRET'],
125
+ corpid: ENV['WECHAT_CORPID'],
126
+ corpsecret: ENV['WECHAT_CORPSECRET'],
127
+ agentid: ENV['WECHAT_AGENTID'],
128
+ token: ENV['WECHAT_TOKEN'],
129
+ access_token: ENV['WECHAT_ACCESS_TOKEN'],
130
+ encrypt_mode: ENV['WECHAT_ENCRYPT_MODE'],
131
+ timeout: ENV['WECHAT_TIMEOUT'],
132
+ skip_verify_ssl: ENV['WECHAT_SKIP_VERIFY_SSL'],
133
+ encoding_aes_key: ENV['WECHAT_ENCODING_AES_KEY'],
134
+ jsapi_ticket: ENV['WECHAT_JSAPI_TICKET'],
135
+ trusted_domain_fullname: ENV['WECHAT_TRUSTED_DOMAIN_FULLNAME'] }
136
+ {default: value}
137
+ end
138
+
139
+ private_class_method def self.class_exists?(class_name)
140
+ return Module.const_get(class_name).present?
141
+ rescue NameError
142
+ return false
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,66 @@
1
+ module Wechat
2
+ module Cipher
3
+ BLOCK_SIZE = 32
4
+ CIPHER = 'AES-256-CBC'.freeze
5
+
6
+ def encrypt(plain, encoding_aes_key)
7
+ cipher = OpenSSL::Cipher.new(CIPHER)
8
+ cipher.encrypt
9
+
10
+ cipher.padding = 0
11
+ key_data = Base64.decode64(encoding_aes_key + '=')
12
+ cipher.key = key_data
13
+ cipher.iv = [key_data].pack('H*')
14
+
15
+ cipher.update(plain) + cipher.final
16
+ end
17
+
18
+ def decrypt(msg, encoding_aes_key)
19
+ cipher = OpenSSL::Cipher.new(CIPHER)
20
+ cipher.decrypt
21
+
22
+ cipher.padding = 0
23
+ key_data = Base64.decode64(encoding_aes_key + '=')
24
+ cipher.key = key_data
25
+ cipher.iv = [key_data].pack('H*')
26
+
27
+ plain = cipher.update(msg) + cipher.final
28
+ decode_padding(plain)
29
+ end
30
+
31
+ # app_id or corp_id
32
+ def pack(content, app_id)
33
+ random = SecureRandom.hex(8)
34
+ text = content.force_encoding('ASCII-8BIT')
35
+ msg_len = [text.length].pack('N')
36
+
37
+ encode_padding("#{random}#{msg_len}#{text}#{app_id}")
38
+ end
39
+
40
+ def unpack(msg)
41
+ msg = decode_padding(msg)
42
+ msg_len = msg[16, 4].reverse.unpack('V')[0]
43
+ content = msg[20, msg_len]
44
+ app_id = msg[(20 + msg_len)..-1]
45
+
46
+ [content, app_id]
47
+ end
48
+
49
+ private
50
+
51
+ def encode_padding(data)
52
+ length = data.bytes.length
53
+ amount_to_pad = BLOCK_SIZE - (length % BLOCK_SIZE)
54
+ amount_to_pad = BLOCK_SIZE if amount_to_pad == 0
55
+ padding = ([amount_to_pad].pack('c') * amount_to_pad)
56
+ data + padding
57
+ end
58
+
59
+ def decode_padding(plain)
60
+ pad = plain.bytes[-1]
61
+ # no padding
62
+ pad = 0 if pad < 1 || pad > BLOCK_SIZE
63
+ plain[0...(plain.length - pad)]
64
+ end
65
+ end
66
+ end