Class: RubyAsterisk::AGI::Session

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby-asterisk/agi/session.rb

Overview

Wraps a single FastAGI connection lifecycle.

Parses the AGI environment block sent by Asterisk at connection start, then provides command methods that write AGI commands and return parsed response hashes. Under an Async Fiber scheduler the socket reads/writes yield the Fiber rather than blocking a thread.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(socket, logger: Logger.new($stdout), env_timeout: 10) ⇒ Session

Returns a new instance of Session.

Parameters:

  • env_timeout (Numeric, nil) (defaults to: 10)

    seconds to wait for the initial AGI environment block before giving up (guards against slowloris-style connect-and-stall clients). Applied only to the handshake, never to command reads (which may legitimately block for minutes, e.g. STREAM FILE). Pass nil to disable. No-op on Ruby 3.1 (IO#timeout= shim).



23
24
25
26
27
28
# File 'lib/ruby-asterisk/agi/session.rb', line 23

def initialize(socket, logger: Logger.new($stdout), env_timeout: 10)
  @socket      = socket
  @logger      = logger
  @env         = {}
  @env_timeout = env_timeout
end

Instance Attribute Details

#envObject (readonly)

Returns the value of attribute env.



16
17
18
# File 'lib/ruby-asterisk/agi/session.rb', line 16

def env
  @env
end

#socketObject (readonly)

Returns the value of attribute socket.



16
17
18
# File 'lib/ruby-asterisk/agi/session.rb', line 16

def socket
  @socket
end

Instance Method Details

#answerObject

-- Existing wrappers (rewritten to use Protocol.format_command) -------



73
74
75
# File 'lib/ruby-asterisk/agi/session.rb', line 73

def answer
  execute('ANSWER')
end

#channel_status(channel = nil) ⇒ Object



147
148
149
# File 'lib/ruby-asterisk/agi/session.rb', line 147

def channel_status(channel = nil)
  channel ? execute("CHANNEL STATUS #{channel}") : execute('CHANNEL STATUS')
end

#database_del(family, key) ⇒ Object



161
162
163
# File 'lib/ruby-asterisk/agi/session.rb', line 161

def database_del(family, key)
  execute("DATABASE DEL #{family} #{key}")
end

#database_deltree(family, keytree = nil) ⇒ Object



165
166
167
# File 'lib/ruby-asterisk/agi/session.rb', line 165

def database_deltree(family, keytree = nil)
  keytree ? execute("DATABASE DELTREE #{family} #{keytree}") : execute("DATABASE DELTREE #{family}")
end

#database_get(family, key) ⇒ Object

-- AstDB commands -----------------------------------------------------



153
154
155
# File 'lib/ruby-asterisk/agi/session.rb', line 153

def database_get(family, key)
  execute("DATABASE GET #{family} #{key}")
end

#database_put(family, key, value) ⇒ Object



157
158
159
# File 'lib/ruby-asterisk/agi/session.rb', line 157

def database_put(family, key, value)
  execute("DATABASE PUT #{family} #{key} #{Protocol.quote(value)}")
end

#dial(target, timeout: 30, options: '') ⇒ Object

Originate a call leg via Dial dialplan application.



116
117
118
119
120
# File 'lib/ruby-asterisk/agi/session.rb', line 116

def dial(target, timeout: 30, options: '')
  args = [target, timeout.to_s]
  args << options unless options.to_s.empty?
  execute("EXEC Dial #{Protocol.quote(args.join(','))}")
end

#exec(application, *args) ⇒ Object



93
94
95
# File 'lib/ruby-asterisk/agi/session.rb', line 93

def exec(application, *args)
  execute("EXEC #{application} #{Protocol.quote(args.join(','))}")
end

#execute(command_string) ⇒ Hash

Send a raw AGI command string and return the parsed response Hash.

Handles Asterisk's two-line 520- syntax-error format by accumulating continuation lines until the closing 520 End of proper usage. line.

Any embedded CR/LF in command_string is stripped before sending: AGI is a line-oriented protocol, so a newline in a (possibly caller-controlled) argument or identifier would otherwise inject additional commands.

Parameters:

  • command_string (String)

Returns:

  • (Hash)

    { code: Integer, result: String|nil, data: String|nil }

Raises:



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/ruby-asterisk/agi/session.rb', line 54

def execute(command_string)
  line = command_string.to_s.delete("\r\n")
  @socket.write("#{line}\n")
  line = @socket.gets
  raise Error, 'Connection closed by Asterisk' unless line
  raise HangupError, 'Channel hung up' if line.chomp == 'HANGUP'

  response = Protocol.parse_response(line)

  if response[:code] >= 500
    response = Protocol.collect_multiline_error(line.chomp, response, @socket)
    raise Error, "AGI error (#{response[:code]}): #{response[:data]}"
  end

  response
end

#get_data(filename, timeout: 5000, max_digits: 1024) ⇒ Object



105
106
107
# File 'lib/ruby-asterisk/agi/session.rb', line 105

def get_data(filename, timeout: 5000, max_digits: 1024)
  execute("GET DATA #{Protocol.quote(filename)} #{timeout} #{max_digits}")
end

#get_variable(name) ⇒ Object



101
102
103
# File 'lib/ruby-asterisk/agi/session.rb', line 101

def get_variable(name)
  execute("GET VARIABLE #{name}")
end

#hangup(channel = nil) ⇒ Object



77
78
79
# File 'lib/ruby-asterisk/agi/session.rb', line 77

def hangup(channel = nil)
  channel ? execute("HANGUP #{channel}") : execute('HANGUP')
end

#read_envObject

Read the initial AGI environment block (agi_key: value lines until blank line).

Raises:

  • (IO::TimeoutError)

    on Ruby >= 3.2 if the peer stalls beyond env_timeout



33
34
35
36
37
38
39
# File 'lib/ruby-asterisk/agi/session.rb', line 33

def read_env
  with_env_timeout do
    @env = Protocol.parse_env_block(@socket) do |line|
      @logger.debug("AGI env: unexpected line: #{line}")
    end
  end
end

#record_file(filename, format: 'wav', escape_digits: '#', timeout_ms: -1,, offset: 0, beep: true, silence: nil) ⇒ Object

Record audio to a file.



131
132
133
134
135
136
137
# File 'lib/ruby-asterisk/agi/session.rb', line 131

def record_file(filename, format: 'wav', escape_digits: '#', timeout_ms: -1, offset: 0, beep: true, silence: nil)
  cmd = "RECORD FILE #{Protocol.quote(filename)} #{format} " \
        "#{Protocol.quote(escape_digits)} #{timeout_ms} #{offset}"
  cmd += ' BEEP' if beep
  cmd += " s=#{silence}" if silence
  execute(cmd)
end

#say_digits(digits, escape_digits = '') ⇒ Object



85
86
87
# File 'lib/ruby-asterisk/agi/session.rb', line 85

def say_digits(digits, escape_digits = '')
  execute("SAY DIGITS #{digits} #{Protocol.escape_argument(escape_digits)}")
end

#say_number(number, escape_digits = '') ⇒ Object



89
90
91
# File 'lib/ruby-asterisk/agi/session.rb', line 89

def say_number(number, escape_digits = '')
  execute("SAY NUMBER #{number} #{Protocol.escape_argument(escape_digits)}")
end

#send_image(filename) ⇒ Object



143
144
145
# File 'lib/ruby-asterisk/agi/session.rb', line 143

def send_image(filename)
  execute("SEND IMAGE #{filename}")
end

#send_text(text) ⇒ Object



139
140
141
# File 'lib/ruby-asterisk/agi/session.rb', line 139

def send_text(text)
  execute("SEND TEXT #{Protocol.quote(text)}")
end

#set_variable(name, value) ⇒ Object



97
98
99
# File 'lib/ruby-asterisk/agi/session.rb', line 97

def set_variable(name, value)
  execute("SET VARIABLE #{name} #{Protocol.quote(value)}")
end

#stream_file(filename, escape_digits = '') ⇒ Object



81
82
83
# File 'lib/ruby-asterisk/agi/session.rb', line 81

def stream_file(filename, escape_digits = '')
  execute("STREAM FILE #{Protocol.quote(filename)} #{Protocol.quote(escape_digits)}")
end

#verbose(message, level: 1) ⇒ Object



109
110
111
# File 'lib/ruby-asterisk/agi/session.rb', line 109

def verbose(message, level: 1)
  execute("VERBOSE #{Protocol.quote(message)} #{level}")
end

#wait_for_digit(timeout_ms = 5000) ⇒ Object

Block until a DTMF digit is pressed or timeout expires. Like every verb this returns the parsed response Hash { code:, result:, data: }; the pressed key's decimal ASCII value (or -1 on timeout) is in response.



126
127
128
# File 'lib/ruby-asterisk/agi/session.rb', line 126

def wait_for_digit(timeout_ms = 5000)
  execute("WAIT FOR DIGIT #{timeout_ms}")
end