将wechat放到项目里面

chenlw_dev v20160426_01
guange 9 years ago
parent 5a60702412
commit cc9825e6a2

@ -6,7 +6,7 @@ unless RUBY_PLATFORM =~ /w32/
gem 'iconv'
end
gem 'wechat',git: 'https://github.com/guange2015/wechat.git'
gem 'wechat',path: 'lib/wechat'
gem 'grack', path:'lib/grack'
gem 'gitlab', path: 'lib/gitlab-cli'
gem 'rest-client'

@ -1,4 +1,4 @@
source "http://rubygems.org"
source "https://rubygems.org"
gemspec

@ -0,0 +1,6 @@
# Save as .codeclimate.yml (note leading .) in project root directory
languages:
Ruby: true
exclude_paths:
- "lib/generators/wechat/templates/app/controllers/wechats_controller.rb"

@ -0,0 +1,17 @@
Documentation:
Enabled: false
Metrics/LineLength:
Max: 150
Metrics/AbcSize:
Max: 37
Metrics/ClassLength:
Max: 150
Metrics/MethodLength:
Max: 15
Style/NumericLiterals:
MinDigits: 7

@ -0,0 +1,22 @@
language: ruby
sudo: false
rvm:
- 2.0.0
- 2.1
- 2.2
- 2.3
matrix:
allow_failures:
- rvm: 2.3
install:
- "travis_retry bundle config build.nokogiri --use-system-libraries"
- "travis_retry bundle install --retry 3"
script: bundle exec rake
git:
depth: 10

@ -0,0 +1,121 @@
# Changelog
## v0.7.1 (released at 1/11/2016)
* Fix after using http, upload file function break. #78
* Add callback function after_wechat_response support. by @zfben #79
* Should using department_id instead of departmentid at enterprise api: user_simplelist/user_list.
## v0.7.0 (released at 1/1/2016)
* Using [http](https://github.com/httprb/http) instead of rest-client for performance reason. (not support upload file yet)
## v0.6.9 (released at 1/6/2016)
* Fix token refresh bug on multi worker. #76
* Rewrite the token relative code to add more storage support in future.
## v0.6.8 (released at 12/25/2015)
* Support Rails 5.0.0.beta1.
* English README available
* Fix oauth2_url calling error, fix #75
## v0.6.7 (released at 12/18/2015)
* Add timeout configuration option, close #74
* New getuserinfo and oauth2_url to support getting FromUserName from web page.
## v0.6.6 (released at 12/15/2015)
* Add jsapi_ticket support for Enterprise Account
* Default generated WechatsController < ActionController::Base, as many Rails application may having #authenticate_user or #set_current_user in ApplicationController, so easily affect the first time using experience.
* New syntax `on :view, with: 'VIEW_URL'` support.
* New command `upload_replaceparty` which combine three sub command to make uploading department easier.
* New command `upload_replaceuser` which combine three sub command to make uploading user easier.
## v0.6.5 (released at 11/24/2015)
* Handle 48001 error if token is expire/not valid, close #71
* ApiLoader will do config reading and initialize the api instead of spreading the logic.
## v0.6.4 (released at 11/16/2015)
* Command mode now display different command set based on enterprise/public account setting
* Move config logic in command/wechat to ApiLoader class
* Unsubscribe can only reply plain text 'success' #68
* Fix 404 qrcode download problem, by @huangxiangdan #69
## v0.6.3 (released at 11/14/2015)
* Official testing and support public encrypt mode, also fix one cipher bug, many thanks to @hlltc #67
* hlltc report public account FILE_BASE no longer needs, clean code #67
* Media command line reflect recent Tecent json schema change. #67
## v0.6.2 (released at 11/05/2015)
* Tecent report location API changed, so change wechat gems also. #64
## v0.6.1 (released at 10/20/2015)
* Handle 40001, invalid credential, access_token is invalid or not latest hint # 57
* Support at Rails 4.2.1 wechat can not run #58
## v0.6.0 (released at 10/08/2015)
### Scan and Batch job are BREAK CHANGE!
* Scan 2D barcode using new syntax `on :scan, with: 'BINDING_QR_CODE' ` instead of `on :event, with: 'BINDING_QR_CODE' ` in previous version #55
Which will fix can not using `on :event, with: "scan" ` problem
* Batch job using new syntax `on :batch_job, with: 'replace_user' `
instead of previous `on :event, with: 'replace_user' `.
* Click menu support new syntax `on :click, with: 'BOOK_LUNCH' `, but `on :event, with: 'BOOK_LUNCH' ` still supported. perfer `on :click` because it running faster and more nature expression.
* Wechat::Responder using Hash for new :client and :batch_job event, avoid time consuming Array match responder
* Fix refresh token not working problem under ruby 2.0.0 #54
* New department_update, user_batchdelete, convert_to_openid API
## v0.5.0 (released at 9/25/2015)
* Only relay on activesupport on run time, so will greatly improve wechat cli startup time
* Add rails generator support `rails g wechat:install`
* Add batch job support for enterprise account like batch create user/department, both API, callback responder and CLI
* Add material management API and CLI
* Add tag API and CLI for enterprise account
* Add QR code scene function for public account
## v0.4.2 (released at 9/7/2015)
* Fix wrong number of arguments at Wechat::Responder.on by using arity #47
* Fix can not access wechat method after using instance level context.
* Fix skip_verify_ssl parameter error.
## v0.4.1 (released at 9/6/2015)
* Limit news articles collection to 10, close #5
* Resolve the conflict with gem "responders" by @seamon #45
## v0.4.0 (released at 9/5/2015)
* Enable the verify SSL for enterprise mode by default, as security is more importent than speed, but still can switch off by configure
* Support scancode_push/scancode_waitmsg event.
* New API method can get wechat server IP list
* New API to query/create department/media/material
* Fix can not read token_file in mingw bug, which introduce at #43
## v0.3.0 (released at 8/30/2015)
* New user group management API
* Allow transfer to customer service on fallback. #42
* Read and write access_token properly using file locking, #43
## v0.2.0 (released at 8/27/2015)
* Add wechat enterprise account support
* Make responder execute in action context, by @lazing #15
* jsapi_ticket support, by @feitian124 #27
* Rename gems to wechat and ambitious to being #1 gems about development wechat. thanks Xiaoning transfer this gem name.
* Original gem `wechat-rails` author skinnyworm trasfer to Eric-Guo as maintainer
## v0.1.1
* Initial release from [wechat-rails](https://github.com/skinnyworm/wechat-rails).

@ -0,0 +1,11 @@
source 'https://rubygems.org'
gemspec
# jquery-rails is used by the dummy application
gem 'jquery-rails'
gem 'rake', '~> 10.4.2'
gem 'codeclimate-test-reporter', group: :test, require: nil
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 skinnyworm
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,27 @@
#!/usr/bin/env rake
begin
require 'bundler/setup'
rescue LoadError
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
end
begin
require 'rdoc/task'
rescue LoadError
require 'rdoc/rdoc'
require 'rake/rdoctask'
RDoc::Task = Rake::RDocTask
end
RDoc::Task.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'Wechat'
rdoc.options << '--line-numbers'
rdoc.rdoc_files.include('README.rdoc')
rdoc.rdoc_files.include('lib/**/*.rb')
end
require File.join('bundler', 'gem_tasks')
require File.join('rspec', 'core', 'rake_task')
RSpec::Core::RakeTask.new(:spec)
task default: :spec

File diff suppressed because it is too large Load Diff

@ -0,0 +1,37 @@
module ActionController
module WechatResponder
def wechat_responder(opts = {})
include Wechat::Responder
self.corpid = opts[:corpid] || Wechat.config.corpid
self.agentid = opts[:agentid] || Wechat.config.agentid
self.encrypt_mode = opts[:encrypt_mode] || Wechat.config.encrypt_mode || corpid.present?
self.timeout = opts[:timeout] || 20
self.skip_verify_ssl = opts[:skip_verify_ssl]
self.token = opts[:token] || Wechat.config.token
self.encoding_aes_key = opts[:encoding_aes_key] || Wechat.config.encoding_aes_key
if opts.empty?
self.wechat = Wechat.api
else
if corpid.present?
self.wechat = Wechat::CorpApi.new(corpid, opts[:corpsecret], opts[:access_token], agentid, timeout, skip_verify_ssl, opts[:jsapi_ticket])
else
self.wechat = Wechat::Api.new(opts[:appid], opts[:secret], opts[:access_token], timeout, skip_verify_ssl, opts[:jsapi_ticket])
end
end
end
end
if defined? Base
class << Base
include WechatResponder
end
end
if defined? API
class << API
include WechatResponder
end
end
end

@ -0,0 +1,32 @@
require 'rails/generators/active_record'
module Wechat
module Generators
class InstallGenerator < Rails::Generators::Base
include ::Rails::Generators::Migration
desc 'Install Wechat support files'
source_root File.expand_path('../templates', __FILE__)
def copy_config
template 'config/wechat.yml'
end
def add_wechat_route
route 'resource :wechat, only: [:show, :create]'
end
def copy_wechat_controller
template 'app/controllers/wechats_controller.rb'
end
def copy_model_migration
migration_template 'db/migration.rb', 'db/migrate/create_wechat_logs.rb'
end
def self.next_migration_number(dirname)
::ActiveRecord::Generators::Base.next_migration_number(dirname)
end
end
end
end

@ -0,0 +1,123 @@
<% if defined? ActionController::API -%>
class WechatsController < ApplicationController
<% else -%>
class WechatsController < ActionController::Base
<% end -%>
wechat_responder
# default text responder when no other match
on :text do |request, content|
request.reply.text "echo: #{content}" # Just echo
end
# When receive 'help', will trigger this responder
on :text, with: 'help' do |request|
request.reply.text 'help content'
end
# When receive '<n>news', will match and will got count as <n> as parameter
on :text, with: /^(\d+) news$/ do |request, count|
# Wechat article can only contain max 10 items, large than 10 will dropped.
news = (1..count.to_i).each_with_object([]) { |n, memo| memo << { title: 'News title', content: "No. #{n} news content" } }
request.reply.news(news) do |article, n, index| # article is return object
article.item title: "#{index} #{n[:title]}", description: n[:content], pic_url: 'http://www.baidu.com/img/bdlogo.gif', url: 'http://www.baidu.com/'
end
end
on :event, with: 'subscribe' do |request|
request.reply.text "#{request[:FromUserName]} subscribe now"
end
# When unsubscribe user scan qrcode qrscene_xxxxxx to subscribe in public account
# notice user will subscribe public account at same time, so wechat won't trigger subscribe event any more
on :scan, with: 'qrscene_xxxxxx' do |request, ticket|
request.reply.text "Unsubscribe user #{request[:FromUserName]} Ticket #{ticket}"
end
# When subscribe user scan scene_id in public account
on :scan, with: 'scene_id' do |request, ticket|
request.reply.text "Subscribe user #{request[:FromUserName]} Ticket #{ticket}"
end
# When no any on :scan responder can match subscribe user scaned scene_id
on :event, with: 'scan' do |request|
if request[:EventKey].present?
request.reply.text "event scan got EventKey #{request[:EventKey]} Ticket #{request[:Ticket]}"
end
end
# When enterprise user press menu BINDING_QR_CODE and success to scan bar code
on :scan, with: 'BINDING_QR_CODE' do |request, scan_result, scan_type|
request.reply.text "User #{request[:FromUserName]} ScanResult #{scan_result} ScanType #{scan_type}"
end
# Except QR code, wechat can also scan CODE_39 bar code in enterprise account
on :scan, with: 'BINDING_BARCODE' do |message, scan_result|
if scan_result.start_with? 'CODE_39,'
message.reply.text "User: #{message[:FromUserName]} scan barcode, result is #{scan_result.split(',')[1]}"
end
end
# When user click the menu button
on :click, with: 'BOOK_LUNCH' do |request, key|
request.reply.text "User: #{request[:FromUserName]} click #{key}"
end
# When user view URL in the menu button
on :view, with: 'http://wechat.somewhere.com/view_url' do |request, view|
request.reply.text "#{request[:FromUserName]} view #{view}"
end
# When user sent the imsage
on :image do |request|
request.reply.image(request[:MediaId]) # Echo the sent image to user
end
# When user sent the voice
on :voice do |request|
request.reply.voice(request[:MediaId]) # Echo the sent voice to user
end
# When user sent the video
on :video do |request|
nickname = wechat.user(request[:FromUserName])['nickname'] # Call wechat api to get sender nickname
request.reply.video(request[:MediaId], title: 'Echo', description: "Got #{nickname} sent video") # Echo the sent video to user
end
# When user sent location
on :location do |request|
request.reply.text("Latitude: #{message[:Latitude]} Longitude: #{message[:Longitude]} Precision: #{message[:Precision]}")
end
on :event, with: 'unsubscribe' do |request|
request.reply.success # user can not receive this message
end
# When user enter the app / agent app
on :event, with: 'enter_agent' do |request|
request.reply.text "#{request[:FromUserName]} enter agent app now"
end
# When batch job create/update user (incremental) finished.
on :batch_job, with: 'sync_user' do |request, batch_job|
request.reply.text "sync_user job #{batch_job[:JobId]} finished, return code #{batch_job[:ErrCode]}, return message #{batch_job[:ErrMsg]}"
end
# When batch job replace user (full sync) finished.
on :batch_job, with: 'replace_user' do |request, batch_job|
request.reply.text "replace_user job #{batch_job[:JobId]} finished, return code #{batch_job[:ErrCode]}, return message #{batch_job[:ErrMsg]}"
end
# When batch job invent user finished.
on :batch_job, with: 'invite_user' do |request, batch_job|
request.reply.text "invite_user job #{batch_job[:JobId]} finished, return code #{batch_job[:ErrCode]}, return message #{batch_job[:ErrMsg]}"
end
# When batch job replace department (full sync) finished.
on :batch_job, with: 'replace_party' do |request, batch_job|
request.reply.text "replace_party job #{batch_job[:JobId]} finished, return code #{batch_job[:ErrCode]}, return message #{batch_job[:ErrMsg]}"
end
# Any not match above will fail to below
on :fallback, respond: 'fallback message'
end

@ -0,0 +1,33 @@
default: &default
corpid: "corpid"
corpsecret: "corpsecret"
agentid: 1
# Or if using public account, only need above two line
# appid: "my_appid"
# secret: "my_secret"
token: "my_token"
access_token: "C:/Users/[username]/wechat_access_token"
encrypt_mode: false # if true must fill encoding_aes_key
encoding_aes_key: "my_encoding_aes_key"
jsapi_ticket: "C:/Users/[user_name]/wechat_jsapi_ticket"
production:
corpid: <%%= ENV['WECHAT_CORPID'] %>
corpsecret: <%%= ENV['WECHAT_CORPSECRET'] %>
agentid: <%%= ENV['WECHAT_AGENTID'] %>
# Or if using public account, only need above two line
# appid: <%= ENV['WECHAT_APPID'] %>
# secret: <%= ENV['WECHAT_APP_SECRET'] %>
token: <%%= ENV['WECHAT_TOKEN'] %>
timeout: 30,
skip_verify_ssl: true
access_token: <%%= ENV['WECHAT_ACCESS_TOKEN'] %>
encrypt_mode: false # if true must fill encoding_aes_key
encoding_aes_key: <%%= ENV['WECHAT_ENCODING_AES_KEY'] %>
jsapi_ticket: <%= ENV['WECHAT_JSAPI_TICKET'] %>
development:
<<: *default
test:
<<: *default

@ -0,0 +1,11 @@
class CreateWechatLogs < ActiveRecord::Migration
def change
create_table :wechat_logs do |t|
t.string :openid, null: false, index: true
t.text :request_raw
t.text :response_raw
t.text :session_raw
t.datetime :created_at, null: false
end
end
end

@ -0,0 +1,28 @@
require 'wechat/api_loader'
require 'wechat/api'
require 'wechat/corp_api'
require 'action_controller/wechat_responder'
module Wechat
autoload :Message, 'wechat/message'
autoload :Responder, 'wechat/responder'
autoload :Cipher, 'wechat/cipher'
autoload :WechatLog, 'wechat/wechat_log'
class AccessTokenExpiredError < StandardError; end
class ResponseError < StandardError
attr_reader :error_code
def initialize(errcode, errmsg)
@error_code = errcode
super "#{errmsg}(#{error_code})"
end
end
def self.config
ApiLoader.config
end
def self.api
@wechat_api ||= ApiLoader.with({})
end
end

@ -0,0 +1,124 @@
require 'wechat/api_base'
require 'wechat/client'
require 'wechat/token/public_access_token'
require 'wechat/ticket/public_jsapi_ticket'
module Wechat
class Api < ApiBase
API_BASE = 'https://api.weixin.qq.com/cgi-bin/'
OAUTH2_BASE = 'https://api.weixin.qq.com/sns/oauth2/'
def initialize(appid, secret, token_file, timeout, skip_verify_ssl, jsapi_ticket_file)
@client = Client.new(API_BASE, timeout, skip_verify_ssl)
@access_token = Token::PublicAccessToken.new(@client, appid, secret, token_file)
@jsapi_ticket = Ticket::PublicJsapiTicket.new(@client, @access_token, jsapi_ticket_file)
end
def groups
get 'groups/get'
end
def group_create(group_name)
post 'groups/create', JSON.generate(group: { name: group_name })
end
def group_update(groupid, new_group_name)
post 'groups/update', JSON.generate(group: { id: groupid, name: new_group_name })
end
def group_delete(groupid)
post 'groups/delete', JSON.generate(group: { id: groupid })
end
def users(nextid = nil)
params = { params: { next_openid: nextid } } if nextid.present?
get('user/get', params || {})
end
def user(openid)
get 'user/info', params: { openid: openid }
end
def user_group(openid)
post 'groups/getid', JSON.generate(openid: openid)
end
def user_change_group(openid, to_groupid)
post 'groups/members/update', JSON.generate(openid: openid, to_groupid: to_groupid)
end
def user_update_remark(openid, remark)
post 'user/info/updateremark', JSON.generate(openid: openid, remark: remark)
end
def qrcode_create_scene(scene_id, expire_seconds = 604800)
post 'qrcode/create', JSON.generate(expire_seconds: expire_seconds,
action_name: 'QR_SCENE',
action_info: { scene: { scene_id: scene_id } })
end
def qrcode_create_limit_scene(scene_id_or_str)
case scene_id_or_str
when Fixnum
post 'qrcode/create', JSON.generate(action_name: 'QR_LIMIT_SCENE',
action_info: { scene: { scene_id: scene_id_or_str } })
else
post 'qrcode/create', JSON.generate(action_name: 'QR_LIMIT_STR_SCENE',
action_info: { scene: { scene_str: scene_id_or_str } })
end
end
def menu
get 'menu/get'
end
def menu_delete
get 'menu/delete'
end
def menu_create(menu)
# 微信不接受7bit escaped json(eg \uxxxx), 中文必须UTF-8编码, 这可能是个安全漏洞
post 'menu/create', JSON.generate(menu)
end
def material(media_id)
get 'material/get', params: { media_id: media_id }, as: :file
end
def material_count
get 'material/get_materialcount'
end
def material_list(type, offset, count)
post 'material/batchget_material', JSON.generate(type: type, offset: offset, count: count)
end
def material_add(type, file)
post_file 'material/add_material', file, params: { type: type }
end
def material_delete(media_id)
post 'material/del_material', media_id: media_id
end
def custom_message_send(message)
post 'message/custom/send', message.to_json, content_type: :json
end
def template_message_send(message)
post 'message/template/send', message.to_json, content_type: :json
end
# http://mp.weixin.qq.com/wiki/17/c0f37d5704f0b64713d5d2c37b468d75.html
# 第二步通过code换取网页授权access_token
def web_access_token(code)
params = {
appid: access_token.appid,
secret: access_token.secret,
code: code,
grant_type: 'authorization_code'
}
get 'access_token', params: params, base: OAUTH2_BASE
end
end
end

@ -0,0 +1,51 @@
module Wechat
class ApiBase
attr_reader :access_token, :client, :jsapi_ticket
MP_BASE = 'https://mp.weixin.qq.com/cgi-bin/'
def callbackip
get 'getcallbackip'
end
def qrcode(ticket)
client.get 'showqrcode', params: { ticket: ticket }, base: MP_BASE, as: :file
end
def media(media_id)
get 'media/get', params: { media_id: media_id }, as: :file
end
def media_create(type, file)
post_file 'media/upload', file, params: { type: type }
end
protected
def get(path, headers = {})
with_access_token(headers[:params]) do |params|
client.get path, headers.merge(params: params)
end
end
def post(path, payload, headers = {})
with_access_token(headers[:params]) do |params|
client.post path, payload, headers.merge(params: params)
end
end
def post_file(path, file, headers = {})
with_access_token(headers[:params]) do |params|
client.post_file path, file, headers.merge(params: params)
end
end
def with_access_token(params = {}, tries = 2)
params ||= {}
yield(params.merge(access_token: access_token.token))
rescue AccessTokenExpiredError
access_token.refresh
retry unless (tries -= 1).zero?
end
end
end

@ -0,0 +1,79 @@
module Wechat
module ApiLoader
def self.with(options)
c = ApiLoader.config
token_file = options[:token_file] || c.access_token || '/var/tmp/wechat_access_token'
js_token_file = options[:js_token_file] || c.jsapi_ticket || '/var/tmp/wechat_jsapi_ticket'
if c.appid && c.secret && token_file.present?
Wechat::Api.new(c.appid, c.secret, token_file, c.timeout, c.skip_verify_ssl, js_token_file)
elsif c.corpid && c.corpsecret && token_file.present?
Wechat::CorpApi.new(c.corpid, c.corpsecret, token_file, c.agentid, c.timeout, c.skip_verify_ssl, js_token_file)
else
puts <<-HELP
Need create ~/.wechat.yml with wechat appid and secret
or running at rails root folder so wechat can read config/wechat.yml
HELP
exit 1
end
end
@config = nil
def self.config
return @config unless @config.nil?
@config ||= loading_config!
end
private
def self.loading_config!
config ||= config_from_file || config_from_environment
if defined?(::Rails)
config[:access_token] ||= Rails.root.join('tmp/access_token').to_s
config[:jsapi_ticket] ||= Rails.root.join('tmp/jsapi_ticket').to_s
end
config[:timeout] ||= 20
config.symbolize_keys!
@config = OpenStruct.new(config)
end
def self.config_from_file
if defined?(::Rails)
config_file = Rails.root.join('config/wechat.yml')
return YAML.load(ERB.new(File.read(config_file)).result)[Rails.env] if File.exist?(config_file)
else
rails_config_file = File.join(Dir.getwd, 'config/wechat.yml')
home_config_file = File.join(Dir.home, '.wechat.yml')
if File.exist?(rails_config_file)
rails_env = ENV['RAILS_ENV'] || 'default'
config = YAML.load(ERB.new(File.read(rails_config_file)).result)[rails_env]
if config.present? && (config['appid'] || config['corpid'])
puts "Using rails project config/wechat.yml #{rails_env} setting..."
return config
end
end
if File.exist?(home_config_file)
return YAML.load ERB.new(File.read(home_config_file)).result
end
end
end
def self.config_from_environment
{ appid: ENV['WECHAT_APPID'],
secret: ENV['WECHAT_SECRET'],
corpid: ENV['WECHAT_CORPID'],
corpsecret: ENV['WECHAT_CORPSECRET'],
agentid: ENV['WECHAT_AGENTID'],
token: ENV['WECHAT_TOKEN'],
access_token: ENV['WECHAT_ACCESS_TOKEN'],
encrypt_mode: ENV['WECHAT_ENCRYPT_MODE'],
timeout: ENV['WECHAT_TIMEOUT'],
skip_verify_ssl: ENV['WECHAT_SKIP_VERIFY_SSL'],
encoding_aes_key: ENV['WECHAT_ENCODING_AES_KEY'],
jsapi_ticket: ENV['WECHAT_JSAPI_TICKET'] }
end
end
end

@ -0,0 +1,72 @@
require 'openssl/cipher'
require 'securerandom'
require 'base64'
module Wechat
module Cipher
extend ActiveSupport::Concern
BLOCK_SIZE = 32
CIPHER = 'AES-256-CBC'
def encrypt(plain, encoding_aes_key)
cipher = OpenSSL::Cipher.new(CIPHER)
cipher.encrypt
cipher.padding = 0
key_data = Base64.decode64(encoding_aes_key + '=')
cipher.key = key_data
cipher.iv = key_data[0..16]
cipher.update(plain) + cipher.final
end
def decrypt(msg, encoding_aes_key)
cipher = OpenSSL::Cipher.new(CIPHER)
cipher.decrypt
cipher.padding = 0
key_data = Base64.decode64(encoding_aes_key + '=')
cipher.key = key_data
cipher.iv = key_data[0..16]
plain = cipher.update(msg) + cipher.final
decode_padding(plain)
end
# app_id or corp_id
def pack(content, app_id)
random = SecureRandom.hex(8)
text = content.force_encoding('ASCII-8BIT')
msg_len = [text.length].pack('N')
encode_padding("#{random}#{msg_len}#{text}#{app_id}")
end
def unpack(msg)
msg = decode_padding(msg)
msg_len = msg[16, 4].reverse.unpack('V')[0]
content = msg[20, msg_len]
app_id = msg[(20 + msg_len)..-1]
[content, app_id]
end
private
def encode_padding(data)
length = data.bytes.length
amount_to_pad = BLOCK_SIZE - (length % BLOCK_SIZE)
amount_to_pad = BLOCK_SIZE if amount_to_pad == 0
padding = ([amount_to_pad].pack('c') * amount_to_pad)
data + padding
end
def decode_padding(plain)
pad = plain.bytes[-1]
# no padding
pad = 0 if pad < 1 || pad > BLOCK_SIZE
plain[0...(plain.length - pad)]
end
end
end

@ -0,0 +1,90 @@
require 'http'
module Wechat
class Client
attr_reader :base, :ssl_context
def initialize(base, timeout, skip_verify_ssl)
@base = base
HTTP.timeout(:global, write: timeout, connect: timeout, read: timeout)
@ssl_context = OpenSSL::SSL::SSLContext.new
@ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE if skip_verify_ssl
end
def get(path, get_header = {})
request(path, get_header) do |url, header|
params = header.delete(:params)
HTTP.headers(header).get(url, params: params, ssl_context: ssl_context)
end
end
def post(path, payload, post_header = {})
request(path, post_header) do |url, header|
params = header.delete(:params)
HTTP.headers(header).post(url, params: params, body: payload, ssl_context: ssl_context)
end
end
def post_file(path, file, post_header = {})
request(path, post_header) do |url, header|
params = header.delete(:params)
HTTP.headers(header)
.post(url, params: params,
form: { media: HTTP::FormData::File.new(file),
hack: 'X' }, # Existing here for http-form_data 1.0.1 handle single param improperly
ssl_context: ssl_context)
end
end
private
def request(path, header = {}, &_block)
url = "#{header.delete(:base) || base}#{path}"
as = header.delete(:as)
header.merge!('Accept' => 'application/json')
response = yield(url, header)
fail "Request not OK, response status #{response.status}" if response.status != 200
parse_response(response, as || :json) do |parse_as, data|
break data unless parse_as == :json && data['errcode'].present?
case data['errcode']
when 0 # for request didn't expect results
data
# 42001: access_token timeout
# 40014: invalid access_token
# 40001, invalid credential, access_token is invalid or not latest hint
# 48001, api unauthorized hint, for qrcode creation # 71
when 42001, 40014, 40001, 48001
fail AccessTokenExpiredError
else
fail ResponseError.new(data['errcode'], data['errmsg'])
end
end
end
def parse_response(response, as)
content_type = response.headers[:content_type]
parse_as = {
%r{^application\/json} => :json,
%r{^image\/.*} => :file
}.each_with_object([]) { |match, memo| memo << match[1] if content_type =~ match[0] }.first || as || :text
case parse_as
when :file
file = Tempfile.new('tmp')
file.binmode
file.write(response.body)
file.close
data = file
when :json
data = JSON.parse response.body.to_s.gsub(/[\u0000-\u001f]+/, '')
else
data = response.body
end
yield(parse_as, data)
end
end
end

@ -0,0 +1,166 @@
require 'wechat/api_base'
require 'wechat/client'
require 'wechat/token/corp_access_token'
require 'wechat/ticket/corp_jsapi_ticket'
require 'cgi'
module Wechat
class CorpApi < ApiBase
attr_reader :agentid
API_BASE = 'https://qyapi.weixin.qq.com/cgi-bin/'
def initialize(appid, secret, token_file, agentid, timeout, skip_verify_ssl, jsapi_ticket_file)
@client = Client.new(API_BASE, timeout, skip_verify_ssl)
@access_token = Token::CorpAccessToken.new(@client, appid, secret, token_file)
@agentid = agentid
@jsapi_ticket = Ticket::CorpJsapiTicket.new(@client, @access_token, jsapi_ticket_file)
end
def agent_list
get 'agent/list'
end
def agent(agentid)
get 'agent/get', params: { agentid: agentid }
end
def user(userid)
get 'user/get', params: { userid: userid }
end
def getuserinfo(code)
get 'user/getuserinfo', params: { code: code }
end
def oauth2_url(redirect_uri, appid)
redirect_uri = CGI.escape(redirect_uri)
"https://open.weixin.qq.com/connect/oauth2/authorize?appid=#{appid}&redirect_uri=#{redirect_uri}&response_type=code&scope=snsapi_base#wechat_redirect"
end
def convert_to_openid(userid)
post 'user/convert_to_openid', JSON.generate(userid: userid, agentid: agentid)
end
def invite_user(userid)
post 'invite/send', JSON.generate(userid: userid)
end
def user_auth_success(userid)
get 'user/authsucc', params: { userid: userid }
end
def user_delete(userid)
get 'user/delete', params: { userid: userid }
end
def user_batchdelete(useridlist)
post 'user/batchdelete', JSON.generate(useridlist: useridlist)
end
def batch_job_result(jobid)
get 'batch/getresult', params: { jobid: jobid }
end
def batch_replaceparty(media_id)
post 'batch/replaceparty', JSON.generate(media_id: media_id)
end
def batch_syncuser(media_id)
post 'batch/syncuser', JSON.generate(media_id: media_id)
end
def batch_replaceuser(media_id)
post 'batch/replaceuser', JSON.generate(media_id: media_id)
end
def department_create(name, parentid)
post 'department/create', JSON.generate(name: name, parentid: parentid)
end
def department_delete(departmentid)
get 'department/delete', params: { id: departmentid }
end
def department_update(departmentid, name = nil, parentid = nil, order = nil)
post 'department/update', JSON.generate({ id: departmentid, name: name, parentid: parentid, order: order }.reject { |_k, v| v.nil? })
end
def department(departmentid = 1)
get 'department/list', params: { id: departmentid }
end
def user_simplelist(department_id, fetch_child = 0, status = 0)
get 'user/simplelist', params: { department_id: department_id, fetch_child: fetch_child, status: status }
end
def user_list(department_id, fetch_child = 0, status = 0)
get 'user/list', params: { department_id: department_id, fetch_child: fetch_child, status: status }
end
def tag_create(tagname, tagid = nil)
post 'tag/create', JSON.generate(tagname: tagname, tagid: tagid)
end
def tag_update(tagid, tagname)
post 'tag/update', JSON.generate(tagid: tagid, tagname: tagname)
end
def tag_delete(tagid)
get 'tag/delete', params: { tagid: tagid }
end
def tags
get 'tag/list'
end
def tag(tagid)
get 'tag/get', params: { tagid: tagid }
end
def tag_add_user(tagid, userids = nil, departmentids = nil)
post 'tag/addtagusers', JSON.generate(tagid: tagid, userlist: userids, partylist: departmentids)
end
def tag_del_user(tagid, userids = nil, departmentids = nil)
post 'tag/deltagusers', JSON.generate(tagid: tagid, userlist: userids, partylist: departmentids)
end
def menu
get 'menu/get', params: { agentid: agentid }
end
def menu_delete
get 'menu/delete', params: { agentid: agentid }
end
def menu_create(menu)
# 微信不接受7bit escaped json(eg \uxxxx), 中文必须UTF-8编码, 这可能是个安全漏洞
post 'menu/create', JSON.generate(menu), params: { agentid: agentid }
end
def material_count
get 'material/get_count', params: { agentid: agentid }
end
def material_list(type, offset, count)
post 'material/batchget', JSON.generate(type: type, agentid: agentid, offset: offset, count: count)
end
def material(media_id)
get 'material/get', params: { media_id: media_id, agentid: agentid }, as: :file
end
def material_add(type, file)
post_file 'material/add_material', file, params: { type: type, agentid: agentid }
end
def material_delete(media_id)
get 'material/del', params: { media_id: media_id, agentid: agentid }
end
def message_send(openid, message)
post 'message/send', Message.to(openid).text(message).agent_id(agentid).to_json, content_type: :json
end
end
end

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save