phoenix_rails 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7709aaa6258a7487078052fdbe24809e9890152f
4
+ data.tar.gz: 9b210022272c81b6f95dc80d6bd1e26a754dcc0d
5
+ SHA512:
6
+ metadata.gz: a272113710ee384f2b5b478ce85b2a5994ac36b9f39bf808ba052630c7aa4ea3c5e10a943a430173a4a9c6c03b191144d3d79fdc4cf07093d32f24772d88dd2e
7
+ data.tar.gz: b05cc1b45a2afee0b327925bba1b9191383c46f053ed5983089f951ad675d3321436464b7dc3e9f02cd958a20b6462e4d24c1567a9f7e573afe535d7166ef49f
@@ -0,0 +1,115 @@
1
+ require 'openssl'
2
+ require 'multi_json'
3
+
4
+ module PhoenixRails
5
+ # Trigger events on Channels
6
+ class Channel
7
+ attr_reader :name
8
+ INVALID_CHANNEL_REGEX = /[^A-Za-z0-9_\-=@,.;]/
9
+ def initialize(base_url, name, client = PhoenixRails)
10
+ @uri = base_url.dup
11
+ if PhoenixRails::Channel::INVALID_CHANNEL_REGEX.match(name)
12
+ raise PhoenixRails::Error, "Illegal channel name '#{name}'"
13
+ end
14
+ @uri.path = @uri.path + "/channels/#{name}/"
15
+ @name = name
16
+ @client = client
17
+ end
18
+
19
+ # Trigger event
20
+ def trigger!(event_name, data, socket_id = nil)
21
+ params = {}
22
+ if socket_id
23
+ validate_socket_id(socket_id)
24
+ params[:socket_id] = socket_id
25
+ end
26
+ @client.trigger(name, event_name, data, params)
27
+ end
28
+
29
+ # Trigger event, catching and logging any errors.
30
+ def trigger(event_name, data, socket_id = nil)
31
+ trigger!(event_name, data, socket_id)
32
+ rescue PhoenixRails::Error => e
33
+ PhoenixRails.logger.error("#{e.message} (#{e.class})")
34
+ PhoenixRails.logger.debug(e.backtrace.join("\n"))
35
+ end
36
+
37
+ # Request users for a presence channel
38
+ # Only works on presence channels (see: http://PhoenixRails.com/docs/client_api_guide/client_presence_channels and https://PhoenixRails.com/docs/rest_api)
39
+ #
40
+ # @example Response
41
+ # [{"id"=>"4"}]
42
+ #
43
+ # @return [Hash] Array of user hashes for this channel
44
+ # @raise [PhoenixRails::Error] on invalid PhoenixRails response - see the error message for more details
45
+ # @raise [PhoenixRails::HTTPError] on any error raised inside Net::HTTP - the original error is available in the original_error attribute
46
+ #
47
+ def users
48
+ @client.get("/channels/#{name}/users")[:users]
49
+ end
50
+
51
+ # Compute authentication string required as part of the authentication
52
+ # endpoint response. Generally the authenticate method should be used in
53
+ # preference to this one
54
+ #
55
+ # @param socket_id [String] Each PhoenixRails socket connection receives a
56
+ # unique socket_id. This is sent from phoenix.js to your server when
57
+ # channel authentication is required.
58
+ # @param custom_string [String] Allows signing additional data
59
+ # @return [String]
60
+ #
61
+ def authentication_string(socket_id, custom_string = nil)
62
+ validate_socket_id(socket_id)
63
+
64
+ unless custom_string.nil? || custom_string.kind_of?(String)
65
+ raise Error, 'Custom argument must be a string'
66
+ end
67
+
68
+ string_to_sign = [socket_id, name, custom_string].
69
+ compact.map(&:to_s).join(':')
70
+ PhoenixRails.logger.debug "Signing #{string_to_sign}"
71
+ token = @client.authentication_token
72
+ digest = OpenSSL::Digest::SHA256.new
73
+ signature = OpenSSL::HMAC.hexdigest(digest, token.secret, string_to_sign)
74
+
75
+ return "#{token.key}:#{signature}"
76
+ end
77
+
78
+ # Generate the expected response for an authentication endpoint.
79
+ #
80
+ # @example Private channels
81
+ # render :json => PhoenixRails['private-my_channel'].authenticate(params[:socket_id])
82
+ #
83
+ # @example Presence channels
84
+ # render :json => PhoenixRails['private-my_channel'].authenticate(params[:socket_id], {
85
+ # :user_id => current_user.id, # => required
86
+ # :user_info => { # => optional - for example
87
+ # :name => current_user.name,
88
+ # :email => current_user.email
89
+ # }
90
+ # })
91
+ #
92
+ # @param socket_id [String]
93
+ # @param custom_data [Hash] used for example by private channels
94
+ #
95
+ # @return [Hash]
96
+ #
97
+ # @private Custom data is sent to server as JSON-encoded string
98
+ #
99
+ def authenticate(socket_id, custom_data = nil)
100
+ custom_data = MultiJson.encode(custom_data) if custom_data
101
+ auth = authentication_string(socket_id, custom_data)
102
+ r = {:auth => auth}
103
+ r[:channel_data] = custom_data if custom_data
104
+ r
105
+ end
106
+
107
+ private
108
+
109
+ def validate_socket_id(socket_id)
110
+ unless socket_id && /\A\d+\.\d+\z/.match(socket_id)
111
+ raise PhoenixRails::Error, "Invalid socket ID #{socket_id.inspect}"
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,124 @@
1
+ ire 'signature'
2
+
3
+ module PhoenixRails
4
+ class Client
5
+ attr_accessor :scheme, :host, :port, :app_id, :key, :secret
6
+ attr_writer :connect_timeout, :send_timeout, :receive_timeout, :keep_alive_timeout
7
+
8
+
9
+ def initialize(options = {})
10
+ options = {
11
+ :scheme => 'http',
12
+ :host => 'api.phoenixapp.com',
13
+ :port => 80,
14
+ }.merge(options)
15
+ @scheme, @host, @port, @app_id, @key, @secret = options.values_at(
16
+ :scheme, :host, :port, :app_id, :key, :secret
17
+ )
18
+
19
+ # Default timeouts
20
+ @connect_timeout = 5
21
+ @send_timeout = 5
22
+ @receive_timeout = 5
23
+ @keep_alive_timeout = 30
24
+ end
25
+
26
+ def url=(url)
27
+ uri = URI.parse(url)
28
+ @scheme = uri.scheme
29
+ @app_id = uri.path.split('/').last
30
+ @key = uri.user
31
+ @secret = uri.password
32
+ @host = uri.host
33
+ @port = uri.port
34
+ end
35
+
36
+ def encrypted=(boolean)
37
+ @scheme = boolean ? 'https' : 'http'
38
+ # Configure port if it hasn't already been configured
39
+ @port = boolean ? 443 : 80
40
+ end
41
+
42
+ def encrypted?
43
+ @scheme == 'https'
44
+ end
45
+
46
+ def timeout=(value)
47
+ @connect_timeout, @send_timeout, @receive_timeout = value, value, value
48
+ end
49
+
50
+ ## INTERACE WITH THE API ##
51
+
52
+ def resource(path)
53
+ Resource.new(self, path)
54
+ end
55
+
56
+ def get(path, params = {})
57
+ Resource.new(self, path).get(params)
58
+ end
59
+
60
+ def post(path, params = {})
61
+ Resource.new(self, path).post(params)
62
+ end
63
+
64
+ def channel(channel_name)
65
+ raise ConfigurationError, 'Missing client configuration: please check that key, secret and app_id are configured.' unless configured?
66
+ Channel.new(url, channel_name, self)
67
+ end
68
+
69
+ alias :[] :channel
70
+
71
+ def channels(params = {})
72
+ get('/channels', params)
73
+ end
74
+
75
+ def trigger(channels, event_name, data, params = {})
76
+ post('/events', trigger_params(channels, event_name, data, params))
77
+ end
78
+
79
+ # @private Construct a net/http http client
80
+ def http_client
81
+ @client ||= begin
82
+ require 'httpclient'
83
+
84
+ HTTPClient.new.tap do |c|
85
+ c.connect_timeout = @connect_timeout
86
+ c.send_timeout = @send_timeout
87
+ c.receive_timeout = @receive_timeout
88
+ c.keep_alive_timeout = @keep_alive_timeout
89
+ end
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def trigger_params(channels, event_name, data, params)
96
+ channels = Array(channels).map(&:to_s)
97
+ raise PhoenixRails::Error, "Too many channels (#{channels.length}), max 10" if channels.length > 10
98
+
99
+ case data
100
+ when String
101
+ encoded_data = data
102
+ else
103
+ begin
104
+ encoded_data = MultiJson.encode(data)
105
+ rescue MultiJson::DecodeError => e
106
+ PhoenixRails.logger.error("Could not convert #{data.inspect} into JSON")
107
+ raise e
108
+ end
109
+ end
110
+
111
+ params.merge({
112
+ :name => event_name,
113
+ :channels => channels,
114
+ :data => encoded_data
115
+ })
116
+ end
117
+
118
+ def configured?
119
+ host && scheme && key && secret && app_id
120
+ end
121
+
122
+ end
123
+ end
124
+
@@ -0,0 +1,65 @@
1
+ require 'signature'
2
+ require 'digest/md5'
3
+ require 'multi_json'
4
+
5
+ module PhoenixRails
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
+
13
+ @body = body
14
+ if body
15
+ params[:body_md5] = Digest::MD5.hexdigest(body)
16
+ @head['Content-Type'] = 'application/json'
17
+ end
18
+
19
+ request = Signature::Request.new(verb.to_s.upcase, uri.path, params)
20
+ request.sign(client.authentication_token)
21
+ @params = request.signed_params
22
+ end
23
+
24
+ def send
25
+ http = @client.http_client
26
+
27
+ begin
28
+ response = http.request(@verb, @uri, @params, @body, @head)
29
+ rescue HTTPClient::BadResponseError, HTTPClient::TimeoutError,
30
+ SocketError, Errno::ECONNREFUSED => e
31
+ error = PhoenixRails::HTTPError.new("#{e.message} (#{e.class})")
32
+ error.original_error = e
33
+ raise error
34
+ end
35
+
36
+ body = response.body ? response.body.chomp : nil
37
+
38
+ return handle_response(response.code.to_i, body)
39
+ end
40
+
41
+ private
42
+
43
+ def handle_response(status_code, body)
44
+ case status_code
45
+ when 200
46
+ return symbolize_first_level(MultiJson.decode(body))
47
+ when 400
48
+ raise Error, "Bad request: #{body}"
49
+ when 401
50
+ raise AuthenticationError, body
51
+ when 404
52
+ raise Error, "404 Not found (#{@uri.path})"
53
+ else
54
+ raise Error, "Unknown error (status code #{status_code}): #{body}"
55
+ end
56
+ end
57
+
58
+ def symbolize_first_level(hash)
59
+ hash.inject({}) do |result, (key, value)|
60
+ result[key.to_sym] = value
61
+ result
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,27 @@
1
+ module PhoenixRails
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
10
+ end
11
+
12
+ def post(params)
13
+ body = MultiJson.encode(params)
14
+ create_request(:post, {}, body).send
15
+ end
16
+
17
+ private
18
+
19
+ def create_request(verb, params, body = nil)
20
+ Request.new(@client, verb, url, params, body)
21
+ end
22
+
23
+ def url
24
+ @_url ||= @client.url(@path)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,41 @@
1
+ class PhoenixRails
2
+ class Error < RuntimeError; end
3
+ class AuthenticationError < Error; end
4
+ class ConfigurationError < Error; end
5
+ class HTTPError < Error; attr_accessor :original_error; end
6
+
7
+ class << self
8
+ extend Forwardable
9
+
10
+ def_delegators :default_client, :scheme, :host, :port, :app_id, :key, :secret
11
+ def_delegators :default_client, :scheme=, :host=, :port=, :app_id=, :key=, :secret=
12
+
13
+ def_delegators :default_client, :authentication_token, :url
14
+ def_delegators :default_client, :encrypted=, :url=
15
+ def_delegators :default_client, :timeout=, :connect_timeout=, :send_timeout=, :receive_timeout=, :keep_alive_timeout=
16
+
17
+ def_delegators :default_client, :get, :post
18
+ def_delegators :default_client, :channels, :trigger
19
+ def_delegators :default_client, :channel, :[]
20
+
21
+ def logger
22
+ @logger ||= begin
23
+ log = Logger.new($stdout)
24
+ log.level = Logger::INFO
25
+ log
26
+ end
27
+ end
28
+
29
+ def default_client
30
+ @default_client ||= PhoenixRails::Client.new
31
+ end
32
+ end
33
+
34
+ if ENV['PHOENIX_RAILS_URL']
35
+ self.url = ENV['PHOENIX_RAILS_URL']
36
+ end
37
+ end
38
+
39
+ require 'pusher/channel'
40
+ require 'pusher/request'
41
+ require 'pusher/resource'
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: phoenix_rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Nguyen Le
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-04-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: httpclient
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.7'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: multi_json
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.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.0'
41
+ description: Gem for pushing event to a Phoenix server for realtime
42
+ email: nathanle89@gmail.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - lib/phoenix_rails.rb
48
+ - lib/phoenix_rails/channel.rb
49
+ - lib/phoenix_rails/client.rb
50
+ - lib/phoenix_rails/request.rb
51
+ - lib/phoenix_rails/resource.rb
52
+ homepage: http://rubygems.org/gems/phoenix_rails
53
+ licenses:
54
+ - MIT
55
+ metadata: {}
56
+ post_install_message:
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubyforge_project:
72
+ rubygems_version: 2.5.1
73
+ signing_key:
74
+ specification_version: 4
75
+ summary: Rails gem for Phoenix integration
76
+ test_files: []