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.
- 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
|