# frozen_string_literal: true # Wechat service, View more at: https://mp.weixin.qq.com/wiki # example: # Wechat.notify(event: 'crash', first: 'Your website is not accessible', time: Time.now, reason: 'Connect fail!', remark: 'remark', redirect_url: 'www.baidu.com') module Ocean module Wechat extend self BASE_URL = 'https://api.weixin.qq.com/cgi-bin' ACCESS_TOKEN_FILE = 'tmp/access_token.txt' NOTIFY_RECIPIENT_FILE = 'config/wechat_notify.yml' SUCCESS_CODE = 0 ACCESS_TOKEN_EXPIRED_CODE = 420_01 ACCESS_TOKEN_INVALID_CODE = 400_01 NOTIFY_DEFAULT_COLOR = '#173177' MAX_RETRY_NUM = 5 def access_token read_access_token || refresh_access_token end # Use wechat template to send message def notify(**args) notify_block = lambda do |open_id| url = "#{BASE_URL}/message/template/send" params = { touser: open_id, template_id: wechat_notify(args[:event], 'template'), url: args[:redirect_url], data: {} } args.each { |k, v| params[:data][k] = { value: v, color: NOTIFY_DEFAULT_COLOR } } request(url, params, :post) end [].tap do |item| wechat_notify(args[:event], 'users').split(',').each do |open_id| item << { open_id: open_id, success: notify_block.call(open_id)[:errcode] == SUCCESS_CODE } end end end # Get all notify template def notify_templates request("#{BASE_URL}/template/get_all_private_template") end # Get all followers def users request("#{BASE_URL}/user/get") end def create_user_tag(name) params = { tag: { name: name } } request("#{BASE_URL}/tags/create", params, :post) end # Get all user tag def user_tags request("#{BASE_URL}/tags/get") end def find_users_by_tag(name) res = request( "#{BASE_URL}/user/tag/get", { tagid: find_tag_id_by_name(name) }, :post ) res['count'] == 0 ? [] : res['data']['openid'] end def find_tag_id_by_name(name) user_tags[:tags].detect { |item| item['name'] == name }['id'] end def add_tag_for_users(users, tag_id) params = { open_list: users, tagid: tag_id } request("#{BASE_URL}/tags/members/batchtagging", params, :post) end private def request(source_url, params = {}, method = :get, retry_num = 0) url = compose_url(source_url) log.debug "Send #{method} request #{params} to #{url}" res = if method == :get RestClient.get(url, params) else RestClient.post(url, params.to_json, content_type: :json, accept: :json) end res_body = read_content(res) json = JSON.parse force_encoding(res_body) json.symbolize_keys! if json[:errcode] == ACCESS_TOKEN_EXPIRED_CODE || json[:errcode] == ACCESS_TOKEN_INVALID_CODE # Try again five times at most return if retry_num == MAX_RETRY_NUM log.info("Access token is expired or invalid, retry #{retry_num} ...") refresh_access_token # Control call frequency sleep(retry_num * 10) retry_num += 1 return request(source_url, params, method, retry_num) elsif json[:errcode] && json[:errcode] != SUCCESS_CODE raise "Request #{url}, errmsg: #{json[:errmsg]}, errcode: #{json[:errcode]}" else json end rescue StandardError => ex log.error("Call wechat api error: #{ex}\n #{ex.backtrace.join("\n")}") json end # Remove not UTF-8 encoding def force_encoding(data) data.force_encoding('UTF-8').gsub(/[\u0000-\u001f\u007f\u0080-\u009f]+/, '*') end def read_content(res) if res['content-encoding'].present? && res['content-encoding'] == 'gzip' Zlib::GzipReader.new(StringIO.new(res.body), encoding: 'UTF-8').read else res.body end end def compose_url(source_url) source_url.include?('?') ? source_url : "#{source_url}?access_token=#{access_token}" end def refresh_access_token params = { grant_type: 'client_credential', appid: ENV.fetch('WECHAT_APP_ID'), secret: ENV.fetch('WECHAT_APP_SECRET') } res = request("#{BASE_URL}/token?#{params.to_query}", {}, 'GET') save_access_token(res[:access_token]) if res[:access_token] res[:access_token] end def save_access_token(token) file = File.new(ACCESS_TOKEN_FILE, 'w') file.write(token) rescue IOError => ex log.error("Write access_token fail: #{ex}") ensure file.close end def read_access_token File.exist?(ACCESS_TOKEN_FILE) ? File.read(ACCESS_TOKEN_FILE) : nil end def wechat_notify(event, key) File.exist?(NOTIFY_RECIPIENT_FILE) ? YAML.load_file(NOTIFY_RECIPIENT_FILE)[event.to_s][key] : [] end def log Rails.logger end end end