rocket_chat-realtime 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.
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