gfd_wechat 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +321 -0
- data/LICENSE +21 -0
- data/README-CN.md +815 -0
- data/README.md +844 -0
- data/bin/wechat +520 -0
- data/lib/action_controller/wechat_responder.rb +72 -0
- data/lib/generators/wechat/config_generator.rb +36 -0
- data/lib/generators/wechat/install_generator.rb +20 -0
- data/lib/generators/wechat/menu_generator.rb +21 -0
- data/lib/generators/wechat/redis_store_generator.rb +16 -0
- data/lib/generators/wechat/session_generator.rb +36 -0
- data/lib/generators/wechat/templates/MENU_README +3 -0
- data/lib/generators/wechat/templates/app/controllers/wechats_controller.rb +12 -0
- data/lib/generators/wechat/templates/app/models/wechat_config.rb +46 -0
- data/lib/generators/wechat/templates/app/models/wechat_session.rb +17 -0
- data/lib/generators/wechat/templates/config/initializers/wechat_redis_store.rb +42 -0
- data/lib/generators/wechat/templates/config/wechat.yml +72 -0
- data/lib/generators/wechat/templates/config/wechat_menu.yml +6 -0
- data/lib/generators/wechat/templates/config/wechat_menu_android.yml +15 -0
- data/lib/generators/wechat/templates/db/config_migration.rb.erb +40 -0
- data/lib/generators/wechat/templates/db/session_migration.rb.erb +10 -0
- data/lib/wechat/api.rb +54 -0
- data/lib/wechat/api_base.rb +63 -0
- data/lib/wechat/api_loader.rb +145 -0
- data/lib/wechat/cipher.rb +66 -0
- data/lib/wechat/concern/common.rb +217 -0
- data/lib/wechat/controller_api.rb +96 -0
- data/lib/wechat/corp_api.rb +168 -0
- data/lib/wechat/helpers.rb +47 -0
- data/lib/wechat/http_client.rb +112 -0
- data/lib/wechat/message.rb +265 -0
- data/lib/wechat/mp_api.rb +46 -0
- data/lib/wechat/responder.rb +308 -0
- data/lib/wechat/signature.rb +10 -0
- data/lib/wechat/ticket/corp_jsapi_ticket.rb +14 -0
- data/lib/wechat/ticket/jsapi_base.rb +84 -0
- data/lib/wechat/ticket/public_jsapi_ticket.rb +14 -0
- data/lib/wechat/token/access_token_base.rb +53 -0
- data/lib/wechat/token/corp_access_token.rb +13 -0
- data/lib/wechat/token/public_access_token.rb +13 -0
- data/lib/wechat.rb +52 -0
- 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,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,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
|