wechat-rails 0.1.0
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/LICENSE +21 -0
- data/README.md +5 -0
- data/Rakefile +31 -0
- data/bin/wechat +134 -0
- data/lib/wechat-rails.rb +39 -0
- data/lib/wechat/access_token.rb +35 -0
- data/lib/wechat/api.rb +68 -0
- data/lib/wechat/client.rb +74 -0
- data/lib/wechat/message.rb +163 -0
- data/lib/wechat/responder.rb +98 -0
- metadata +109 -0
    
        checksums.yaml
    ADDED
    
    | @@ -0,0 +1,7 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            SHA1:
         | 
| 3 | 
            +
              metadata.gz: 49c382d50e750b4bb75413972a7f8ddc74a372a1
         | 
| 4 | 
            +
              data.tar.gz: ce7ac829af66603e79aee9ae127eec3e077f8e0f
         | 
| 5 | 
            +
            SHA512:
         | 
| 6 | 
            +
              metadata.gz: 4a67f307848a3bb0fa57737fd50c136a987dbc9234f84b8a389163c1662e6fdb49b179b7e2bab4ef57f92e94e068404e7b166cb9a58a433cacc7b36990f4989a
         | 
| 7 | 
            +
              data.tar.gz: b32b63a48c6cd730c0ebc590945dc38ef886b502459eb3aabf723ca844343740de9b8df80e5907d21533964a4db49436cd35b42e7a88bc556839b812c16512ba
         | 
    
        data/LICENSE
    ADDED
    
    | @@ -0,0 +1,21 @@ | |
| 1 | 
            +
            The MIT License (MIT)
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            Copyright (c) 2014 skinnyworm
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Permission is hereby granted, free of charge, to any person obtaining a copy
         | 
| 6 | 
            +
            of this software and associated documentation files (the "Software"), to deal
         | 
| 7 | 
            +
            in the Software without restriction, including without limitation the rights
         | 
| 8 | 
            +
            to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
         | 
| 9 | 
            +
            copies of the Software, and to permit persons to whom the Software is
         | 
| 10 | 
            +
            furnished to do so, subject to the following conditions:
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            The above copyright notice and this permission notice shall be included in all
         | 
| 13 | 
            +
            copies or substantial portions of the Software.
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
         | 
| 16 | 
            +
            IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
         | 
| 17 | 
            +
            FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
         | 
| 18 | 
            +
            AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
         | 
| 19 | 
            +
            LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
         | 
| 20 | 
            +
            OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
         | 
| 21 | 
            +
            SOFTWARE.
         | 
    
        data/README.md
    ADDED
    
    | @@ -0,0 +1,5 @@ | |
| 1 | 
            +
            Wechat Rails
         | 
| 2 | 
            +
            ======================
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            [](https://travis-ci.org/skinnyworm/wechat-rails) [](https://codeclimate.com/github/skinnyworm/wechat-rails) [](https://codeclimate.com/github/skinnyworm/wechat-rails)
         | 
| 5 | 
            +
             | 
    
        data/Rakefile
    ADDED
    
    | @@ -0,0 +1,31 @@ | |
| 1 | 
            +
            #!/usr/bin/env rake
         | 
| 2 | 
            +
            begin
         | 
| 3 | 
            +
              require 'bundler/setup'
         | 
| 4 | 
            +
            rescue LoadError
         | 
| 5 | 
            +
              puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
         | 
| 6 | 
            +
            end
         | 
| 7 | 
            +
            begin
         | 
| 8 | 
            +
              require 'rdoc/task'
         | 
| 9 | 
            +
            rescue LoadError
         | 
| 10 | 
            +
              require 'rdoc/rdoc'
         | 
| 11 | 
            +
              require 'rake/rdoctask'
         | 
| 12 | 
            +
              RDoc::Task = Rake::RDocTask
         | 
| 13 | 
            +
            end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            RDoc::Task.new(:rdoc) do |rdoc|
         | 
| 16 | 
            +
              rdoc.rdoc_dir = 'rdoc'
         | 
| 17 | 
            +
              rdoc.title    = 'WechatRails'
         | 
| 18 | 
            +
              rdoc.options << '--line-numbers'
         | 
| 19 | 
            +
              rdoc.rdoc_files.include('README.rdoc')
         | 
| 20 | 
            +
              rdoc.rdoc_files.include('lib/**/*.rb')
         | 
| 21 | 
            +
            end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
             | 
| 24 | 
            +
            require File.join('bundler', 'gem_tasks')
         | 
| 25 | 
            +
            require File.join('rspec', 'core', 'rake_task')
         | 
| 26 | 
            +
            RSpec::Core::RakeTask.new(:spec)
         | 
| 27 | 
            +
             | 
| 28 | 
            +
             | 
| 29 | 
            +
            Bundler::GemHelper.install_tasks
         | 
| 30 | 
            +
             | 
| 31 | 
            +
            task :default => :spec
         | 
    
        data/bin/wechat
    ADDED
    
    | @@ -0,0 +1,134 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            lib = File.expand_path(File.dirname(__FILE__) + '/../lib')
         | 
| 4 | 
            +
            $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            require 'thor'
         | 
| 7 | 
            +
            require "wechat-rails"
         | 
| 8 | 
            +
            require 'json'  
         | 
| 9 | 
            +
            require "active_support/core_ext"
         | 
| 10 | 
            +
            require 'fileutils'
         | 
| 11 | 
            +
             | 
| 12 | 
            +
             | 
| 13 | 
            +
             | 
| 14 | 
            +
            class App < Thor                                                 
         | 
| 15 | 
            +
              class Helper
         | 
| 16 | 
            +
                def self.with(options)
         | 
| 17 | 
            +
                  appid =  ENV["WECHAT_APPID"]
         | 
| 18 | 
            +
                  secret = ENV["WECHAT_SECRET"]
         | 
| 19 | 
            +
                  token_file = options[:toke_file] ||  ENV["WECHAT_ACCESS_TOKEN"]
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  if (appid.nil? || secret.nil? || token_file.nil?)
         | 
| 22 | 
            +
                  puts <<-HELP
         | 
| 23 | 
            +
            You need set wechat appid and secret in environment variables.
         | 
| 24 | 
            +
             | 
| 25 | 
            +
              export WECHAT_APPID=<appid>
         | 
| 26 | 
            +
              export WECHAT_SECRET=<secret>
         | 
| 27 | 
            +
              export WECHAT_ACCESS_TOKEN=<file location for storing access token>
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            HELP
         | 
| 30 | 
            +
                  exit 1
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
                  Wechat::Api.new(appid, secret, token_file)
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
              package_name "Wechat"
         | 
| 37 | 
            +
              option :toke_file, :aliases=>"-t", :desc => "File to store access token"
         | 
| 38 | 
            +
             | 
| 39 | 
            +
              desc "users", "关注者列表"
         | 
| 40 | 
            +
              def users
         | 
| 41 | 
            +
                puts Helper.with(options).users
         | 
| 42 | 
            +
              end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
              desc "user [OPEN_ID]", "查找关注者"
         | 
| 45 | 
            +
              def user(open_id)
         | 
| 46 | 
            +
                puts Helper.with(options).user(open_id)
         | 
| 47 | 
            +
              end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
              desc "menu", "当前菜单"
         | 
| 50 | 
            +
              def menu
         | 
| 51 | 
            +
                puts Helper.with(options).menu
         | 
| 52 | 
            +
              end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
              desc "menu_delete", "删除菜单"
         | 
| 55 | 
            +
              def menu_delete
         | 
| 56 | 
            +
                puts "Menu deleted" if Helper.with(options).menu_delete
         | 
| 57 | 
            +
              end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
              desc "menu_create [MENU_YAML]", "删除菜单"
         | 
| 60 | 
            +
              def menu_create(menu_yaml)
         | 
| 61 | 
            +
                menu = YAML.load(File.new(menu_yaml).read)
         | 
| 62 | 
            +
                puts "Menu created" if Helper.with(options).menu_create(menu)
         | 
| 63 | 
            +
              end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
              desc "media [MEDIA_ID, PATH]", "媒体下载"
         | 
| 66 | 
            +
              def media(media_id, path)
         | 
| 67 | 
            +
                tmp_file = Helper.with(options).media(media_id)
         | 
| 68 | 
            +
                FileUtils.mv(tmp_file.path, path)
         | 
| 69 | 
            +
                puts "File downloaded"
         | 
| 70 | 
            +
              end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
              desc "media_create [MEDIA_ID, PATH]", "媒体上传"
         | 
| 73 | 
            +
              def media_create(type, path)
         | 
| 74 | 
            +
                file = File.new(path)
         | 
| 75 | 
            +
                puts Helper.with(options).media_create(type, file)
         | 
| 76 | 
            +
              end
         | 
| 77 | 
            +
              
         | 
| 78 | 
            +
              desc "custom_text [OPENID, TEXT_MESSAGE]", "发送文字客服消息"
         | 
| 79 | 
            +
              def custom_text openid, text_message
         | 
| 80 | 
            +
                puts Helper.with(options).custom_message_send Wechat::Message.to(openid).text(text_message)
         | 
| 81 | 
            +
              end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
              desc "custom_image [OPENID, IMAGE_PATH]", "发送图片客服消息"
         | 
| 84 | 
            +
              def custom_image openid, image_path
         | 
| 85 | 
            +
                file = File.new(image_path)
         | 
| 86 | 
            +
                api = Helper.with(options)
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                media_id = api.media_create("image", file)["media_id"]
         | 
| 89 | 
            +
                puts api.custom_message_send Wechat::Message.to(openid).image(media_id)
         | 
| 90 | 
            +
              end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
              desc "custom_voice [OPENID, VOICE_PATH]", "发送语音客服消息"
         | 
| 93 | 
            +
              def custom_voice openid, voice_path
         | 
| 94 | 
            +
                file = File.new(voice_path)
         | 
| 95 | 
            +
                api = Helper.with(options)
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                media_id = api.media_create("voice", file)["media_id"]
         | 
| 98 | 
            +
                puts api.custom_message_send Wechat::Message.to(openid).voice(media_id)
         | 
| 99 | 
            +
              end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
              desc "custom_video [OPENID, VIDEO_PATH]", "发送视频客服消息"
         | 
| 102 | 
            +
              method_option :title, :aliases => "-h", :desc => "视频标题"
         | 
| 103 | 
            +
              method_option :description, :aliases => "-d", :desc => "视频描述"
         | 
| 104 | 
            +
              def custom_video openid, video_path
         | 
| 105 | 
            +
                file = File.new(video_path)
         | 
| 106 | 
            +
                api = Helper.with(options)
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                api_opts = options.slice(:title, :description)
         | 
| 109 | 
            +
                media_id = api.media_create("video", file)["media_id"]
         | 
| 110 | 
            +
                puts api.custom_message_send Wechat::Message.to(openid).video(media_id, api_opts)
         | 
| 111 | 
            +
              end
         | 
| 112 | 
            +
             | 
| 113 | 
            +
              desc "custom_music [OPENID, THUMBNAIL_PATH, MUSIC_URL]", "发送音乐客服消息"
         | 
| 114 | 
            +
              method_option :title, :aliases => "-h", :desc => "音乐标题"
         | 
| 115 | 
            +
              method_option :description, :aliases => "-d", :desc => "音乐描述"
         | 
| 116 | 
            +
              method_option :HQ_music_url, :aliases => "-u", :desc => "高质量音乐URL链接"
         | 
| 117 | 
            +
              def custom_music openid, thumbnail_path, music_url
         | 
| 118 | 
            +
                file = File.new(thumbnail_path)
         | 
| 119 | 
            +
                api = Helper.with(options)
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                api_opts = options.slice(:title, :description, :HQ_music_url)
         | 
| 122 | 
            +
                thumb_media_id = api.media_create("thumb", file)["thumb_media_id"]
         | 
| 123 | 
            +
                puts api.custom_message_send Wechat::Message.to(openid).music(thumb_media_id, music_url, api_opts)
         | 
| 124 | 
            +
              end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
              desc "custom_news [OPENID, NEWS_YAML_FILE]", "发送图文客服消息"
         | 
| 127 | 
            +
              def custom_news openid, news_yaml
         | 
| 128 | 
            +
                articles = YAML.load(File.new(news_yaml).read)
         | 
| 129 | 
            +
                puts Helper.with(options).custom_message_send Wechat::Message.to(openid).news(articles["articles"])
         | 
| 130 | 
            +
              end
         | 
| 131 | 
            +
             | 
| 132 | 
            +
            end
         | 
| 133 | 
            +
             | 
| 134 | 
            +
            App.start
         | 
    
        data/lib/wechat-rails.rb
    ADDED
    
    | @@ -0,0 +1,39 @@ | |
| 1 | 
            +
            require "wechat/api"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Wechat
         | 
| 4 | 
            +
              autoload :Message, "wechat/message"
         | 
| 5 | 
            +
              autoload :Responder, "wechat/responder"
         | 
| 6 | 
            +
              autoload :Response, "wechat/response"
         | 
| 7 | 
            +
             | 
| 8 | 
            +
              class AccessTokenExpiredError < StandardError; end
         | 
| 9 | 
            +
              class ResponseError < StandardError
         | 
| 10 | 
            +
                attr_reader :error_code
         | 
| 11 | 
            +
                def initialize(errcode, errmsg)
         | 
| 12 | 
            +
                  error_code = errcode
         | 
| 13 | 
            +
                  super "#{errmsg}(#{error_code})"
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
              end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
              attr_reader :config
         | 
| 18 | 
            +
             | 
| 19 | 
            +
              def self.config
         | 
| 20 | 
            +
                @config ||= OpenStruct.new(
         | 
| 21 | 
            +
                  app_id: ENV["WECHAT_APPID"],
         | 
| 22 | 
            +
                  secret: ENV["WECHAT_SECRET"],
         | 
| 23 | 
            +
                  token: ENV["WECHAT_TOKEN"],
         | 
| 24 | 
            +
                  access_token: ENV["WECHAT_ACCESS_TOKEN"]
         | 
| 25 | 
            +
                )
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
              def self.api
         | 
| 29 | 
            +
                @api ||= Wechat::Api.new(self.config.app_id, self.config.secret, self.config.access_token)
         | 
| 30 | 
            +
              end
         | 
| 31 | 
            +
            end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
            if defined? ActionController::Base
         | 
| 34 | 
            +
              class ActionController::Base
         | 
| 35 | 
            +
                def self.wechat_rails
         | 
| 36 | 
            +
                  self.send(:include, Wechat::Responder)
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
              end
         | 
| 39 | 
            +
            end
         | 
| @@ -0,0 +1,35 @@ | |
| 1 | 
            +
            module Wechat
         | 
| 2 | 
            +
              class AccessToken
         | 
| 3 | 
            +
                attr_reader :client, :appid, :secret, :token_file, :token_data
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                def initialize(client, appid, secret, token_file)
         | 
| 6 | 
            +
                  @appid = appid
         | 
| 7 | 
            +
                  @secret = secret
         | 
| 8 | 
            +
                  @client = client
         | 
| 9 | 
            +
                  @token_file = token_file
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def token
         | 
| 13 | 
            +
                  begin
         | 
| 14 | 
            +
                    @token_data ||= JSON.parse(File.read(token_file))
         | 
| 15 | 
            +
                  rescue
         | 
| 16 | 
            +
                    self.refresh
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
                  return valid_token(@token_data)
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def refresh
         | 
| 22 | 
            +
                  data = client.get("token", params:{grant_type: "client_credential", appid: appid, secret: secret})
         | 
| 23 | 
            +
                  File.open(token_file, 'w'){|f| f.write(data.to_s)} if valid_token(data)
         | 
| 24 | 
            +
                  return @token_data = data
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                private 
         | 
| 28 | 
            +
                def valid_token token_data
         | 
| 29 | 
            +
                  access_token = token_data["access_token"]
         | 
| 30 | 
            +
                  raise "Response didn't have access_token" if  access_token.blank?
         | 
| 31 | 
            +
                  return access_token
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
            end
         | 
    
        data/lib/wechat/api.rb
    ADDED
    
    | @@ -0,0 +1,68 @@ | |
| 1 | 
            +
            require 'wechat/client'
         | 
| 2 | 
            +
            require 'wechat/access_token'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            class Wechat::Api
         | 
| 5 | 
            +
              attr_reader :app_id, :secret, :access_token, :client
         | 
| 6 | 
            +
             | 
| 7 | 
            +
              API_BASE = "https://api.weixin.qq.com/cgi-bin/"
         | 
| 8 | 
            +
              FILE_BASE = "http://file.api.weixin.qq.com/cgi-bin/"
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              def initialize app_id, secret, token_file
         | 
| 11 | 
            +
                @client = Wechat::Client.new(API_BASE)
         | 
| 12 | 
            +
                @access_token = Wechat::AccessToken.new(@client, app_id, secret, token_file)
         | 
| 13 | 
            +
              end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              def users
         | 
| 16 | 
            +
                get("user/get")
         | 
| 17 | 
            +
              end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
              def user openid
         | 
| 20 | 
            +
                get("user/info", params:{openid: openid})
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
              def menu
         | 
| 24 | 
            +
                get("menu/get")
         | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
              def menu_delete
         | 
| 28 | 
            +
                get("menu/delete")
         | 
| 29 | 
            +
              end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
              def menu_create menu
         | 
| 32 | 
            +
                # 微信不接受7bit escaped json(eg \uxxxx), 中文必须UTF-8编码, 这可能是个安全漏洞
         | 
| 33 | 
            +
                post("menu/create", JSON.generate(menu))
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
              def media media_id
         | 
| 37 | 
            +
                response = get "media/get", params:{media_id: media_id}, base: FILE_BASE, as: :file
         | 
| 38 | 
            +
              end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
              def media_create type, file
         | 
| 41 | 
            +
                post "media/upload", {upload:{media: file}}, params:{type: type}, base: FILE_BASE
         | 
| 42 | 
            +
              end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
              def custom_message_send message
         | 
| 45 | 
            +
                post "message/custom/send", message.to_json, content_type: :json
         | 
| 46 | 
            +
              end
         | 
| 47 | 
            +
              
         | 
| 48 | 
            +
             | 
| 49 | 
            +
              protected
         | 
| 50 | 
            +
              def get path, headers={}
         | 
| 51 | 
            +
                with_access_token(headers[:params]){|params| client.get path, headers.merge(params: params)}
         | 
| 52 | 
            +
              end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
              def post path, payload, headers = {}
         | 
| 55 | 
            +
                with_access_token(headers[:params]){|params| client.post path, payload, headers.merge(params: params)}
         | 
| 56 | 
            +
              end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
              def with_access_token params={}, tries=2
         | 
| 59 | 
            +
                begin
         | 
| 60 | 
            +
                  params ||= {}
         | 
| 61 | 
            +
                  yield(params.merge(access_token: access_token.token))
         | 
| 62 | 
            +
                rescue Wechat::AccessTokenExpiredError => ex
         | 
| 63 | 
            +
                  access_token.refresh
         | 
| 64 | 
            +
                  retry unless (tries -= 1).zero?
         | 
| 65 | 
            +
                end 
         | 
| 66 | 
            +
              end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
            end
         | 
| @@ -0,0 +1,74 @@ | |
| 1 | 
            +
            require 'rest_client'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Wechat
         | 
| 4 | 
            +
              class Client
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                attr_reader :base
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                def initialize(base)
         | 
| 9 | 
            +
                  @base = base
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def get path, header={}
         | 
| 13 | 
            +
                  request(path, header) do |url, header|
         | 
| 14 | 
            +
                    RestClient.get(url, header)
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def post path, payload, header = {}
         | 
| 19 | 
            +
                  request(path, header) do |url, header|
         | 
| 20 | 
            +
                    RestClient.post(url, payload, header)
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def request path, header={}, &block
         | 
| 25 | 
            +
                  url = "#{header.delete(:base) || self.base}#{path}"
         | 
| 26 | 
            +
                  as = header.delete(:as)
         | 
| 27 | 
            +
                  header.merge!(:accept => :json)
         | 
| 28 | 
            +
                  response = yield(url, header)
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  raise "Request not OK, response code #{response.code}" if response.code != 200
         | 
| 31 | 
            +
                  parse_response(response, as || :json) do |parse_as, data|
         | 
| 32 | 
            +
                    break data unless (parse_as == :json && data["errcode"].present?)
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                    case data["errcode"]
         | 
| 35 | 
            +
                    when 0 # for request didn't expect results
         | 
| 36 | 
            +
                      true
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                    when 42001, 40014 #42001: access_token超时, 40014:不合法的access_token
         | 
| 39 | 
            +
                      raise AccessTokenExpiredError
         | 
| 40 | 
            +
                      
         | 
| 41 | 
            +
                    else
         | 
| 42 | 
            +
                      raise ResponseError.new(data['errcode'], data['errmsg'])
         | 
| 43 | 
            +
                    end
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                private
         | 
| 48 | 
            +
                def parse_response response, as
         | 
| 49 | 
            +
                  content_type = response.headers[:content_type] 
         | 
| 50 | 
            +
                  parse_as = {
         | 
| 51 | 
            +
                    /^application\/json/ => :json,
         | 
| 52 | 
            +
                    /^image\/.*/ => :file
         | 
| 53 | 
            +
                  }.inject([]){|memo, match| memo<<match[1] if content_type =~ match[0]; memo}.first || as || :text
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  case parse_as
         | 
| 56 | 
            +
                  when :file
         | 
| 57 | 
            +
                    file = Tempfile.new("tmp")
         | 
| 58 | 
            +
                    file.binmode
         | 
| 59 | 
            +
                    file.write(response.body)
         | 
| 60 | 
            +
                    file.close
         | 
| 61 | 
            +
                    data = file
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  when :json
         | 
| 64 | 
            +
                    data = JSON.parse(response.body)
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  else
         | 
| 67 | 
            +
                    data = response.body
         | 
| 68 | 
            +
                  end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                  return yield(parse_as, data)
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
              end
         | 
| 74 | 
            +
            end
         | 
| @@ -0,0 +1,163 @@ | |
| 1 | 
            +
            module Wechat
         | 
| 2 | 
            +
              class Message
         | 
| 3 | 
            +
             | 
| 4 | 
            +
                JSON_KEY_MAP = {
         | 
| 5 | 
            +
                  "ToUserName" => "touser",
         | 
| 6 | 
            +
                  "MediaId" => "media_id",
         | 
| 7 | 
            +
                  "ThumbMediaId" => "thumb_media_id"
         | 
| 8 | 
            +
                }
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                class << self
         | 
| 11 | 
            +
                  def from_hash message_hash
         | 
| 12 | 
            +
                    self.new(message_hash)
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  def to to_user
         | 
| 16 | 
            +
                    self.new(:ToUserName=>to_user, :CreateTime=>Time.now.to_i)
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                class ArticleBuilder
         | 
| 21 | 
            +
                  attr_reader :items
         | 
| 22 | 
            +
                  delegate :count, to: :items
         | 
| 23 | 
            +
                  def initialize 
         | 
| 24 | 
            +
                    @items=Array.new
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  def item title: "title", description: nil, pic_url: nil, url: nil
         | 
| 28 | 
            +
                    items << {:Title=> title, :Description=> description, :PicUrl=> pic_url, :Url=> url}
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                attr_reader :message_hash
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def initialize(message_hash)
         | 
| 35 | 
            +
                  @message_hash = message_hash || {}
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                def [](key)
         | 
| 39 | 
            +
                  message_hash[key]
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                def reply
         | 
| 43 | 
            +
                  Message.new(
         | 
| 44 | 
            +
                    :ToUserName=>message_hash[:FromUserName], 
         | 
| 45 | 
            +
                    :FromUserName=>message_hash[:ToUserName], 
         | 
| 46 | 
            +
                    :CreateTime=>Time.now.to_i
         | 
| 47 | 
            +
                  )
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                def as type
         | 
| 51 | 
            +
                  case type
         | 
| 52 | 
            +
                  when :text
         | 
| 53 | 
            +
                    message_hash[:Content]
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  when :image, :voice, :video
         | 
| 56 | 
            +
                    Wechat.api.media(message_hash[:MediaId])
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  when :location
         | 
| 59 | 
            +
                    message_hash.slice(:Location_X, :Location_Y, :Scale, :Label).inject({}){|results, value| 
         | 
| 60 | 
            +
                      results[value[0].to_s.underscore.to_sym] = value[1]; results}
         | 
| 61 | 
            +
                  else
         | 
| 62 | 
            +
                    raise "Don't know how to parse message as #{type}"
         | 
| 63 | 
            +
                  end
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                def to openid
         | 
| 67 | 
            +
                  update(:ToUserName=>openid)
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                def text content
         | 
| 71 | 
            +
                  update(:MsgType=>"text", :Content=>content)
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                def image media_id
         | 
| 75 | 
            +
                  update(:MsgType=>"image", :Image=>{:MediaId=>media_id})
         | 
| 76 | 
            +
                end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                def voice media_id
         | 
| 79 | 
            +
                  update(:MsgType=>"voice", :Voice=>{:MediaId=>media_id})
         | 
| 80 | 
            +
                end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                def video media_id, opts={}
         | 
| 83 | 
            +
                  video_fields = camelize_hash_keys(opts.slice(:title, :description).merge(media_id: media_id))
         | 
| 84 | 
            +
                  update(:MsgType=>"video", :Video=>video_fields)
         | 
| 85 | 
            +
                end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                def music thumb_media_id, music_url, opts={}
         | 
| 88 | 
            +
                  music_fields = camelize_hash_keys(opts.slice(:title, :description, :HQ_music_url).merge(music_url: music_url, thumb_media_id: thumb_media_id))
         | 
| 89 | 
            +
                  update(:MsgType=>"music", :Music=>music_fields)
         | 
| 90 | 
            +
                end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                def news collection, &block
         | 
| 93 | 
            +
                  if block_given?
         | 
| 94 | 
            +
                    article = ArticleBuilder.new
         | 
| 95 | 
            +
                    collection.each{|item| yield(article, item)}
         | 
| 96 | 
            +
                    items = article.items
         | 
| 97 | 
            +
                  else
         | 
| 98 | 
            +
                    items = collection.collect do |item| 
         | 
| 99 | 
            +
                     camelize_hash_keys(item.symbolize_keys.slice(:title, :description, :pic_url, :url))
         | 
| 100 | 
            +
                    end
         | 
| 101 | 
            +
                  end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                  update(:MsgType=>"news", :ArticleCount=> items.count, 
         | 
| 104 | 
            +
                    :Articles=> items.collect{|item| camelize_hash_keys(item)})
         | 
| 105 | 
            +
                end
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                def to_xml
         | 
| 108 | 
            +
                  message_hash.to_xml(root: "xml", children: "item", skip_instruct: true, skip_types: true)
         | 
| 109 | 
            +
                end
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                def to_json
         | 
| 112 | 
            +
                  json_hash = deep_recursive(message_hash) do |key, value|
         | 
| 113 | 
            +
                    key = key.to_s
         | 
| 114 | 
            +
                    [(JSON_KEY_MAP[key] || key.downcase), value]
         | 
| 115 | 
            +
                  end
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                  json_hash.slice!("touser", "msgtype", "content", "image", "voice", "video", "music", "news", "articles").to_hash
         | 
| 118 | 
            +
                  case json_hash["msgtype"]
         | 
| 119 | 
            +
                  when "text"
         | 
| 120 | 
            +
                    json_hash["text"] = {"content" => json_hash.delete("content")}
         | 
| 121 | 
            +
                  when "news"
         | 
| 122 | 
            +
                    json_hash["news"] = {"articles" => json_hash.delete("articles")}
         | 
| 123 | 
            +
                  end
         | 
| 124 | 
            +
                  JSON.generate(json_hash)
         | 
| 125 | 
            +
                end
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                def save_to! model_class
         | 
| 128 | 
            +
                  model = model_class.new(underscore_hash_keys(message_hash))
         | 
| 129 | 
            +
                  model.save!
         | 
| 130 | 
            +
                  return self
         | 
| 131 | 
            +
                end
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                private
         | 
| 134 | 
            +
                def camelize_hash_keys hash
         | 
| 135 | 
            +
                  deep_recursive(hash){|key, value| [key.to_s.camelize.to_sym, value]} 
         | 
| 136 | 
            +
                end
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                def underscore_hash_keys hash
         | 
| 139 | 
            +
                  deep_recursive(hash){|key, value| [key.to_s.underscore.to_sym, value]} 
         | 
| 140 | 
            +
                end
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                def update fields={}
         | 
| 143 | 
            +
                  message_hash.merge!(fields)
         | 
| 144 | 
            +
                  return self
         | 
| 145 | 
            +
                end
         | 
| 146 | 
            +
             | 
| 147 | 
            +
                def deep_recursive hash, &block
         | 
| 148 | 
            +
                  hash.inject({}) do |memo, val|
         | 
| 149 | 
            +
                    key,value = *val
         | 
| 150 | 
            +
                    case value.class.name
         | 
| 151 | 
            +
                    when "Hash"
         | 
| 152 | 
            +
                      value = deep_recursive(value, &block)
         | 
| 153 | 
            +
                    when "Array"
         | 
| 154 | 
            +
                      value = value.collect{|item| item.is_a?(Hash) ? deep_recursive(item, &block) : item}
         | 
| 155 | 
            +
                    end
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                    key,value = yield(key, value)
         | 
| 158 | 
            +
                    memo.merge!(key => value)
         | 
| 159 | 
            +
                  end
         | 
| 160 | 
            +
                end
         | 
| 161 | 
            +
             | 
| 162 | 
            +
              end
         | 
| 163 | 
            +
            end
         | 
| @@ -0,0 +1,98 @@ | |
| 1 | 
            +
            module Wechat
         | 
| 2 | 
            +
              module Responder
         | 
| 3 | 
            +
                extend ActiveSupport::Concern
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                included do 
         | 
| 6 | 
            +
                  self.skip_before_filter :verify_authenticity_token
         | 
| 7 | 
            +
                  self.before_filter :verify_signature, only: [:show, :create]
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                module ClassMethods
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def on message_type, with: nil, respond: nil, &block
         | 
| 13 | 
            +
                    raise "Unknow message type" unless message_type.in? [:text, :image, :voice, :video, :location, :link, :event, :fallback]
         | 
| 14 | 
            +
                    config=respond.nil? ? {} : {:respond=>respond}
         | 
| 15 | 
            +
                    config.merge!(:proc=>block) if block_given?
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    if (with.present? && !message_type.in?([:text, :event]))
         | 
| 18 | 
            +
                      raise "Only text and event message can take :with parameters"
         | 
| 19 | 
            +
                    else
         | 
| 20 | 
            +
                      config.merge!(:with=>with) if with.present?
         | 
| 21 | 
            +
                    end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                    responders(message_type) << config
         | 
| 24 | 
            +
                    return config
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  def responders type
         | 
| 28 | 
            +
                    @responders ||= Hash.new
         | 
| 29 | 
            +
                    @responders[type] ||= Array.new
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  def responder_for message, &block
         | 
| 33 | 
            +
                    message_type = message[:MsgType].to_sym
         | 
| 34 | 
            +
                    responders = responders(message_type)
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                    case message_type
         | 
| 37 | 
            +
                    when :text
         | 
| 38 | 
            +
                      yield(* match_responders(responders, message[:Content]))
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                    when :event
         | 
| 41 | 
            +
                      yield(* match_responders(responders, message[:Event]))
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                    else
         | 
| 44 | 
            +
                      yield(responders.first)
         | 
| 45 | 
            +
                    end
         | 
| 46 | 
            +
                  end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                  private 
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  def match_responders responders, value
         | 
| 51 | 
            +
                    matched = responders.inject({scoped:nil, general:nil}) do |matched, responder|
         | 
| 52 | 
            +
                      condition = responder[:with]
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                      if condition.nil?
         | 
| 55 | 
            +
                        matched[:general] ||= [responder, value]
         | 
| 56 | 
            +
                        next matched
         | 
| 57 | 
            +
                      end
         | 
| 58 | 
            +
                      
         | 
| 59 | 
            +
                      if condition.is_a? Regexp
         | 
| 60 | 
            +
                        matched[:scoped] ||= [responder] + $~.captures if(value =~ condition)
         | 
| 61 | 
            +
                      else
         | 
| 62 | 
            +
                        matched[:scoped] ||= [responder, value] if(value == condition)
         | 
| 63 | 
            +
                      end
         | 
| 64 | 
            +
                      matched
         | 
| 65 | 
            +
                    end
         | 
| 66 | 
            +
                    return matched[:scoped] || matched[:general] 
         | 
| 67 | 
            +
                  end
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                
         | 
| 71 | 
            +
                def show
         | 
| 72 | 
            +
                  render :text => params[:echostr]
         | 
| 73 | 
            +
                end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                def create
         | 
| 76 | 
            +
                  request = Wechat::Message.from_hash(params[:xml])
         | 
| 77 | 
            +
                  response = self.class.responder_for(request) do |responder, *args|
         | 
| 78 | 
            +
                    responder ||= self.class.responders(:fallback).first
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                    next if responder.nil?
         | 
| 81 | 
            +
                    next request.reply.text responder[:respond] if (responder[:respond])
         | 
| 82 | 
            +
                    next responder[:proc].call(*args.unshift(request)) if (responder[:proc])
         | 
| 83 | 
            +
                  end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                  if response.respond_to? :to_xml
         | 
| 86 | 
            +
                    render xml: response.to_xml
         | 
| 87 | 
            +
                  else
         | 
| 88 | 
            +
                    render :nothing => true, :status => 200, :content_type => 'text/html'
         | 
| 89 | 
            +
                  end
         | 
| 90 | 
            +
                end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                private
         | 
| 93 | 
            +
                def verify_signature
         | 
| 94 | 
            +
                  array = [Wechat.config.token, params[:timestamp], params[:nonce]].compact.sort
         | 
| 95 | 
            +
                  render :text => "Forbidden", :status => 403 if params[:signature] != Digest::SHA1.hexdigest(array.join)
         | 
| 96 | 
            +
                end
         | 
| 97 | 
            +
              end
         | 
| 98 | 
            +
            end
         | 
    
        metadata
    ADDED
    
    | @@ -0,0 +1,109 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: wechat-rails
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: 0.1.0
         | 
| 5 | 
            +
            platform: ruby
         | 
| 6 | 
            +
            authors:
         | 
| 7 | 
            +
            - Skinnyworm
         | 
| 8 | 
            +
            autorequire: 
         | 
| 9 | 
            +
            bindir: bin
         | 
| 10 | 
            +
            cert_chain: []
         | 
| 11 | 
            +
            date: 2014-04-01 00:00:00.000000000 Z
         | 
| 12 | 
            +
            dependencies:
         | 
| 13 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 14 | 
            +
              name: rails
         | 
| 15 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 16 | 
            +
                requirements:
         | 
| 17 | 
            +
                - - ~>
         | 
| 18 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 19 | 
            +
                    version: 3.2.14
         | 
| 20 | 
            +
              type: :runtime
         | 
| 21 | 
            +
              prerelease: false
         | 
| 22 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 23 | 
            +
                requirements:
         | 
| 24 | 
            +
                - - ~>
         | 
| 25 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 26 | 
            +
                    version: 3.2.14
         | 
| 27 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 28 | 
            +
              name: nokogiri
         | 
| 29 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 30 | 
            +
                requirements:
         | 
| 31 | 
            +
                - - '>='
         | 
| 32 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 33 | 
            +
                    version: 1.6.0
         | 
| 34 | 
            +
              type: :runtime
         | 
| 35 | 
            +
              prerelease: false
         | 
| 36 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 37 | 
            +
                requirements:
         | 
| 38 | 
            +
                - - '>='
         | 
| 39 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 40 | 
            +
                    version: 1.6.0
         | 
| 41 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 42 | 
            +
              name: rest-client
         | 
| 43 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 44 | 
            +
                requirements:
         | 
| 45 | 
            +
                - - '>='
         | 
| 46 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 47 | 
            +
                    version: '0'
         | 
| 48 | 
            +
              type: :runtime
         | 
| 49 | 
            +
              prerelease: false
         | 
| 50 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 51 | 
            +
                requirements:
         | 
| 52 | 
            +
                - - '>='
         | 
| 53 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 54 | 
            +
                    version: '0'
         | 
| 55 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 56 | 
            +
              name: rspec-rails
         | 
| 57 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 58 | 
            +
                requirements:
         | 
| 59 | 
            +
                - - ~>
         | 
| 60 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 61 | 
            +
                    version: 2.14.0
         | 
| 62 | 
            +
              type: :development
         | 
| 63 | 
            +
              prerelease: false
         | 
| 64 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 65 | 
            +
                requirements:
         | 
| 66 | 
            +
                - - ~>
         | 
| 67 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 68 | 
            +
                    version: 2.14.0
         | 
| 69 | 
            +
            description: API and message handling for wechat in rails environment
         | 
| 70 | 
            +
            email: askinnyworm@gmail.com
         | 
| 71 | 
            +
            executables:
         | 
| 72 | 
            +
            - wechat
         | 
| 73 | 
            +
            extensions: []
         | 
| 74 | 
            +
            extra_rdoc_files: []
         | 
| 75 | 
            +
            files:
         | 
| 76 | 
            +
            - LICENSE
         | 
| 77 | 
            +
            - README.md
         | 
| 78 | 
            +
            - Rakefile
         | 
| 79 | 
            +
            - bin/wechat
         | 
| 80 | 
            +
            - lib/wechat-rails.rb
         | 
| 81 | 
            +
            - lib/wechat/access_token.rb
         | 
| 82 | 
            +
            - lib/wechat/api.rb
         | 
| 83 | 
            +
            - lib/wechat/client.rb
         | 
| 84 | 
            +
            - lib/wechat/message.rb
         | 
| 85 | 
            +
            - lib/wechat/responder.rb
         | 
| 86 | 
            +
            homepage: https://github.com/skinnyworm/wechat-rails
         | 
| 87 | 
            +
            licenses: []
         | 
| 88 | 
            +
            metadata: {}
         | 
| 89 | 
            +
            post_install_message: 
         | 
| 90 | 
            +
            rdoc_options: []
         | 
| 91 | 
            +
            require_paths:
         | 
| 92 | 
            +
            - lib
         | 
| 93 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 94 | 
            +
              requirements:
         | 
| 95 | 
            +
              - - '>='
         | 
| 96 | 
            +
                - !ruby/object:Gem::Version
         | 
| 97 | 
            +
                  version: '0'
         | 
| 98 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 99 | 
            +
              requirements:
         | 
| 100 | 
            +
              - - '>='
         | 
| 101 | 
            +
                - !ruby/object:Gem::Version
         | 
| 102 | 
            +
                  version: '0'
         | 
| 103 | 
            +
            requirements: []
         | 
| 104 | 
            +
            rubyforge_project: 
         | 
| 105 | 
            +
            rubygems_version: 2.2.1
         | 
| 106 | 
            +
            signing_key: 
         | 
| 107 | 
            +
            specification_version: 4
         | 
| 108 | 
            +
            summary: DSL for wechat message handling and api
         | 
| 109 | 
            +
            test_files: []
         |