gfd_wechat 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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