eventflit 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/.document +5 -0
 - data/.gemtest +0 -0
 - data/.gitignore +24 -0
 - data/.travis.yml +16 -0
 - data/CHANGELOG.md +0 -0
 - data/Gemfile +2 -0
 - data/LICENSE +20 -0
 - data/README.md +301 -0
 - data/Rakefile +11 -0
 - data/eventflit.gemspec +33 -0
 - data/examples/async_message.rb +28 -0
 - data/lib/eventflit.rb +69 -0
 - data/lib/eventflit/channel.rb +185 -0
 - data/lib/eventflit/client.rb +437 -0
 - data/lib/eventflit/native_notification/client.rb +68 -0
 - data/lib/eventflit/request.rb +109 -0
 - data/lib/eventflit/resource.rb +36 -0
 - data/lib/eventflit/version.rb +3 -0
 - data/lib/eventflit/webhook.rb +110 -0
 - data/spec/channel_spec.rb +170 -0
 - data/spec/client_spec.rb +629 -0
 - data/spec/spec_helper.rb +26 -0
 - data/spec/web_hook_spec.rb +117 -0
 - metadata +207 -0
 
| 
         @@ -0,0 +1,68 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Eventflit
         
     | 
| 
      
 2 
     | 
    
         
            +
              module NativeNotification
         
     | 
| 
      
 3 
     | 
    
         
            +
                class Client
         
     | 
| 
      
 4 
     | 
    
         
            +
                  attr_reader :app_id, :host
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
                  API_PREFIX = "publisher/app/"
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                  def initialize(app_id, host, scheme, eventflit_client)
         
     | 
| 
      
 9 
     | 
    
         
            +
                    @app_id = app_id
         
     | 
| 
      
 10 
     | 
    
         
            +
                    @host = host
         
     | 
| 
      
 11 
     | 
    
         
            +
                    @scheme = scheme
         
     | 
| 
      
 12 
     | 
    
         
            +
                    @eventflit_client = eventflit_client
         
     | 
| 
      
 13 
     | 
    
         
            +
                  end
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                  # Send a notification via the native notifications API
         
     | 
| 
      
 16 
     | 
    
         
            +
                  def notify(interests, data = {})
         
     | 
| 
      
 17 
     | 
    
         
            +
                    Request.new(
         
     | 
| 
      
 18 
     | 
    
         
            +
                      @eventflit_client,
         
     | 
| 
      
 19 
     | 
    
         
            +
                      :post,
         
     | 
| 
      
 20 
     | 
    
         
            +
                      url("/publishes"),
         
     | 
| 
      
 21 
     | 
    
         
            +
                      {},
         
     | 
| 
      
 22 
     | 
    
         
            +
                      payload(interests, data)
         
     | 
| 
      
 23 
     | 
    
         
            +
                    ).send_sync
         
     | 
| 
      
 24 
     | 
    
         
            +
                  end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                  private
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                  # {
         
     | 
| 
      
 29 
     | 
    
         
            +
                  #   interests: [Array of interests],
         
     | 
| 
      
 30 
     | 
    
         
            +
                  #   apns: {
         
     | 
| 
      
 31 
     | 
    
         
            +
                  #     See https://docs.eventflit.com/push_notifications/ios/server
         
     | 
| 
      
 32 
     | 
    
         
            +
                  #   },
         
     | 
| 
      
 33 
     | 
    
         
            +
                  #   gcm: {
         
     | 
| 
      
 34 
     | 
    
         
            +
                  #     See https://docs.eventflit.com/push_notifications/android/server
         
     | 
| 
      
 35 
     | 
    
         
            +
                  #   }
         
     | 
| 
      
 36 
     | 
    
         
            +
                  # }
         
     | 
| 
      
 37 
     | 
    
         
            +
                  #
         
     | 
| 
      
 38 
     | 
    
         
            +
                  # @raise [Eventflit::Error] if the interests array is empty
         
     | 
| 
      
 39 
     | 
    
         
            +
                  # @return [String]
         
     | 
| 
      
 40 
     | 
    
         
            +
                  def payload(interests, data)
         
     | 
| 
      
 41 
     | 
    
         
            +
                    interests = Array(interests).map(&:to_s)
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                    raise Eventflit::Error, "Interests array must not be empty" if interests.length == 0
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                    data = deep_symbolize_keys!(data)
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                    data.merge!(interests: interests)
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                    MultiJson.encode(data)
         
     | 
| 
      
 50 
     | 
    
         
            +
                  end
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                  def url(path = nil)
         
     | 
| 
      
 53 
     | 
    
         
            +
                    URI.parse("#{@scheme}://#{@host}/#{API_PREFIX}/#{@app_id}#{path}")
         
     | 
| 
      
 54 
     | 
    
         
            +
                  end
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
                  # Symbolize all keys in the hash recursively
         
     | 
| 
      
 57 
     | 
    
         
            +
                  def deep_symbolize_keys!(hash)
         
     | 
| 
      
 58 
     | 
    
         
            +
                    hash.keys.each do |k|
         
     | 
| 
      
 59 
     | 
    
         
            +
                      ks = k.respond_to?(:to_sym) ? k.to_sym : k
         
     | 
| 
      
 60 
     | 
    
         
            +
                      hash[ks] = hash.delete(k)
         
     | 
| 
      
 61 
     | 
    
         
            +
                      deep_symbolize_keys!(hash[ks]) if hash[ks].kind_of?(Hash)
         
     | 
| 
      
 62 
     | 
    
         
            +
                    end
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                    hash
         
     | 
| 
      
 65 
     | 
    
         
            +
                  end
         
     | 
| 
      
 66 
     | 
    
         
            +
                end
         
     | 
| 
      
 67 
     | 
    
         
            +
              end
         
     | 
| 
      
 68 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,109 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'eventflit-signature'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'digest/md5'
         
     | 
| 
      
 3 
     | 
    
         
            +
            require 'multi_json'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module Eventflit
         
     | 
| 
      
 6 
     | 
    
         
            +
              class Request
         
     | 
| 
      
 7 
     | 
    
         
            +
                attr_reader :body, :params
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                def initialize(client, verb, uri, params, body = nil)
         
     | 
| 
      
 10 
     | 
    
         
            +
                  @client, @verb, @uri = client, verb, uri
         
     | 
| 
      
 11 
     | 
    
         
            +
                  @head = {
         
     | 
| 
      
 12 
     | 
    
         
            +
                    'X-Eventflit-Library' => 'eventflit-http-ruby ' + Eventflit::VERSION
         
     | 
| 
      
 13 
     | 
    
         
            +
                  }
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                  @body = body
         
     | 
| 
      
 16 
     | 
    
         
            +
                  if body
         
     | 
| 
      
 17 
     | 
    
         
            +
                    params[:body_md5] = Digest::MD5.hexdigest(body)
         
     | 
| 
      
 18 
     | 
    
         
            +
                    @head['Content-Type'] = 'application/json'
         
     | 
| 
      
 19 
     | 
    
         
            +
                  end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                  request = Eventflit::Signature::Request.new(verb.to_s.upcase, uri.path, params)
         
     | 
| 
      
 22 
     | 
    
         
            +
                  request.sign(client.authentication_token)
         
     | 
| 
      
 23 
     | 
    
         
            +
                  @params = request.signed_params
         
     | 
| 
      
 24 
     | 
    
         
            +
                end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                def send_sync
         
     | 
| 
      
 27 
     | 
    
         
            +
                  http = @client.sync_http_client
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 30 
     | 
    
         
            +
                    response = http.request(@verb, @uri, @params, @body, @head)
         
     | 
| 
      
 31 
     | 
    
         
            +
                  rescue HTTPClient::BadResponseError, HTTPClient::TimeoutError,
         
     | 
| 
      
 32 
     | 
    
         
            +
                         SocketError, Errno::ECONNREFUSED => e
         
     | 
| 
      
 33 
     | 
    
         
            +
                    error = Eventflit::HTTPError.new("#{e.message} (#{e.class})")
         
     | 
| 
      
 34 
     | 
    
         
            +
                    error.original_error = e
         
     | 
| 
      
 35 
     | 
    
         
            +
                    raise error
         
     | 
| 
      
 36 
     | 
    
         
            +
                  end
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                  body = response.body ? response.body.chomp : nil
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                  return handle_response(response.code.to_i, body)
         
     | 
| 
      
 41 
     | 
    
         
            +
                end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                def send_async
         
     | 
| 
      
 44 
     | 
    
         
            +
                  if defined?(EventMachine) && EventMachine.reactor_running?
         
     | 
| 
      
 45 
     | 
    
         
            +
                    http_client = @client.em_http_client(@uri)
         
     | 
| 
      
 46 
     | 
    
         
            +
                    df = EM::DefaultDeferrable.new
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                    http = case @verb
         
     | 
| 
      
 49 
     | 
    
         
            +
                    when :post
         
     | 
| 
      
 50 
     | 
    
         
            +
                      http_client.post({
         
     | 
| 
      
 51 
     | 
    
         
            +
                        :query => @params, :body => @body, :head => @head
         
     | 
| 
      
 52 
     | 
    
         
            +
                      })
         
     | 
| 
      
 53 
     | 
    
         
            +
                    when :get
         
     | 
| 
      
 54 
     | 
    
         
            +
                      http_client.get({
         
     | 
| 
      
 55 
     | 
    
         
            +
                        :query => @params, :head => @head
         
     | 
| 
      
 56 
     | 
    
         
            +
                      })
         
     | 
| 
      
 57 
     | 
    
         
            +
                    else
         
     | 
| 
      
 58 
     | 
    
         
            +
                      raise "Unsupported verb"
         
     | 
| 
      
 59 
     | 
    
         
            +
                    end
         
     | 
| 
      
 60 
     | 
    
         
            +
                    http.callback {
         
     | 
| 
      
 61 
     | 
    
         
            +
                      begin
         
     | 
| 
      
 62 
     | 
    
         
            +
                        df.succeed(handle_response(http.response_header.status, http.response.chomp))
         
     | 
| 
      
 63 
     | 
    
         
            +
                      rescue => e
         
     | 
| 
      
 64 
     | 
    
         
            +
                        df.fail(e)
         
     | 
| 
      
 65 
     | 
    
         
            +
                      end
         
     | 
| 
      
 66 
     | 
    
         
            +
                    }
         
     | 
| 
      
 67 
     | 
    
         
            +
                    http.errback { |e|
         
     | 
| 
      
 68 
     | 
    
         
            +
                      message = "Network error connecting to eventflit (#{http.error})"
         
     | 
| 
      
 69 
     | 
    
         
            +
                      Eventflit.logger.debug(message)
         
     | 
| 
      
 70 
     | 
    
         
            +
                      df.fail(Error.new(message))
         
     | 
| 
      
 71 
     | 
    
         
            +
                    }
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
                    return df
         
     | 
| 
      
 74 
     | 
    
         
            +
                  else
         
     | 
| 
      
 75 
     | 
    
         
            +
                    http = @client.sync_http_client
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
                    return http.request_async(@verb, @uri, @params, @body, @head)
         
     | 
| 
      
 78 
     | 
    
         
            +
                  end
         
     | 
| 
      
 79 
     | 
    
         
            +
                end
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
                private
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
                def handle_response(status_code, body)
         
     | 
| 
      
 84 
     | 
    
         
            +
                  case status_code
         
     | 
| 
      
 85 
     | 
    
         
            +
                  when 200
         
     | 
| 
      
 86 
     | 
    
         
            +
                    return symbolize_first_level(MultiJson.decode(body))
         
     | 
| 
      
 87 
     | 
    
         
            +
                  when 202
         
     | 
| 
      
 88 
     | 
    
         
            +
                    return body.empty? ? true : symbolize_first_level(MultiJson.decode(body))
         
     | 
| 
      
 89 
     | 
    
         
            +
                  when 400
         
     | 
| 
      
 90 
     | 
    
         
            +
                    raise Error, "Bad request: #{body}"
         
     | 
| 
      
 91 
     | 
    
         
            +
                  when 401
         
     | 
| 
      
 92 
     | 
    
         
            +
                    raise AuthenticationError, body
         
     | 
| 
      
 93 
     | 
    
         
            +
                  when 404
         
     | 
| 
      
 94 
     | 
    
         
            +
                    raise Error, "404 Not found (#{@uri.path})"
         
     | 
| 
      
 95 
     | 
    
         
            +
                  when 407
         
     | 
| 
      
 96 
     | 
    
         
            +
                    raise Error, "Proxy Authentication Required"
         
     | 
| 
      
 97 
     | 
    
         
            +
                  else
         
     | 
| 
      
 98 
     | 
    
         
            +
                    raise Error, "Unknown error (status code #{status_code}): #{body}"
         
     | 
| 
      
 99 
     | 
    
         
            +
                  end
         
     | 
| 
      
 100 
     | 
    
         
            +
                end
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
                def symbolize_first_level(hash)
         
     | 
| 
      
 103 
     | 
    
         
            +
                  hash.inject({}) do |result, (key, value)|
         
     | 
| 
      
 104 
     | 
    
         
            +
                    result[key.to_sym] = value
         
     | 
| 
      
 105 
     | 
    
         
            +
                    result
         
     | 
| 
      
 106 
     | 
    
         
            +
                  end
         
     | 
| 
      
 107 
     | 
    
         
            +
                end
         
     | 
| 
      
 108 
     | 
    
         
            +
              end
         
     | 
| 
      
 109 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,36 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Eventflit
         
     | 
| 
      
 2 
     | 
    
         
            +
              class Resource
         
     | 
| 
      
 3 
     | 
    
         
            +
                def initialize(client, path)
         
     | 
| 
      
 4 
     | 
    
         
            +
                  @client = client
         
     | 
| 
      
 5 
     | 
    
         
            +
                  @path = path
         
     | 
| 
      
 6 
     | 
    
         
            +
                end
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                def get(params)
         
     | 
| 
      
 9 
     | 
    
         
            +
                  create_request(:get, params).send_sync
         
     | 
| 
      
 10 
     | 
    
         
            +
                end
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                def get_async(params)
         
     | 
| 
      
 13 
     | 
    
         
            +
                  create_request(:get, params).send_async
         
     | 
| 
      
 14 
     | 
    
         
            +
                end
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                def post(params)
         
     | 
| 
      
 17 
     | 
    
         
            +
                  body = MultiJson.encode(params)
         
     | 
| 
      
 18 
     | 
    
         
            +
                  create_request(:post, {}, body).send_sync
         
     | 
| 
      
 19 
     | 
    
         
            +
                end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                def post_async(params)
         
     | 
| 
      
 22 
     | 
    
         
            +
                  body = MultiJson.encode(params)
         
     | 
| 
      
 23 
     | 
    
         
            +
                  create_request(:post, {}, body).send_async
         
     | 
| 
      
 24 
     | 
    
         
            +
                end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                private
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                def create_request(verb, params, body = nil)
         
     | 
| 
      
 29 
     | 
    
         
            +
                  Request.new(@client, verb, url, params, body)
         
     | 
| 
      
 30 
     | 
    
         
            +
                end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                def url
         
     | 
| 
      
 33 
     | 
    
         
            +
                  @_url ||= @client.url(@path)
         
     | 
| 
      
 34 
     | 
    
         
            +
                end
         
     | 
| 
      
 35 
     | 
    
         
            +
              end
         
     | 
| 
      
 36 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,110 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'multi_json'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'openssl'
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            module Eventflit
         
     | 
| 
      
 5 
     | 
    
         
            +
              # Used to parse and authenticate WebHooks
         
     | 
| 
      
 6 
     | 
    
         
            +
              #
         
     | 
| 
      
 7 
     | 
    
         
            +
              # @example Sinatra
         
     | 
| 
      
 8 
     | 
    
         
            +
              #   post '/webhooks' do
         
     | 
| 
      
 9 
     | 
    
         
            +
              #     webhook = Eventflit::WebHook.new(request)
         
     | 
| 
      
 10 
     | 
    
         
            +
              #     if webhook.valid?
         
     | 
| 
      
 11 
     | 
    
         
            +
              #       webhook.events.each do |event|
         
     | 
| 
      
 12 
     | 
    
         
            +
              #         case event["name"]
         
     | 
| 
      
 13 
     | 
    
         
            +
              #         when 'channel_occupied'
         
     | 
| 
      
 14 
     | 
    
         
            +
              #           puts "Channel occupied: #{event["channel"]}"
         
     | 
| 
      
 15 
     | 
    
         
            +
              #         when 'channel_vacated'
         
     | 
| 
      
 16 
     | 
    
         
            +
              #           puts "Channel vacated: #{event["channel"]}"
         
     | 
| 
      
 17 
     | 
    
         
            +
              #         end
         
     | 
| 
      
 18 
     | 
    
         
            +
              #       end
         
     | 
| 
      
 19 
     | 
    
         
            +
              #     else
         
     | 
| 
      
 20 
     | 
    
         
            +
              #       status 401
         
     | 
| 
      
 21 
     | 
    
         
            +
              #     end
         
     | 
| 
      
 22 
     | 
    
         
            +
              #     return
         
     | 
| 
      
 23 
     | 
    
         
            +
              #   end
         
     | 
| 
      
 24 
     | 
    
         
            +
              #
         
     | 
| 
      
 25 
     | 
    
         
            +
              class WebHook
         
     | 
| 
      
 26 
     | 
    
         
            +
                attr_reader :key, :signature
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                # Provide either a Rack::Request or a Hash containing :key, :signature,
         
     | 
| 
      
 29 
     | 
    
         
            +
                # :body, and :content_type (optional)
         
     | 
| 
      
 30 
     | 
    
         
            +
                #
         
     | 
| 
      
 31 
     | 
    
         
            +
                def initialize(request, client = Eventflit)
         
     | 
| 
      
 32 
     | 
    
         
            +
                  @client = client
         
     | 
| 
      
 33 
     | 
    
         
            +
                  # For Rack::Request and ActionDispatch::Request
         
     | 
| 
      
 34 
     | 
    
         
            +
                  if request.respond_to?(:env) && request.respond_to?(:content_type)
         
     | 
| 
      
 35 
     | 
    
         
            +
                    @key = request.env['HTTP_X_EVENTFLIT_KEY']
         
     | 
| 
      
 36 
     | 
    
         
            +
                    @signature = request.env["HTTP_X_EVENTFLIT_SIGNATURE"]
         
     | 
| 
      
 37 
     | 
    
         
            +
                    @content_type = request.content_type
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                    request.body.rewind
         
     | 
| 
      
 40 
     | 
    
         
            +
                    @body = request.body.read
         
     | 
| 
      
 41 
     | 
    
         
            +
                    request.body.rewind
         
     | 
| 
      
 42 
     | 
    
         
            +
                  else
         
     | 
| 
      
 43 
     | 
    
         
            +
                    @key, @signature, @body = request.values_at(:key, :signature, :body)
         
     | 
| 
      
 44 
     | 
    
         
            +
                    @content_type = request[:content_type] || 'application/json'
         
     | 
| 
      
 45 
     | 
    
         
            +
                  end
         
     | 
| 
      
 46 
     | 
    
         
            +
                end
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                # Returns whether the WebHook is valid by checking that the signature
         
     | 
| 
      
 49 
     | 
    
         
            +
                # matches the configured key & secret. In the case that the webhook is
         
     | 
| 
      
 50 
     | 
    
         
            +
                # invalid, the reason is logged
         
     | 
| 
      
 51 
     | 
    
         
            +
                #
         
     | 
| 
      
 52 
     | 
    
         
            +
                # @param extra_tokens [Hash] If you have extra tokens for your Eventflit app, you can specify them so that they're used to attempt validation.
         
     | 
| 
      
 53 
     | 
    
         
            +
                #
         
     | 
| 
      
 54 
     | 
    
         
            +
                def valid?(extra_tokens = nil)
         
     | 
| 
      
 55 
     | 
    
         
            +
                  extra_tokens = [extra_tokens] if extra_tokens.kind_of?(Hash)
         
     | 
| 
      
 56 
     | 
    
         
            +
                  if @key == @client.key
         
     | 
| 
      
 57 
     | 
    
         
            +
                    return check_signature(@client.secret)
         
     | 
| 
      
 58 
     | 
    
         
            +
                  elsif extra_tokens
         
     | 
| 
      
 59 
     | 
    
         
            +
                    extra_tokens.each do |token|
         
     | 
| 
      
 60 
     | 
    
         
            +
                      return check_signature(token[:secret]) if @key == token[:key]
         
     | 
| 
      
 61 
     | 
    
         
            +
                    end
         
     | 
| 
      
 62 
     | 
    
         
            +
                  end
         
     | 
| 
      
 63 
     | 
    
         
            +
                  Eventflit.logger.warn "Received webhook with unknown key: #{key}"
         
     | 
| 
      
 64 
     | 
    
         
            +
                  return false
         
     | 
| 
      
 65 
     | 
    
         
            +
                end
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
                # Array of events (as Hashes) contained inside the webhook
         
     | 
| 
      
 68 
     | 
    
         
            +
                #
         
     | 
| 
      
 69 
     | 
    
         
            +
                def events
         
     | 
| 
      
 70 
     | 
    
         
            +
                  data["events"]
         
     | 
| 
      
 71 
     | 
    
         
            +
                end
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
                # The time at which the WebHook was initially triggered by Eventflit, i.e.
         
     | 
| 
      
 74 
     | 
    
         
            +
                # when the event occurred
         
     | 
| 
      
 75 
     | 
    
         
            +
                #
         
     | 
| 
      
 76 
     | 
    
         
            +
                # @return [Time]
         
     | 
| 
      
 77 
     | 
    
         
            +
                #
         
     | 
| 
      
 78 
     | 
    
         
            +
                def time
         
     | 
| 
      
 79 
     | 
    
         
            +
                  Time.at(data["time_ms"].to_f/1000)
         
     | 
| 
      
 80 
     | 
    
         
            +
                end
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
                # Access the parsed WebHook body
         
     | 
| 
      
 83 
     | 
    
         
            +
                #
         
     | 
| 
      
 84 
     | 
    
         
            +
                def data
         
     | 
| 
      
 85 
     | 
    
         
            +
                  @data ||= begin
         
     | 
| 
      
 86 
     | 
    
         
            +
                    case @content_type
         
     | 
| 
      
 87 
     | 
    
         
            +
                    when 'application/json'
         
     | 
| 
      
 88 
     | 
    
         
            +
                      MultiJson.decode(@body)
         
     | 
| 
      
 89 
     | 
    
         
            +
                    else
         
     | 
| 
      
 90 
     | 
    
         
            +
                      raise "Unknown Content-Type (#{@content_type})"
         
     | 
| 
      
 91 
     | 
    
         
            +
                    end
         
     | 
| 
      
 92 
     | 
    
         
            +
                  end
         
     | 
| 
      
 93 
     | 
    
         
            +
                end
         
     | 
| 
      
 94 
     | 
    
         
            +
             
     | 
| 
      
 95 
     | 
    
         
            +
                private
         
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
      
 97 
     | 
    
         
            +
                # Checks signature against secret and returns boolean
         
     | 
| 
      
 98 
     | 
    
         
            +
                #
         
     | 
| 
      
 99 
     | 
    
         
            +
                def check_signature(secret)
         
     | 
| 
      
 100 
     | 
    
         
            +
                  digest = OpenSSL::Digest::SHA256.new
         
     | 
| 
      
 101 
     | 
    
         
            +
                  expected = OpenSSL::HMAC.hexdigest(digest, secret, @body)
         
     | 
| 
      
 102 
     | 
    
         
            +
                  if @signature == expected
         
     | 
| 
      
 103 
     | 
    
         
            +
                    return true
         
     | 
| 
      
 104 
     | 
    
         
            +
                  else
         
     | 
| 
      
 105 
     | 
    
         
            +
                    Eventflit.logger.warn "Received WebHook with invalid signature: got #{@signature}, expected #{expected}"
         
     | 
| 
      
 106 
     | 
    
         
            +
                    return false
         
     | 
| 
      
 107 
     | 
    
         
            +
                  end
         
     | 
| 
      
 108 
     | 
    
         
            +
                end
         
     | 
| 
      
 109 
     | 
    
         
            +
              end
         
     | 
| 
      
 110 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,170 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # -*- coding: utf-8 -*-
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'spec_helper'
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
            describe Eventflit::Channel do
         
     | 
| 
      
 5 
     | 
    
         
            +
              before do
         
     | 
| 
      
 6 
     | 
    
         
            +
                @client = Eventflit::Client.new({
         
     | 
| 
      
 7 
     | 
    
         
            +
                  :app_id => '20',
         
     | 
| 
      
 8 
     | 
    
         
            +
                  :key => '12345678900000001',
         
     | 
| 
      
 9 
     | 
    
         
            +
                  :secret => '12345678900000001',
         
     | 
| 
      
 10 
     | 
    
         
            +
                  :host => 'service.eventflit.com',
         
     | 
| 
      
 11 
     | 
    
         
            +
                  :port => 80,
         
     | 
| 
      
 12 
     | 
    
         
            +
                })
         
     | 
| 
      
 13 
     | 
    
         
            +
                @channel = @client['test_channel']
         
     | 
| 
      
 14 
     | 
    
         
            +
              end
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
              let(:eventflit_url_regexp) { %r{/apps/20/events} }
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
              def stub_post(status, body = nil)
         
     | 
| 
      
 19 
     | 
    
         
            +
                options = {:status => status}
         
     | 
| 
      
 20 
     | 
    
         
            +
                options.merge!({:body => body}) if body
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                stub_request(:post, eventflit_url_regexp).to_return(options)
         
     | 
| 
      
 23 
     | 
    
         
            +
              end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
              def stub_post_to_raise(e)
         
     | 
| 
      
 26 
     | 
    
         
            +
                stub_request(:post, eventflit_url_regexp).to_raise(e)
         
     | 
| 
      
 27 
     | 
    
         
            +
              end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
              describe '#trigger!' do
         
     | 
| 
      
 30 
     | 
    
         
            +
                it "should use @client.trigger internally" do
         
     | 
| 
      
 31 
     | 
    
         
            +
                  expect(@client).to receive(:trigger)
         
     | 
| 
      
 32 
     | 
    
         
            +
                  @channel.trigger('new_event', 'Some data')
         
     | 
| 
      
 33 
     | 
    
         
            +
                end
         
     | 
| 
      
 34 
     | 
    
         
            +
              end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
              describe '#trigger' do
         
     | 
| 
      
 37 
     | 
    
         
            +
                it "should log failure if error raised in http call" do
         
     | 
| 
      
 38 
     | 
    
         
            +
                  stub_post_to_raise(HTTPClient::BadResponseError)
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                  expect(Eventflit.logger).to receive(:error).with("Exception from WebMock (HTTPClient::BadResponseError) (Eventflit::HTTPError)")
         
     | 
| 
      
 41 
     | 
    
         
            +
                  expect(Eventflit.logger).to receive(:debug) #backtrace
         
     | 
| 
      
 42 
     | 
    
         
            +
                  channel = Eventflit::Channel.new(@client.url, 'test_channel', @client)
         
     | 
| 
      
 43 
     | 
    
         
            +
                  channel.trigger('new_event', 'Some data')
         
     | 
| 
      
 44 
     | 
    
         
            +
                end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                it "should log failure if Eventflit returns an error response" do
         
     | 
| 
      
 47 
     | 
    
         
            +
                  stub_post 401, "some signature info"
         
     | 
| 
      
 48 
     | 
    
         
            +
                  expect(Eventflit.logger).to receive(:error).with("some signature info (Eventflit::AuthenticationError)")
         
     | 
| 
      
 49 
     | 
    
         
            +
                  expect(Eventflit.logger).to receive(:debug) #backtrace
         
     | 
| 
      
 50 
     | 
    
         
            +
                  channel = Eventflit::Channel.new(@client.url, 'test_channel', @client)
         
     | 
| 
      
 51 
     | 
    
         
            +
                  channel.trigger('new_event', 'Some data')
         
     | 
| 
      
 52 
     | 
    
         
            +
                end
         
     | 
| 
      
 53 
     | 
    
         
            +
              end
         
     | 
| 
      
 54 
     | 
    
         
            +
             
     | 
| 
      
 55 
     | 
    
         
            +
              describe "#initialization" do
         
     | 
| 
      
 56 
     | 
    
         
            +
                it "should not be too long" do
         
     | 
| 
      
 57 
     | 
    
         
            +
                  expect { @client['b'*201] }.to raise_error(Eventflit::Error)
         
     | 
| 
      
 58 
     | 
    
         
            +
                end
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                it "should not use bad characters" do
         
     | 
| 
      
 61 
     | 
    
         
            +
                  expect { @client['*^!±`/""'] }.to raise_error(Eventflit::Error)
         
     | 
| 
      
 62 
     | 
    
         
            +
                end
         
     | 
| 
      
 63 
     | 
    
         
            +
              end
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
              describe "#trigger_async" do
         
     | 
| 
      
 66 
     | 
    
         
            +
                it "should use @client.trigger_async internally" do
         
     | 
| 
      
 67 
     | 
    
         
            +
                  expect(@client).to receive(:trigger_async)
         
     | 
| 
      
 68 
     | 
    
         
            +
                  @channel.trigger_async('new_event', 'Some data')
         
     | 
| 
      
 69 
     | 
    
         
            +
                end
         
     | 
| 
      
 70 
     | 
    
         
            +
              end
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
              describe '#info' do
         
     | 
| 
      
 73 
     | 
    
         
            +
                it "should call the Client#channel_info" do
         
     | 
| 
      
 74 
     | 
    
         
            +
                  expect(@client).to receive(:get)
         
     | 
| 
      
 75 
     | 
    
         
            +
                                       .with("/channels/mychannel", anything)
         
     | 
| 
      
 76 
     | 
    
         
            +
                                       .and_return({:occupied => true, :subscription_count => 12})
         
     | 
| 
      
 77 
     | 
    
         
            +
                  @channel = @client['mychannel']
         
     | 
| 
      
 78 
     | 
    
         
            +
                  @channel.info
         
     | 
| 
      
 79 
     | 
    
         
            +
                end
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
                it "should assemble the requested attributes into the info option" do
         
     | 
| 
      
 82 
     | 
    
         
            +
                  expect(@client).to receive(:get)
         
     | 
| 
      
 83 
     | 
    
         
            +
                                       .with(anything, {:info => "user_count,connection_count"})
         
     | 
| 
      
 84 
     | 
    
         
            +
                                       .and_return({:occupied => true, :subscription_count => 12, :user_count => 12})
         
     | 
| 
      
 85 
     | 
    
         
            +
                  @channel = @client['presence-foo']
         
     | 
| 
      
 86 
     | 
    
         
            +
                  @channel.info(%w{user_count connection_count})
         
     | 
| 
      
 87 
     | 
    
         
            +
                end
         
     | 
| 
      
 88 
     | 
    
         
            +
              end
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
              describe '#users' do
         
     | 
| 
      
 91 
     | 
    
         
            +
                it "should call the Client#channel_users" do
         
     | 
| 
      
 92 
     | 
    
         
            +
                  expect(@client).to receive(:get).with("/channels/presence-mychannel/users", {}).and_return({:users => {'id' => '4'}})
         
     | 
| 
      
 93 
     | 
    
         
            +
                  @channel = @client['presence-mychannel']
         
     | 
| 
      
 94 
     | 
    
         
            +
                  @channel.users
         
     | 
| 
      
 95 
     | 
    
         
            +
                end
         
     | 
| 
      
 96 
     | 
    
         
            +
              end
         
     | 
| 
      
 97 
     | 
    
         
            +
             
     | 
| 
      
 98 
     | 
    
         
            +
              describe "#authentication_string" do
         
     | 
| 
      
 99 
     | 
    
         
            +
                def authentication_string(*data)
         
     | 
| 
      
 100 
     | 
    
         
            +
                  lambda { @channel.authentication_string(*data) }
         
     | 
| 
      
 101 
     | 
    
         
            +
                end
         
     | 
| 
      
 102 
     | 
    
         
            +
             
     | 
| 
      
 103 
     | 
    
         
            +
                it "should return an authentication string given a socket id" do
         
     | 
| 
      
 104 
     | 
    
         
            +
                  auth = @channel.authentication_string('1.1')
         
     | 
| 
      
 105 
     | 
    
         
            +
             
     | 
| 
      
 106 
     | 
    
         
            +
                  expect(auth).to eq('12345678900000001:02259dff9a2a3f71ea8ab29ac0c0c0ef7996c8f3fd3702be5533f30da7d7fed4')
         
     | 
| 
      
 107 
     | 
    
         
            +
                end
         
     | 
| 
      
 108 
     | 
    
         
            +
             
     | 
| 
      
 109 
     | 
    
         
            +
                it "should raise error if authentication is invalid" do
         
     | 
| 
      
 110 
     | 
    
         
            +
                  [nil, ''].each do |invalid|
         
     | 
| 
      
 111 
     | 
    
         
            +
                    expect(authentication_string(invalid)).to raise_error Eventflit::Error
         
     | 
| 
      
 112 
     | 
    
         
            +
                  end
         
     | 
| 
      
 113 
     | 
    
         
            +
                end
         
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
                describe 'with extra string argument' do
         
     | 
| 
      
 116 
     | 
    
         
            +
                  it 'should be a string or nil' do
         
     | 
| 
      
 117 
     | 
    
         
            +
                    expect(authentication_string('1.1', 123)).to raise_error Eventflit::Error
         
     | 
| 
      
 118 
     | 
    
         
            +
                    expect(authentication_string('1.1', {})).to raise_error Eventflit::Error
         
     | 
| 
      
 119 
     | 
    
         
            +
             
     | 
| 
      
 120 
     | 
    
         
            +
                    expect(authentication_string('1.1', 'boom')).not_to raise_error
         
     | 
| 
      
 121 
     | 
    
         
            +
                    expect(authentication_string('1.1', nil)).not_to raise_error
         
     | 
| 
      
 122 
     | 
    
         
            +
                  end
         
     | 
| 
      
 123 
     | 
    
         
            +
             
     | 
| 
      
 124 
     | 
    
         
            +
                  it "should return an authentication string given a socket id and custom args" do
         
     | 
| 
      
 125 
     | 
    
         
            +
                    auth = @channel.authentication_string('1.1', 'foobar')
         
     | 
| 
      
 126 
     | 
    
         
            +
             
     | 
| 
      
 127 
     | 
    
         
            +
                    expect(auth).to eq("12345678900000001:#{hmac(@client.secret, "1.1:test_channel:foobar")}")
         
     | 
| 
      
 128 
     | 
    
         
            +
                  end
         
     | 
| 
      
 129 
     | 
    
         
            +
                end
         
     | 
| 
      
 130 
     | 
    
         
            +
              end
         
     | 
| 
      
 131 
     | 
    
         
            +
             
     | 
| 
      
 132 
     | 
    
         
            +
              describe '#authenticate' do
         
     | 
| 
      
 133 
     | 
    
         
            +
                before :each do
         
     | 
| 
      
 134 
     | 
    
         
            +
                  @custom_data = {:uid => 123, :info => {:name => 'Foo'}}
         
     | 
| 
      
 135 
     | 
    
         
            +
                end
         
     | 
| 
      
 136 
     | 
    
         
            +
             
     | 
| 
      
 137 
     | 
    
         
            +
                it 'should return a hash with signature including custom data and data as json string' do
         
     | 
| 
      
 138 
     | 
    
         
            +
                  allow(MultiJson).to receive(:encode).with(@custom_data).and_return 'a json string'
         
     | 
| 
      
 139 
     | 
    
         
            +
             
     | 
| 
      
 140 
     | 
    
         
            +
                  response = @channel.authenticate('1.1', @custom_data)
         
     | 
| 
      
 141 
     | 
    
         
            +
             
     | 
| 
      
 142 
     | 
    
         
            +
                  expect(response).to eq({
         
     | 
| 
      
 143 
     | 
    
         
            +
                    :auth => "12345678900000001:#{hmac(@client.secret, "1.1:test_channel:a json string")}",
         
     | 
| 
      
 144 
     | 
    
         
            +
                    :channel_data => 'a json string'
         
     | 
| 
      
 145 
     | 
    
         
            +
                  })
         
     | 
| 
      
 146 
     | 
    
         
            +
                end
         
     | 
| 
      
 147 
     | 
    
         
            +
             
     | 
| 
      
 148 
     | 
    
         
            +
                it 'should fail on invalid socket_ids' do
         
     | 
| 
      
 149 
     | 
    
         
            +
                  expect {
         
     | 
| 
      
 150 
     | 
    
         
            +
                    @channel.authenticate('1.1:')
         
     | 
| 
      
 151 
     | 
    
         
            +
                  }.to raise_error Eventflit::Error
         
     | 
| 
      
 152 
     | 
    
         
            +
             
     | 
| 
      
 153 
     | 
    
         
            +
                  expect {
         
     | 
| 
      
 154 
     | 
    
         
            +
                    @channel.authenticate('1.1foo', 'channel')
         
     | 
| 
      
 155 
     | 
    
         
            +
                  }.to raise_error Eventflit::Error
         
     | 
| 
      
 156 
     | 
    
         
            +
             
     | 
| 
      
 157 
     | 
    
         
            +
                  expect {
         
     | 
| 
      
 158 
     | 
    
         
            +
                    @channel.authenticate(':1.1')
         
     | 
| 
      
 159 
     | 
    
         
            +
                  }.to raise_error Eventflit::Error
         
     | 
| 
      
 160 
     | 
    
         
            +
             
     | 
| 
      
 161 
     | 
    
         
            +
                  expect {
         
     | 
| 
      
 162 
     | 
    
         
            +
                    @channel.authenticate('foo1.1', 'channel')
         
     | 
| 
      
 163 
     | 
    
         
            +
                  }.to raise_error Eventflit::Error
         
     | 
| 
      
 164 
     | 
    
         
            +
             
     | 
| 
      
 165 
     | 
    
         
            +
                  expect {
         
     | 
| 
      
 166 
     | 
    
         
            +
                    @channel.authenticate('foo', 'channel')
         
     | 
| 
      
 167 
     | 
    
         
            +
                  }.to raise_error Eventflit::Error
         
     | 
| 
      
 168 
     | 
    
         
            +
                end
         
     | 
| 
      
 169 
     | 
    
         
            +
              end
         
     | 
| 
      
 170 
     | 
    
         
            +
            end
         
     |