trails 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/Manifest.txt +4 -1
- data/README.txt +5 -5
- data/lib/trails.rb +1 -3
- data/lib/trails/exception.rb +6 -0
- data/lib/trails/twilio/account.rb +134 -0
- data/lib/trails/twilio/call_handling.rb +40 -0
- data/lib/trails/twilio/incoming.rb +58 -0
- metadata +8 -5
- data/bin/trails +0 -0
data/History.txt
CHANGED
data/Manifest.txt
CHANGED
data/README.txt
CHANGED
@@ -14,11 +14,11 @@ Includes support for a special MimeType? (twiml), and functional tests: as_twili
|
|
14
14
|
|
15
15
|
== SYNOPSIS:
|
16
16
|
|
17
|
-
class ApplicationController < ActionController::Base
|
18
|
-
..
|
17
|
+
class ApplicationController < ActionController::Base
|
18
|
+
..
|
19
19
|
include Twilio::CallHandling
|
20
|
-
..
|
21
|
-
end
|
20
|
+
..
|
21
|
+
end
|
22
22
|
|
23
23
|
== REQUIREMENTS:
|
24
24
|
|
@@ -33,7 +33,7 @@ end
|
|
33
33
|
|
34
34
|
== LICENSE:
|
35
35
|
|
36
|
-
Copyright
|
36
|
+
Copyright 2010 Hemant Bhanoo
|
37
37
|
|
38
38
|
Licensed under the Apache License, Version 2.0 (the "License");
|
39
39
|
you may not use this file except in compliance with the License.
|
data/lib/trails.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
module Trails
|
2
|
-
VERSION = '1.0.
|
2
|
+
VERSION = '1.0.1'
|
3
3
|
end
|
4
4
|
begin
|
5
5
|
TwilioRest
|
@@ -7,8 +7,6 @@ rescue
|
|
7
7
|
require 'twiliorest.rb'
|
8
8
|
end
|
9
9
|
|
10
|
-
require 'action_controller' # yes?
|
11
|
-
|
12
10
|
require 'trails/exception.rb'
|
13
11
|
require 'trails/twilio/account'
|
14
12
|
require 'trails/twilio/call_handling'
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module Trails
|
2
|
+
module Twilio
|
3
|
+
class Account
|
4
|
+
attr_reader :config
|
5
|
+
def initialize( opts = {} )
|
6
|
+
if( opts.blank? )
|
7
|
+
STDERR.puts "no opts specified. trying to pull opts from #{self.class.config.inspect}"
|
8
|
+
opts = self.class.config[self.class.config.keys.first]
|
9
|
+
end
|
10
|
+
@config = opts.dup
|
11
|
+
@sid = @config[:sid] || raise( "no sid specified on #{self}" )
|
12
|
+
@token = @config[:token]
|
13
|
+
@logger = @config[:logger]
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.sid_from_request( request )
|
17
|
+
( :development == RAILS_ENV.to_sym ) ? request.params['AccountSid'] : request.env["HTTP_X_TWILIO_ACCOUNTSID"]
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.from_request( request )
|
21
|
+
sid = sid_from_request( request )
|
22
|
+
unless( config.has_key?( sid ) )
|
23
|
+
logger.warn{ "unknown account #{sid}. Request params: #{request.inspect}" }
|
24
|
+
raise Trails::Exception::UnknownAccount.new( sid )
|
25
|
+
end
|
26
|
+
account = new( config[sid].dup )
|
27
|
+
raise Trails::Exception::InvalidSignature unless account.verify_caller( request )
|
28
|
+
account
|
29
|
+
end
|
30
|
+
|
31
|
+
def verify_caller( request )
|
32
|
+
# TODO: check caller credentials here. :)
|
33
|
+
return true
|
34
|
+
end
|
35
|
+
|
36
|
+
# Make outgoing calls.
|
37
|
+
def call( number, handler_url, opts = {} )
|
38
|
+
params = {
|
39
|
+
'Caller' => opts['Caller'],
|
40
|
+
'Called' => number,
|
41
|
+
'Url' => handler_url,
|
42
|
+
'Method' => opts['Method'] || 'GET',
|
43
|
+
'Timeout' => opts['Timeout'] || 15
|
44
|
+
}
|
45
|
+
api_version = opts[:api_version] || '2008-08-01'
|
46
|
+
logger.debug( "Calling twlio with params: #{params.inspect}" )
|
47
|
+
make_request( File.join( base_uri, 'Calls' ), 'POST', params )
|
48
|
+
end
|
49
|
+
|
50
|
+
# still a WIP:
|
51
|
+
# number: to
|
52
|
+
# body: text
|
53
|
+
# url : response url
|
54
|
+
# opts:
|
55
|
+
# :from => number
|
56
|
+
# :method => GET/POST
|
57
|
+
MAX_SMS_LENGTH = 160
|
58
|
+
def send_sms( number, body, url, opts = {} )
|
59
|
+
params = {
|
60
|
+
'From' => opts[:from] || @config[:default_number],
|
61
|
+
'To' => number,
|
62
|
+
'Body' => body,
|
63
|
+
'Url' => url,
|
64
|
+
'Method' => opts[:method] || 'POST'
|
65
|
+
}
|
66
|
+
url = File.join( base_uri, 'SMS/Messages' )
|
67
|
+
logger.debug{ "Calling #{url} with #{params.inspect}" }
|
68
|
+
make_request(url, 'POST', params )
|
69
|
+
end
|
70
|
+
|
71
|
+
def incoming_numbers( reset = false )
|
72
|
+
if( @incoming_numbers.nil? || reset )
|
73
|
+
response =
|
74
|
+
make_request( File.join( base_uri, 'IncomingPhoneNumbers' ), 'GET' )
|
75
|
+
|
76
|
+
if( 200 == response.code.to_i )
|
77
|
+
@raw_incoming_numbers = Hpricot( response.body )
|
78
|
+
else
|
79
|
+
raise "got response code #{response.code} and body #{response.body}"
|
80
|
+
end
|
81
|
+
@incoming_numbers = @raw_incoming_numbers.search( '//phonenumber').
|
82
|
+
collect{|j| j.inner_html}
|
83
|
+
end
|
84
|
+
return @incoming_numbers
|
85
|
+
end
|
86
|
+
|
87
|
+
def outgoing_numbers( reset = false )
|
88
|
+
if( @outgoing_numbers.nil? || reset )
|
89
|
+
response =
|
90
|
+
make_request( "/2008-08-01/Accounts/#{@sid}/OutgoingCallerIds",
|
91
|
+
'GET' )
|
92
|
+
@outgoing_numbers_raw = Hpricot( response.body ) if( 200 == response.code.to_i )
|
93
|
+
@outgoing_numbers = @outgoing_numbers_raw.search( '//phonenumber').
|
94
|
+
collect{|j| j.inner_html}
|
95
|
+
end
|
96
|
+
return @outgoing_numbers
|
97
|
+
end
|
98
|
+
|
99
|
+
protected
|
100
|
+
|
101
|
+
|
102
|
+
# This makes it easy to create and call the TwilioRest library without
|
103
|
+
# having to worry about where credentials come from and stuff.
|
104
|
+
def make_request( *args )
|
105
|
+
@twilio_account ||= TwilioRest::Account.new( @sid, @token )
|
106
|
+
@twilio_account.request( *args )
|
107
|
+
end
|
108
|
+
|
109
|
+
def base_uri( opts = {} )
|
110
|
+
api_version = opts[:api_version] || @api_version || '2008-08-01'
|
111
|
+
sid = opts[:sid] || @sid
|
112
|
+
"/#{api_version}/Accounts/#{sid}/"
|
113
|
+
end
|
114
|
+
|
115
|
+
def logger
|
116
|
+
self.class.logger
|
117
|
+
end
|
118
|
+
def self.logger
|
119
|
+
return @logger unless @logger.nil?
|
120
|
+
@logger = Logger.new( STDERR )
|
121
|
+
@logger.level = Logger::WARN
|
122
|
+
return @logger
|
123
|
+
end
|
124
|
+
def self.config
|
125
|
+
@@cfg ||= YAML::load_file( config_file )
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.config_file
|
129
|
+
return File.join( RAILS_ROOT, 'config', 'twilio.yml' )
|
130
|
+
end
|
131
|
+
|
132
|
+
end # class Account
|
133
|
+
end # module Twilio
|
134
|
+
end # module Trails
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Trails
|
2
|
+
module Twilio
|
3
|
+
module CallHandling
|
4
|
+
def self.included( klass )
|
5
|
+
raise "can\'t include #{self} in #{klass} - not a Controller?" unless
|
6
|
+
klass.respond_to?( :before_filter )
|
7
|
+
Mime::Type.register_alias( "text/html", :twiml ) unless Mime.const_defined?( 'TWIML' )
|
8
|
+
klass.send( :before_filter, :setup_incoming_call )
|
9
|
+
klass.send( :attr_reader, :incoming_call )
|
10
|
+
klass.send( :alias_method_chain, :protect_against_forgery?, :twilio )
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def protect_against_forgery_with_twilio?
|
16
|
+
is_twilio_call? ? false : protect_against_forgery_without_twilio?
|
17
|
+
end
|
18
|
+
|
19
|
+
def setup_incoming_call
|
20
|
+
return unless is_twilio_call?
|
21
|
+
logger.debug( "at the beginning, request.params = #{request.parameters}" )
|
22
|
+
request.format = :twiml
|
23
|
+
response.content_type = 'text/xml'
|
24
|
+
@incoming_call = Trails::Twilio::Incoming.new( request )
|
25
|
+
end
|
26
|
+
|
27
|
+
# TODO: Move this onto the request object,
|
28
|
+
# it makes more sense to say:
|
29
|
+
#
|
30
|
+
# request.is_twilio_call?
|
31
|
+
#
|
32
|
+
def is_twilio_call?
|
33
|
+
return !Trails::Twilio::Account.sid_from_request( request ).blank?
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
end # module CallHandling
|
39
|
+
end # module Twilio
|
40
|
+
end # module Trails
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Trails
|
2
|
+
module Twilio
|
3
|
+
class Incoming
|
4
|
+
attr_reader :account
|
5
|
+
def initialize( request, opts = {} )
|
6
|
+
@request = request
|
7
|
+
@account = Trails::Twilio::Account.from_request( request )
|
8
|
+
end
|
9
|
+
|
10
|
+
protected
|
11
|
+
attr_reader :request
|
12
|
+
def twilio_data
|
13
|
+
@twilio_data ||= requests.params.slice( INCOMING_VARS ).dup
|
14
|
+
end
|
15
|
+
|
16
|
+
INCOMING_VARS = [
|
17
|
+
# Always available:
|
18
|
+
'CallGuid', # A unique identifier for this call, generated by Twilio. It's 34 characters long, and always starts with the letters CA.
|
19
|
+
'Caller', # The phone number of the party that initiated the call. If the call is inbound, then it is the caller's caller-id. If the call is outbound, i.e., initiated by making a request to the REST Call API, then this is the phone number you specify as the caller-id.
|
20
|
+
'Called', # The phone number of the party that was called. If the call is inbound, then it's your application phone number. If the call is outbound, then it's the phone number you provided to call.
|
21
|
+
'AccountGuid', # Your Twilio account number which is the Twilio Account GUID for the call. It is 34 characters long, and always starts with the letters AC.
|
22
|
+
'CallStatus', # The status of the phone call. The value can be "in-progress", "completed", "busy", "failed" or "no-answer". For a call that was answered and is currently going on, the status would be "in-progress". For a call that couldn't be started because the called party was busy, didn't pick up, or the number dialed wasn't valid: "busy", "no-answer", or "failed" would be returned. If the call finished because the call ended or was hung up, the status would be "completed".
|
23
|
+
'CallerCity', # The city of the caller.
|
24
|
+
'CallerState', # The state or province of the caller.
|
25
|
+
'CallerZip', # The postal code of the caller.
|
26
|
+
'CallerCountry', # The country of the caller.
|
27
|
+
'CalledCity', # The city of the called party.
|
28
|
+
'CalledState', # The state or province of the called party.
|
29
|
+
'CalledZip', # The postal code of the called party.
|
30
|
+
'CalledCountry', # The country of the called party.
|
31
|
+
# Gather:
|
32
|
+
'Digits', # The digits received from the caller
|
33
|
+
|
34
|
+
'RecordingUrl', # The URL of the recorded audio file
|
35
|
+
'Duration', # The time duration of the recorded audio file
|
36
|
+
'Digits', # What (if any) key was pressed to end the recording
|
37
|
+
|
38
|
+
# SMS:
|
39
|
+
'SmsMessageSid', # Message SID
|
40
|
+
'AccountSid', # Account ID
|
41
|
+
'From', #
|
42
|
+
'To', #
|
43
|
+
'Body', # 160 chars
|
44
|
+
|
45
|
+
]
|
46
|
+
public
|
47
|
+
INCOMING_VARS.uniq.each do |pname|
|
48
|
+
mname = pname.gsub( /[A-Z]/ ) { |s| "_#{s.downcase}" }.gsub( /^_/, '' )
|
49
|
+
# some extra debugging code here:
|
50
|
+
# ActionController::Base.logger.debug{ "defining method: #{mname} for param #{pname}" }
|
51
|
+
define_method( mname ) do
|
52
|
+
return request.params[ pname ]
|
53
|
+
end # define_method
|
54
|
+
end # each
|
55
|
+
|
56
|
+
end #
|
57
|
+
end # module Twilio
|
58
|
+
end # module Trails
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: trails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Hemant Bhanoo
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2010-02-
|
12
|
+
date: 2010-02-06 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -48,8 +48,8 @@ description: |-
|
|
48
48
|
Includes support for a special MimeType? (twiml), and functional tests: as_twilio{ get '/my_action' }
|
49
49
|
email:
|
50
50
|
- hemant@bhanoo.com
|
51
|
-
executables:
|
52
|
-
|
51
|
+
executables: []
|
52
|
+
|
53
53
|
extensions: []
|
54
54
|
|
55
55
|
extra_rdoc_files:
|
@@ -62,8 +62,11 @@ files:
|
|
62
62
|
- Manifest.txt
|
63
63
|
- README.txt
|
64
64
|
- Rakefile
|
65
|
-
- bin/trails
|
66
65
|
- lib/trails.rb
|
66
|
+
- lib/trails/exception.rb
|
67
|
+
- lib/trails/twilio/account.rb
|
68
|
+
- lib/trails/twilio/call_handling.rb
|
69
|
+
- lib/trails/twilio/incoming.rb
|
67
70
|
- test/test_trails.rb
|
68
71
|
has_rdoc: true
|
69
72
|
homepage: http://code.google.com/p/twilio-on-rails/
|
data/bin/trails
DELETED
File without changes
|