rocket_chat-realtime 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.gitlab-ci.yml +50 -0
  4. data/.overcommit.yml +20 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +12 -0
  7. data/.travis.yml +6 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/Gemfile +23 -0
  10. data/Gemfile.lock +100 -0
  11. data/LICENSE +201 -0
  12. data/README.md +40 -0
  13. data/Rakefile +8 -0
  14. data/bin/console +12 -0
  15. data/bin/setup +8 -0
  16. data/lib/rocket_chat/realtime.rb +58 -0
  17. data/lib/rocket_chat/realtime/adapter.rb +51 -0
  18. data/lib/rocket_chat/realtime/async_task.rb +60 -0
  19. data/lib/rocket_chat/realtime/client.rb +100 -0
  20. data/lib/rocket_chat/realtime/connector.rb +79 -0
  21. data/lib/rocket_chat/realtime/dispatcher.rb +80 -0
  22. data/lib/rocket_chat/realtime/event_emitter.rb +65 -0
  23. data/lib/rocket_chat/realtime/handlers/base.rb +52 -0
  24. data/lib/rocket_chat/realtime/handlers/changed.rb +31 -0
  25. data/lib/rocket_chat/realtime/handlers/ready.rb +23 -0
  26. data/lib/rocket_chat/realtime/handlers/result.rb +34 -0
  27. data/lib/rocket_chat/realtime/message.rb +40 -0
  28. data/lib/rocket_chat/realtime/messages/changed.rb +44 -0
  29. data/lib/rocket_chat/realtime/messages/method.rb +37 -0
  30. data/lib/rocket_chat/realtime/messages/result.rb +40 -0
  31. data/lib/rocket_chat/realtime/messages/subscribe.rb +37 -0
  32. data/lib/rocket_chat/realtime/methods/auth.rb +33 -0
  33. data/lib/rocket_chat/realtime/methods/message.rb +33 -0
  34. data/lib/rocket_chat/realtime/reactor.rb +123 -0
  35. data/lib/rocket_chat/realtime/subscriptions/room.rb +30 -0
  36. data/lib/rocket_chat/realtime/version.rb +7 -0
  37. data/rocket_chat-realtime.gemspec +31 -0
  38. metadata +122 -0
@@ -0,0 +1,40 @@
1
+ # RocketChat::Realtime
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/rocket_chat/realtime`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'rocket_chat-realtime'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle install
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install rocket_chat-realtime
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/rocket_chat-realtime. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/rocket_chat-realtime/blob/master/CODE_OF_CONDUCT.md).
36
+
37
+
38
+ ## Code of Conduct
39
+
40
+ Everyone interacting in the RocketChat::Realtime project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/rocket_chat-realtime/blob/master/CODE_OF_CONDUCT.md).
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'rocket_chat/realtime'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ require 'pry'
12
+ Pry.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ require 'rocket_chat/realtime/version'
6
+ require 'rocket_chat/realtime/message'
7
+ require 'rocket_chat/realtime/reactor'
8
+ require 'rocket_chat/realtime/connector'
9
+ require 'rocket_chat/realtime/adapter'
10
+ require 'rocket_chat/realtime/dispatcher'
11
+ require 'rocket_chat/realtime/async_task'
12
+ require 'rocket_chat/realtime/event_emitter'
13
+ require 'rocket_chat/realtime/client'
14
+
15
+ module RocketChat
16
+ # RocketChat Realtiem API
17
+ #
18
+ # The Realtime API is depend on Metero.js DDP
19
+ # https://github.com/meteor/meteor/blob/devel/packages/ddp/DDP.md
20
+ #
21
+ # @since 0.1.0
22
+ module Realtime
23
+ module_function
24
+
25
+ # Logger
26
+ #
27
+ # @return [Logger]
28
+ #
29
+ # @since 0.1.0
30
+ def logger
31
+ @logger ||=
32
+ Logger.new(STDERR, progname: name, level: Logger::ERROR)
33
+ end
34
+
35
+ # Set logger
36
+ #
37
+ # @param logger [Logger]
38
+ #
39
+ # @since 0.1.0
40
+ def logger=(logger)
41
+ @logger = logger
42
+ end
43
+
44
+ # Connect to RocketChat
45
+ #
46
+ # @param options [Hash] connection options
47
+ #
48
+ # @return [RocketChat::Realtime::Client]
49
+ #
50
+ # @since 0.1.0
51
+ def connect(options = {})
52
+ client = Client.new(options)
53
+ client.connect
54
+ Reactor.run
55
+ client
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RocketChat
4
+ module Realtime
5
+ # Socket Adapter for WebSocket::Driver
6
+ #
7
+ # @since 0.1.0
8
+ class Adapter
9
+ # @since 0.1.0
10
+ attr_reader :url
11
+
12
+ # @param url [String] the server to connect
13
+ #
14
+ # @since 0.1.0
15
+ def initialize(url)
16
+ @url = url
17
+ @mutex = Mutex.new
18
+ @buffer = ''
19
+ end
20
+
21
+ # Enqueue data to send to server
22
+ #
23
+ # @param data [String]
24
+ #
25
+ # @since 0.1.0
26
+ def write(data)
27
+ @mutex.synchronize { @buffer = @buffer.dup.concat(data) }
28
+ end
29
+
30
+ # Pump Buffer
31
+ #
32
+ # @param io [IO] the data write to
33
+ #
34
+ # @return [Number] total bytes written
35
+ #
36
+ # @since 0.1.0
37
+ def pump_buffer(io)
38
+ @mutex.synchronize do
39
+ written = 0
40
+ begin
41
+ written = io.write_nonblock @buffer unless @buffer.empty?
42
+ @buffer = @buffer.byteslice(written..-1) if written.positive?
43
+ rescue IO::WaitWritable, IO::WaitReadable
44
+ return written
45
+ end
46
+ written
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+ require 'forwardable'
5
+
6
+ require 'concurrent'
7
+
8
+ module RocketChat
9
+ module Realtime
10
+ # AsyncTask resolver
11
+ #
12
+ # @since 0.1.0
13
+ class AsyncTask
14
+ class << self
15
+ extend Forwardable
16
+
17
+ delegate %w[start resolve] => :instance
18
+ end
19
+
20
+ include Singleton
21
+ include Concurrent::Promises::FactoryMethods
22
+
23
+ # @since 0.1.0
24
+ TASK_TIMEOUT = 60
25
+
26
+ # @since 0.1.0
27
+ def initialize
28
+ @tasks = Concurrent::Map.new
29
+ end
30
+
31
+ # Register a new task
32
+ #
33
+ # @param id [String] task id
34
+ # @param block [Proc] async taks to execute
35
+ #
36
+ # @return [Concurrent::Promises::ResolvableFuture]
37
+ #
38
+ # @since 0.1.0
39
+ def start(id)
40
+ # TODO: check for atomic
41
+ yield if block_given?
42
+ Concurrent::ScheduledTask.execute(TASK_TIMEOUT) { @tasks.delete(id)&.reject(:timeout) }
43
+ @tasks.fetch_or_store(id, resolvable_future)
44
+ end
45
+
46
+ # Resolve task
47
+ #
48
+ # @param result [RocketChat::Realtime::ReslutMessage]
49
+ #
50
+ # @return [Concurrent::Promises::ResolvableFuture]
51
+ #
52
+ # @since 0.1.0
53
+ def resolve(id, result = nil)
54
+ task = @tasks.delete(id)
55
+ task&.resolve true, result
56
+ task
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'json'
5
+
6
+ require 'websocket/driver'
7
+
8
+ require 'rocket_chat/realtime/methods/auth'
9
+ require 'rocket_chat/realtime/methods/message'
10
+ require 'rocket_chat/realtime/subscriptions/room'
11
+
12
+ module RocketChat
13
+ module Realtime
14
+ # Rocket.Chat Reamtime API
15
+ #
16
+ # @since 0.1.0
17
+ class Client
18
+ extend Forwardable
19
+
20
+ include EventEmitter
21
+ include Methods::Auth
22
+ include Methods::Message
23
+ include Subscriptions::Room
24
+
25
+ # @since 0.1.0
26
+ INITIALIZE_COMMAND = {
27
+ msg: :connect,
28
+ version: '1',
29
+ support: ['1']
30
+ }.freeze
31
+
32
+ # @since 0.1.0
33
+ delegate %w[ping pong] => :driver
34
+
35
+ # @since 0.1.0
36
+ attr_reader :server, :connector, :adapter, :driver
37
+
38
+ # @param options [Hash]
39
+ #
40
+ # @since 0.1.0
41
+ def initialize(options = {})
42
+ @server = options[:server]
43
+ @connector = Connector.new(endpoint)
44
+ @adapter = Adapter.new(endpoint)
45
+ @driver = WebSocket::Driver.client(adapter)
46
+ @dispatcher = Dispatcher.new(self)
47
+ end
48
+
49
+ # @return [String] the realtime api endpoint
50
+ #
51
+ # @since 0.1.0
52
+ def endpoint
53
+ "#{server}/websocket"
54
+ end
55
+
56
+ # Connect to server
57
+ #
58
+ # @since 0.1.0
59
+ def connect
60
+ driver.start
61
+ driver.text(INITIALIZE_COMMAND.to_json)
62
+ Reactor.register(self)
63
+ end
64
+
65
+ # Close connection to server
66
+ #
67
+ # @since 0.1.0
68
+ def disconnect
69
+ @dispatcher.dispose
70
+ driver.close
71
+ Reactor.deregister(self)
72
+ end
73
+
74
+ # WebSocket is opened
75
+ #
76
+ # @return [Boolean] open or not
77
+ #
78
+ # @since 0.1.0
79
+ def opened?
80
+ driver.state == :open
81
+ end
82
+
83
+ # Process I/O
84
+ #
85
+ # @param monitor [NIO::Monitor]
86
+ #
87
+ # @since 0.1.0
88
+ def process(monitor)
89
+ driver.parse(monitor.io.read_nonblock(2**14)) if monitor.readable?
90
+ adapter.pump_buffer(monitor.io) if monitor.writeable?
91
+ rescue IO::WaitReadable, IO::WaitWritable
92
+ # nope
93
+ rescue Errno::ECONNRESET, EOFError, Errno::ECONNABORTED
94
+ RocketChat::Realtime.logger.warn('Remote server is closed.')
95
+ monitor.close
96
+ disconnect
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'openssl'
5
+ require 'forwardable'
6
+
7
+ module RocketChat
8
+ module Realtime
9
+ # Socket manager
10
+ #
11
+ # @since 0.1.0
12
+ class Connector
13
+ extend Forwardable
14
+
15
+ delegate %w[hostname] => :uri
16
+
17
+ # @since 0.1.0
18
+ attr_reader :uri
19
+
20
+ # @param url [String] the websocket server to connect
21
+ #
22
+ # @since 0.1.0
23
+ def initialize(url)
24
+ @uri = URI(url)
25
+ end
26
+
27
+ # Connect to server
28
+ #
29
+ # @return [Socket] the socket
30
+ def connect
31
+ return @socket if @socket
32
+ return raw_socket unless ssl?
33
+
34
+ ssl_socket
35
+ end
36
+
37
+ alias socket connect
38
+
39
+ # Check the SSL enabled
40
+ #
41
+ # @return [Boolean] is enabled
42
+ #
43
+ # @since 0.1.0
44
+ def ssl?
45
+ @uri.scheme == 'wss'
46
+ end
47
+
48
+ # The port to connect
49
+ #
50
+ # @return [Number] the port
51
+ #
52
+ # @since 0.1.0
53
+ def port
54
+ @uri.port || (ssl? ? 443 : 80)
55
+ end
56
+
57
+ protected
58
+
59
+ # @return [Socket] the raw tcp socket
60
+ def raw_socket
61
+ @socket = TCPSocket.new hostname, port
62
+ end
63
+
64
+ # @return [OpenSSL::SSL::SSLSocket] the ssl socket
65
+ def ssl_socket
66
+ store = OpenSSL::X509::Store.new
67
+ store.set_default_paths
68
+
69
+ ctx = OpenSSL::SSL::SSLContext.new
70
+ ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
71
+ ctx.cert_store = store
72
+
73
+ @socket = OpenSSL::SSL::SSLSocket.new(raw_socket, ctx)
74
+ @socket.hostname = @uri.hostname
75
+ @socket.connect
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'forwardable'
5
+
6
+ # RPC
7
+ require 'rocket_chat/realtime/handlers/result'
8
+
9
+ # Subscription
10
+ require 'rocket_chat/realtime/handlers/ready'
11
+ require 'rocket_chat/realtime/handlers/changed'
12
+
13
+ module RocketChat
14
+ module Realtime
15
+ # Message Dispatcher
16
+ #
17
+ # @since 0.1.0
18
+ class Dispatcher
19
+ extend Forwardable
20
+
21
+ # @since 0.1.0
22
+ HANDLERS = {
23
+ # Heartbeat
24
+ 'ping' => ->(dispatcher, _) { dispatcher.driver.text({ 'msg': 'pong' }.to_json) },
25
+ # RPC
26
+ 'result' => Handlers::Result,
27
+ # Subscription
28
+ 'ready' => Handlers::Ready,
29
+ 'changed' => Handlers::Changed
30
+ }.freeze
31
+
32
+ # @since 0.1.0
33
+ delegate %w[logger] => RocketChat::Realtime
34
+
35
+ # @since 0.1.0
36
+ delegate %w[driver] => :client
37
+
38
+ # @since 0.1.0
39
+ attr_reader :client
40
+
41
+ # @param driver [WebSocket::Driver::Client]
42
+ #
43
+ # @since 0.1.0
44
+ def initialize(client)
45
+ @client = client
46
+
47
+ driver.on(:message, &method(:dispatch))
48
+ end
49
+
50
+ # Dispatch messages
51
+ #
52
+ # @param event [WebSocket::Driver::MessageEvent]
53
+ #
54
+ # @return [Boolean] is dispatched
55
+ #
56
+ # @since 0.1.0
57
+ def dispatch(event)
58
+ message = JSON.parse(event.data)
59
+ handler = HANDLERS[message.fetch('msg', nil)]
60
+ if handler
61
+ handler.call(self, message)
62
+ else
63
+ logger.debug("No handler found for: #{message}")
64
+ false
65
+ end
66
+ rescue JSON::ParserError
67
+ # nope
68
+ end
69
+
70
+ # Dispose
71
+ #
72
+ # Clear references
73
+ #
74
+ # @since 0.1.0
75
+ def dispose
76
+ @client = nil
77
+ end
78
+ end
79
+ end
80
+ end