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: []
         
     |