signalwire 1.4.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +55 -0
  3. data/.rubocop.yml +14 -2
  4. data/AUTHORS.md +1 -0
  5. data/CHANGELOG.md +30 -5
  6. data/Gemfile +4 -18
  7. data/LICENSE +21 -0
  8. data/README.md +22 -82
  9. data/Rakefile +0 -17
  10. data/examples/relay/inbound_consumer.rb +26 -0
  11. data/examples/relay/inbound_dial.rb +28 -0
  12. data/examples/relay/outbound_collect.rb +27 -0
  13. data/examples/relay/outbound_consumer.rb +22 -0
  14. data/examples/relay/outbound_record.rb +25 -0
  15. data/lib/signalwire.rb +13 -0
  16. data/lib/signalwire/blade.rb +12 -0
  17. data/lib/signalwire/blade/connection.rb +200 -0
  18. data/lib/signalwire/blade/event_handler.rb +15 -0
  19. data/lib/signalwire/blade/message.rb +38 -0
  20. data/lib/signalwire/blade/message/connect.rb +18 -0
  21. data/lib/signalwire/blade/message/execute.rb +16 -0
  22. data/lib/signalwire/blade/message/subscribe.rb +15 -0
  23. data/lib/signalwire/common.rb +6 -0
  24. data/lib/signalwire/logger.rb +27 -0
  25. data/lib/signalwire/relay.rb +40 -0
  26. data/lib/signalwire/relay/calling.rb +82 -0
  27. data/lib/signalwire/relay/calling/action.rb +16 -0
  28. data/lib/signalwire/relay/calling/action/connect_action.rb +11 -0
  29. data/lib/signalwire/relay/calling/action/play_action.rb +15 -0
  30. data/lib/signalwire/relay/calling/action/prompt_action.rb +15 -0
  31. data/lib/signalwire/relay/calling/action/record_action.rb +15 -0
  32. data/lib/signalwire/relay/calling/call.rb +210 -0
  33. data/lib/signalwire/relay/calling/call_convenience_methods.rb +79 -0
  34. data/lib/signalwire/relay/calling/component.rb +115 -0
  35. data/lib/signalwire/relay/calling/component/answer.rb +27 -0
  36. data/lib/signalwire/relay/calling/component/await.rb +20 -0
  37. data/lib/signalwire/relay/calling/component/connect.rb +45 -0
  38. data/lib/signalwire/relay/calling/component/dial.rb +34 -0
  39. data/lib/signalwire/relay/calling/component/hangup.rb +41 -0
  40. data/lib/signalwire/relay/calling/component/play.rb +46 -0
  41. data/lib/signalwire/relay/calling/component/prompt.rb +62 -0
  42. data/lib/signalwire/relay/calling/component/record.rb +53 -0
  43. data/lib/signalwire/relay/calling/control_component.rb +35 -0
  44. data/lib/signalwire/relay/calling/result.rb +16 -0
  45. data/lib/signalwire/relay/calling/result/answer_result.rb +6 -0
  46. data/lib/signalwire/relay/calling/result/connect_result.rb +9 -0
  47. data/lib/signalwire/relay/calling/result/dial_result.rb +7 -0
  48. data/lib/signalwire/relay/calling/result/hangup_result.rb +7 -0
  49. data/lib/signalwire/relay/calling/result/play_result.rb +6 -0
  50. data/lib/signalwire/relay/calling/result/prompt_result.rb +11 -0
  51. data/lib/signalwire/relay/calling/result/record_result.rb +7 -0
  52. data/lib/signalwire/relay/client.rb +147 -0
  53. data/lib/signalwire/relay/constants.rb +109 -0
  54. data/lib/signalwire/relay/consumer.rb +88 -0
  55. data/lib/signalwire/relay/event.rb +41 -0
  56. data/lib/signalwire/relay/request.rb +6 -0
  57. data/lib/signalwire/rest/client.rb +5 -5
  58. data/lib/signalwire/sdk/fax_response.rb +3 -3
  59. data/lib/signalwire/sdk/messaging_response.rb +0 -2
  60. data/lib/signalwire/sdk/twilio_set_fax.rb +9 -8
  61. data/lib/signalwire/sdk/twilio_set_host.rb +8 -3
  62. data/lib/signalwire/version.rb +5 -0
  63. data/signalwire.gemspec +36 -106
  64. metadata +173 -76
  65. data/LICENSE.txt +0 -20
  66. data/VERSION +0 -1
  67. data/spec/signalwire/rest/client_spec.rb +0 -30
  68. data/spec/signalwire/rest/integration_spec.rb +0 -102
  69. data/spec/signalwire/sdk/configuration_spec.rb +0 -28
  70. data/spec/signalwire/sdk/fax_response_spec.rb +0 -26
  71. data/spec/signalwire/sdk/messaging_response_spec.rb +0 -18
  72. data/spec/signalwire/sdk/voice_response_spec.rb +0 -20
  73. data/spec/signalwire/sdk_spec.rb +0 -27
  74. data/spec/spec_helper.rb +0 -119
  75. data/spec/vcr_cassettes/accounts.yml +0 -27
  76. data/spec/vcr_cassettes/applications.yml +0 -29
  77. data/spec/vcr_cassettes/get_fax.yml +0 -25
  78. data/spec/vcr_cassettes/get_fax_media_instance.yml +0 -27
  79. data/spec/vcr_cassettes/get_fax_media_list.yml +0 -47
  80. data/spec/vcr_cassettes/list_faxes.yml +0 -25
  81. data/spec/vcr_cassettes/local_numbers.yml +0 -26
  82. data/spec/vcr_cassettes/recordings.yml +0 -27
  83. data/spec/vcr_cassettes/send_fax.yml +0 -27
  84. data/spec/vcr_cassettes/toll_free_numbers.yml +0 -34
  85. data/spec/vcr_cassettes/transcriptions.yml +0 -28
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib')
4
+ %w[
5
+ bundler/setup
6
+ signalwire
7
+ ].each { |f| require f }
8
+
9
+ # Set logging to debug for testing
10
+ Signalwire::Logger.logger.level = ::Logger::DEBUG
11
+
12
+ class OutboundConsumer < Signalwire::Relay::Consumer
13
+ def ready
14
+ logger.info 'Dialing out'
15
+ call = client.calling.new_call(from: ENV['FROM_NUMBER'], to: ENV['TO_NUMBER'])
16
+ call.dial
17
+ call.play_tts 'please leave your message after the beep. Press pound when done.'
18
+ result = call.record({"audio": { "beep": "true", "terminators": "#"}})
19
+ call.play_tts 'you said:'
20
+ call.play_audio result.url
21
+ call.hangup
22
+ end
23
+ end
24
+
25
+ OutboundConsumer.new.run
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Signalwire
4
+ end
5
+
6
+ require 'signalwire/sdk'
7
+ require 'signalwire/logger'
8
+ require 'signalwire/common'
9
+
10
+ require 'signalwire/blade'
11
+ require 'signalwire/relay'
12
+
13
+
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Signalwire::Blade
4
+ RECONNECT_PERIOD = 5
5
+ end
6
+
7
+ require 'signalwire/blade/event_handler'
8
+ require 'signalwire/blade/connection'
9
+ require 'signalwire/blade/message'
10
+ require 'signalwire/blade/message/connect'
11
+ require 'signalwire/blade/message/execute'
12
+ require 'signalwire/blade/message/subscribe'
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'has_guarded_handlers'
4
+ require 'eventmachine'
5
+ require 'faye/websocket'
6
+ require 'json'
7
+
8
+ module Signalwire::Blade
9
+ class Connection
10
+ include Signalwire::Logger
11
+ include Signalwire::Blade::EventHandler
12
+ include Signalwire::Common
13
+
14
+ attr_reader :session_id, :connected, :node_id, :connection
15
+
16
+ def initialize(**options)
17
+ @options = options
18
+ @session_id = nil
19
+ @node_id = nil
20
+ @connected = false
21
+ @url = @options.fetch(:url, 'wss://relay.signalwire.com')
22
+ @log_traffic = options.fetch(:log_traffic, true)
23
+ @authentication = options.fetch(:authentication, nil)
24
+
25
+ @inbound_queue = EM::Queue.new
26
+ @outbound_queue = EM::Queue.new
27
+
28
+ @counter = 1
29
+ end
30
+
31
+ def connect!
32
+ setup_started_event
33
+ enable_epoll
34
+ handle_signals
35
+
36
+ main_loop!
37
+ end
38
+
39
+ def reconnect!
40
+ return if @shutdown
41
+ sleep Signalwire::Blade::RECONNECT_PERIOD
42
+ @connected = false
43
+ logger.info "Attempting reconnection"
44
+ main_loop!
45
+ end
46
+
47
+ def main_loop!
48
+ EM.run do
49
+ @ws = Faye::WebSocket::Client.new(@url)
50
+
51
+ @ws.on(:open) { |event| broadcast :started, event }
52
+ @ws.on(:message) { |event| enqueue_inbound event }
53
+ @ws.on(:close) { handle_close }
54
+
55
+ @ws.on :error do |error|
56
+ logger.error "Error occurred: #{error.message}"
57
+ end
58
+
59
+ EM.next_tick { flush_queues }
60
+ end
61
+ end
62
+
63
+ def setup_started_event
64
+ on :started do |_event|
65
+ @connected = true
66
+ myreq = connect_request
67
+ start_periodic_timer
68
+
69
+ write_command(myreq) do |event|
70
+ @session_id = event.dig(:result, :sessionid) unless @session_id
71
+ @node_id = event.dig(:result, :nodeid) unless @node_d
72
+ logger.info "Blade Session connected with id: #{@session_id}"
73
+ broadcast :connected, event
74
+ end
75
+
76
+ rescue StandardError => e
77
+ logger.error e.inspect
78
+ logger.error e.backtrace
79
+ end
80
+ end
81
+
82
+ def enable_epoll
83
+ # This is only enabled on Linux
84
+ EM.epoll
85
+ logger.debug "Running with epoll #{EM.epoll?}"
86
+ end
87
+
88
+ def transmit(message)
89
+ enqueue_outbound message
90
+ end
91
+
92
+ def write(message)
93
+ log_traffic :send, message
94
+ @ws.send(message)
95
+ end
96
+
97
+ def receive(message)
98
+ event = Message.from_json(message.data)
99
+ log_traffic :recv, event.payload
100
+ EM.defer do
101
+ broadcast :message, event
102
+ end
103
+ end
104
+
105
+ def write_command(command, &block)
106
+ once(:message, id: command.id, &block) if block_given?
107
+ transmit(command.build_request.to_json)
108
+ end
109
+
110
+ def execute(params, &block)
111
+ block_given? ? write_command(Execute.new(params), &block) : write_command(Execute.new(params))
112
+ end
113
+
114
+ def subscribe(params, &block)
115
+ block_given? ? write_command(Subscribe.new(params), &block) : write_command(Subscribe.new(params))
116
+ end
117
+
118
+ def handle_close
119
+ reconnect!
120
+ end
121
+
122
+ def disconnect!
123
+ # logger.info 'Stopping Blade event loop'
124
+ @ws = nil
125
+ @connected = false
126
+ EM.stop
127
+ end
128
+
129
+ def flush_queues
130
+ @inbound_queue.pop { |inbound| receive(inbound) } until @inbound_queue.empty?
131
+ if connected?
132
+ @outbound_queue.pop { |outbound| write(outbound) } until @outbound_queue.empty?
133
+ end
134
+
135
+ EM.next_tick { flush_queues }
136
+ end
137
+
138
+ def enqueue_inbound(message)
139
+ @inbound_queue.push message
140
+ end
141
+
142
+ def enqueue_outbound(message)
143
+ @outbound_queue.push message
144
+ end
145
+
146
+ def connect_request
147
+ req = Connect.new
148
+ req[:params][:authentication] = @authentication if @authentication
149
+ req
150
+ end
151
+
152
+ def connected?
153
+ @connected == true
154
+ end
155
+
156
+ def start_periodic_timer
157
+ pinger = EventMachine::PeriodicTimer.new(Signalwire::Relay::PING_TIMEOUT) do
158
+ timeouter = EventMachine::Timer.new(2) do
159
+ # reconnect logic goes here
160
+ logger.error "We got disconnected!"
161
+ pinger.cancel
162
+ reconnect!
163
+ end
164
+
165
+ @ws.ping 'detecting presence' do
166
+ timeouter.cancel
167
+ end
168
+ end
169
+ end
170
+
171
+ def log_traffic(direction, message)
172
+ if @log_traffic
173
+ pretty = case direction
174
+ when :send
175
+ JSON.pretty_generate(JSON.parse(message))
176
+ when :recv
177
+ JSON.pretty_generate(message)
178
+ end
179
+ end
180
+ logger.debug "#{direction.to_s.upcase}: #{pretty}"
181
+ end
182
+
183
+ def handle_signals
184
+ Signal.trap('INT') do
185
+ shutdown_from_signal
186
+ end
187
+
188
+ Signal.trap('TERM') do
189
+ shutdown_from_signal
190
+ end
191
+ end
192
+
193
+
194
+ def shutdown_from_signal
195
+ @shutdown = true
196
+ disconnect!
197
+ exit
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'has_guarded_handlers'
4
+
5
+ module Signalwire::Blade
6
+ module EventHandler
7
+ include HasGuardedHandlers
8
+ alias on register_handler
9
+ alias once register_tmp_handler
10
+
11
+ def broadcast(event_type, event)
12
+ trigger_handler event_type, event, broadcast: true
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Signalwire::Blade
6
+ class Message
7
+ extend Forwardable
8
+ def_delegators :@payload, :[], :[]=, :dig
9
+
10
+ def initialize(params = {})
11
+ @payload = params
12
+ @id = params[:id]
13
+ end
14
+
15
+ def id
16
+ @id ||= SecureRandom.uuid
17
+ end
18
+
19
+ def payload
20
+ @payload ||= {}
21
+ end
22
+
23
+ def build_request
24
+ payload.merge(
25
+ jsonrpc: '2.0',
26
+ id: id
27
+ )
28
+ end
29
+
30
+ def self.from_json(json_hash)
31
+ new JSON.parse(json_hash, symbolize_names: true)
32
+ end
33
+
34
+ def to_s
35
+ inspect
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Signalwire::Blade
4
+ class Connect < Message
5
+ def initialize
6
+ @payload = {
7
+ method: 'blade.connect',
8
+ params: {
9
+ version: {
10
+ major: 2,
11
+ minor: 1,
12
+ revision: 0
13
+ }
14
+ }
15
+ }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Signalwire::Blade
4
+ class Execute < Message
5
+ # Creates an Execute message
6
+ #
7
+ # @param params [Hash] The "params" portion of the execute
8
+ def initialize(params = {})
9
+ @payload =
10
+ {
11
+ method: 'blade.execute',
12
+ params: params
13
+ }
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Signalwire::Blade
4
+ class Subscribe < Message
5
+ # Creates a Subscribe message
6
+ #
7
+ # @param params [Hash] The "params" portion of the message
8
+ def initialize(params = {})
9
+ @payload = {
10
+ method: 'blade.subscription',
11
+ params: params
12
+ }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Signalwire
4
+ module Common
5
+ end
6
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module Signalwire
6
+ module Logger
7
+ class << self
8
+ # A global logger object
9
+ # @return [Logger] a Logger instance
10
+ def logger
11
+ @logger ||= begin
12
+ logger = ::Logger.new(STDERR, progname: 'SignalWire', level: ::Logger::DEBUG)
13
+ logger.level = ENV.fetch('SIGNALWIRE_LOG_LEVEL', ::Logger::WARN)
14
+ logger
15
+ end
16
+ end
17
+ end
18
+
19
+ def logger
20
+ Signalwire::Logger.logger
21
+ end
22
+
23
+ def level=(level)
24
+ Signalwire::Logger.logger.level = level
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Signalwire::Relay
4
+ end
5
+
6
+ require 'signalwire/relay/constants'
7
+ require 'signalwire/relay/client'
8
+ require 'signalwire/relay/request'
9
+ require 'signalwire/relay/event'
10
+ require 'signalwire/relay/consumer'
11
+
12
+ require 'signalwire/relay/calling'
13
+ require 'signalwire/relay/calling/call_convenience_methods'
14
+ require 'signalwire/relay/calling/call'
15
+
16
+ require 'signalwire/relay/calling/action'
17
+ require 'signalwire/relay/calling/action/connect_action'
18
+ require 'signalwire/relay/calling/action/play_action'
19
+ require 'signalwire/relay/calling/action/prompt_action'
20
+ require 'signalwire/relay/calling/action/record_action'
21
+
22
+ require 'signalwire/relay/calling/result'
23
+ require 'signalwire/relay/calling/result/answer_result'
24
+ require 'signalwire/relay/calling/result/connect_result'
25
+ require 'signalwire/relay/calling/result/dial_result'
26
+ require 'signalwire/relay/calling/result/hangup_result'
27
+ require 'signalwire/relay/calling/result/play_result'
28
+ require 'signalwire/relay/calling/result/prompt_result'
29
+ require 'signalwire/relay/calling/result/record_result'
30
+
31
+ require 'signalwire/relay/calling/component'
32
+ require 'signalwire/relay/calling/control_component'
33
+ require 'signalwire/relay/calling/component/answer'
34
+ require 'signalwire/relay/calling/component/connect'
35
+ require 'signalwire/relay/calling/component/dial'
36
+ require 'signalwire/relay/calling/component/hangup'
37
+ require 'signalwire/relay/calling/component/play'
38
+ require 'signalwire/relay/calling/component/prompt'
39
+ require 'signalwire/relay/calling/component/record'
40
+ require 'signalwire/relay/calling/component/await'
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'concurrent-ruby'
5
+
6
+ module Signalwire::Relay
7
+ module Calling
8
+ class Instance
9
+ extend Forwardable
10
+ include Signalwire::Logger
11
+ include Signalwire::Common
12
+
13
+ def_delegators :@client, :relay_execute, :protocol, :on, :once, :broadcast
14
+
15
+ def initialize(client)
16
+ @client = client
17
+ end
18
+
19
+ def calls
20
+ @calls ||= Concurrent::Array.new
21
+ end
22
+
23
+ def contexts
24
+ @contexts ||= Concurrent::Array.new
25
+ end
26
+
27
+ def receive(context:, &block)
28
+ @client.on :event, event_type: 'calling.call.receive' do |event|
29
+ logger.info "Starting up call for #{event.call_params}"
30
+ call_obj = Signalwire::Relay::Calling::Call.from_event(self, event)
31
+ calls << call_obj
32
+ block.call(call_obj) if block_given?
33
+ end
34
+
35
+ receive_command = {
36
+ protocol: protocol,
37
+ method: 'call.receive',
38
+ params: {
39
+ context: context
40
+ }
41
+ }
42
+
43
+ relay_execute receive_command do
44
+ contexts << context
45
+ end
46
+ end
47
+
48
+ def find_call_by_id(call_id)
49
+ calls.find { |call| call.id == call_id }
50
+ end
51
+
52
+ def find_call_by_tag(tag)
53
+ calls.find { |call| call.tag == tag }
54
+ end
55
+
56
+ def end_call(call_id)
57
+ calls.delete find_call_by_id(call_id)
58
+ end
59
+
60
+ def new_call(from:, to:, device_type: 'phone', timeout: 30)
61
+ params = {
62
+ device: {
63
+ type: device_type,
64
+ params: {
65
+ from_number: from,
66
+ to_number: to,
67
+ timeout: timeout
68
+ }
69
+ }
70
+ }
71
+ call = Call.new(self, params)
72
+ calls << call
73
+ call
74
+ end
75
+
76
+ def dial(from:, to:, device_type: 'phone', timeout: 30)
77
+ handle = new_call(from: from, to: to, device_type: device_type, timeout: timeout)
78
+ handle.dial
79
+ end
80
+ end
81
+ end
82
+ end