ruby-smpp 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,224 @@
1
+ # the opposite of a client-based receiver, the server transmitter waill send
2
+ # out MOs to the client when set up
3
+ class Smpp::Server < Smpp::Base
4
+
5
+ attr_accessor :bind_status
6
+
7
+ # Expects a config hash,
8
+ # a proc to invoke for incoming (MO) messages,
9
+ # a proc to invoke for delivery reports,
10
+ # and optionally a hash-like storage for pending delivery reports.
11
+ def initialize(config, received_messages = [], sent_messages = [])
12
+ super(config)
13
+ @state = :unbound
14
+ @received_messages = received_messages
15
+ @sent_messages = sent_messages
16
+
17
+ # Array of un-acked MT message IDs indexed by sequence number.
18
+ # As soon as we receive SubmitSmResponse we will use this to find the
19
+ # associated message ID, and then create a pending delivery report.
20
+ @ack_ids = Array.new(512)
21
+
22
+ ed = @config[:enquire_link_delay_secs] || 5
23
+ comm_inactivity_timeout = [ed - 5, 3].max
24
+ rescue Exception => ex
25
+ logger.error "Exception setting up server: #{ex}"
26
+ raise
27
+ end
28
+
29
+
30
+ #######################################################################
31
+ # Session management functions
32
+ #######################################################################
33
+ # Session helpers
34
+
35
+ # convenience methods
36
+ # is this session currently bound?
37
+ def bound?
38
+ @state == :bound
39
+ end
40
+ # is this session currently unbound?
41
+ def unbound?
42
+ @state == :unbound
43
+ end
44
+ # set of valid bind statuses
45
+ BIND_STATUSES = {:transmitter => :bound_tx,
46
+ :receiver => :bound_rx, :transceiver => :bound_trx}
47
+ # set the bind status based on the common-name for the bind class
48
+ def set_bind_status(bind_classname)
49
+ @bind_status = BIND_STATUSES[bind_classname]
50
+ end
51
+ # and kill the bind status when done
52
+ def unset_bind_status
53
+ @bind_status = nil
54
+ end
55
+ # what is the bind_status?
56
+ def bind_status
57
+ @bind_status
58
+ end
59
+ # convenience function - are we able to transmit in this bind-Status?
60
+ def transmitting?
61
+ # not transmitting if not bound
62
+ return false if unbound? || bind_status.nil?
63
+ # receivers can't transmit
64
+ bind_status != :bound_rx
65
+ end
66
+ # convenience function - are we able to receive in this bind-Status?
67
+ def receiving?
68
+ # not receiving if not bound
69
+ return false if unbound? || bind_status.nil?
70
+ # transmitters can't receive
71
+ bind_status != :bound_tx
72
+ end
73
+
74
+ def am_server?
75
+ true
76
+ end
77
+
78
+ # REVISIT - not sure if these are using the correct data. Currently just
79
+ # pulls the data straight out of the given pdu and sends it right back.
80
+ #
81
+ def fetch_bind_response_class(bind_classname)
82
+ # check we have a valid classname - probably overkill as only our code
83
+ # will send the classnames through
84
+ raise IOError, "bind class name missing" if bind_classname.nil?
85
+ raise IOError, "bind class name: #{bind_classname} unknown" unless BIND_STATUSES.has_key?(bind_classname)
86
+
87
+ case bind_classname
88
+ when :transceiver
89
+ return Smpp::Pdu::BindTransceiverResponse
90
+ when :transmitter
91
+ return Smpp::Pdu::BindTransmitterResponse
92
+ when :receiver
93
+ return Smpp::Pdu::BindReceiverResponse
94
+ end
95
+ end
96
+
97
+ # actually perform the action of binding the session to the given session
98
+ # type
99
+ def bind_session(bind_pdu, bind_classname)
100
+ # TODO: probably should not "raise" here - what's better?
101
+ raise IOError, "Session already bound." if bound?
102
+ response_class = fetch_bind_response_class(bind_classname)
103
+
104
+ # TODO: look inside the pdu for the password and check it
105
+
106
+ send_bind_response(bind_pdu, response_class)
107
+
108
+ @state = :bound
109
+ set_bind_status(bind_classname)
110
+ end
111
+
112
+ # Send BindReceiverResponse PDU - used in response to a "bind_receiver"
113
+ # pdu.
114
+ def send_bind_response(bind_pdu, bind_class)
115
+ resp_pdu = bind_class.new(
116
+ bind_pdu.sequence_number,
117
+ # currently assume that it binds ok
118
+ Pdu::Base::ESME_ROK,
119
+ # TODO: not sure where we get the system ID
120
+ # is this the session id?
121
+ bind_pdu.system_id)
122
+ write_pdu(resp_pdu)
123
+ end
124
+
125
+ #######################################################################
126
+ # Message submission (transmitter) functions (used by transmitter and
127
+ # transceiver-bound system)
128
+ # Note - we only support submit_sm message type, not submit_multi or
129
+ # data_sm message types
130
+ #######################################################################
131
+ # Receive an incoming message to send to the network and respond
132
+ # REVISIT = just a stub
133
+ def receive_sm(pdu)
134
+ # TODO: probably should not "raise" here - what's better?
135
+ raise IOError, "Connection not bound." if unbound?
136
+ # Doesn't matter if it's a TX/RX/TRX, have to send a SubmitSmResponse:
137
+ # raise IOError, "Connection not set to receive" unless receiving?
138
+
139
+ # Must respond to SubmitSm requests with the same sequence number
140
+ m_seq = pdu.sequence_number
141
+ # add the id to the list of ids of which we're awaiting acknowledgement
142
+ @received_messages << m_seq
143
+
144
+ # In theory this is where the MC would actually do something useful with
145
+ # the PDU - eg send it on to the network. We'd check if it worked and
146
+ # send a failure PDU if it failed.
147
+ #
148
+ # Given this is a dummy MC, that's not necessary, so all our responses
149
+ # will be OK.
150
+
151
+ # so respond with a successful response
152
+ pdu = Pdu::SubmitSmResponse.new(m_seq, Pdu::Base::ESME_ROK, message_id = '' )
153
+ write_pdu pdu
154
+ @received_messages.delete m_seq
155
+
156
+ logger.info "Received submit sm message: #{m_seq}"
157
+ end
158
+
159
+ #######################################################################
160
+ # Message delivery (receiver) functions (used by receiver and
161
+ # transceiver-bound system)
162
+ #######################################################################
163
+ # When we get an incoming SMS to send on to the client, we need to
164
+ # initiate one of these PDUs.
165
+ # Note - data doesn't have to be valid, as we're not really doing much
166
+ # useful with it. Only the params that will be pulled out by the test
167
+ # system need to be valid.
168
+ def deliver_sm(from, to, message, config = {})
169
+ # TODO: probably should not "raise" here - what's better?
170
+ raise IOError, "Connection not bound." if unbound?
171
+ raise IOError, "Connection not set to receive" unless receiving?
172
+
173
+ # submit the given message
174
+ new_pdu = Pdu::DeliverSm.new(from, to, message, config)
175
+ write_pdu(new_pdu)
176
+ # add the id to the list of ids of which we're awaiting acknowledgement
177
+ @sent_messages << m_seq
178
+
179
+ logger.info "Delivered SM message id: #{m_seq}"
180
+
181
+ new_pdu
182
+ end
183
+
184
+ # Acknowledge delivery of an outgoing MO message
185
+ # REVISIT = just a stub
186
+ def accept_deliver_sm_response(pdu)
187
+ m_seq = pdu.sequence_number
188
+ # add the id to the list of ids we're awaiting acknowledgement of
189
+ # REVISIT - what id do we need to store?
190
+ unless @sent_messages && @sent_messages.include?(m_seq)
191
+ logger.error("Received deliver response for message for which we have no saved id: #{m_seq}")
192
+ else
193
+ @sent_messages.delete(m_seq)
194
+ logger.info "Acknowledged receipt of SM delivery message id: #{m_seq}"
195
+ end
196
+ end
197
+
198
+
199
+ # a PDU is received
200
+ # these pdus are all responses to a message sent by the client and require
201
+ # their own special response
202
+ def process_pdu(pdu)
203
+ case pdu
204
+ # client has asked to set up a connection
205
+ when Pdu::BindTransmitter
206
+ bind_session(pdu, :transmitter)
207
+ when Pdu::BindReceiver
208
+ bind_session(pdu, :receiver)
209
+ when Pdu::BindTransceiver
210
+ bind_session(pdu, :transceiver)
211
+ # client has acknowledged receipt of a message we sent to them
212
+ when Pdu::DeliverSmResponse
213
+ accept_deliver_sm_response(pdu) # acknowledge its sending
214
+
215
+ # client has asked for a message to be sent
216
+ when Pdu::SubmitSm
217
+ receive_sm(pdu)
218
+ else
219
+ # for generic functions or default fallback
220
+ super(pdu)
221
+ end
222
+ end
223
+
224
+ end
@@ -1,15 +1,22 @@
1
+
2
+ # The SMPP Transceiver maintains a bidirectional connection to an SMSC.
3
+ # Provide a config hash with connection options to get started.
4
+ # See the sample_gateway.rb for examples of config values.
5
+ # The transceiver accepts a delegate object that may implement
6
+ # the following (all optional) methods:
7
+ #
8
+ # mo_received(transceiver, source_addr, destination_addr, short_message)
9
+ # delivery_report_received(transceiver, msg_reference, stat, pdu)
10
+ # message_accepted(transceiver, mt_message_id, smsc_message_id)
11
+ # bound(transceiver)
12
+ # unbound(transceiver)
13
+
1
14
  class Smpp::Transceiver < Smpp::Base
2
15
 
3
- # Expects a config hash,
4
- # a proc to invoke for incoming (MO) messages,
5
- # a proc to invoke for delivery reports,
6
- # and optionally a hash-like storage for pending delivery reports.
7
- def initialize(config, mo_proc, dr_proc, pdr_storage={})
16
+ def initialize(config, delegate, pdr_storage={})
8
17
  super(config)
9
- @state = :unbound
10
- @mo_proc = mo_proc
11
- @dr_proc = dr_proc
12
- @pdr_storage = pdr_storage
18
+ @delegate = delegate
19
+ @pdr_storage = pdr_storage
13
20
 
14
21
  # Array of un-acked MT message IDs indexed by sequence number.
15
22
  # As soon as we receive SubmitSmResponse we will use this to find the
@@ -17,13 +24,14 @@ class Smpp::Transceiver < Smpp::Base
17
24
  @ack_ids = Array.new(512)
18
25
 
19
26
  ed = @config[:enquire_link_delay_secs] || 5
20
- comm_inactivity_timeout = [ed - 5, 3].max
27
+ comm_inactivity_timeout = 2 * ed
21
28
  rescue Exception => ex
22
- logger.error "Exception setting up transceiver: #{ex}"
29
+ logger.error "Exception setting up transceiver: #{ex} at #{ex.backtrace.join("\n")}"
23
30
  raise
24
31
  end
25
32
 
26
- # Send an MT SMS message
33
+ # Send an MT SMS message. Delegate will receive message_accepted callback when SMSC
34
+ # acknowledges.
27
35
  def send_mt(message_id, source_addr, destination_addr, short_message, options={})
28
36
  logger.debug "Sending MT: #{short_message}"
29
37
  if @state == :bound
@@ -37,6 +45,41 @@ class Smpp::Transceiver < Smpp::Base
37
45
  raise InvalidStateException, "Transceiver is unbound. Cannot send MT messages."
38
46
  end
39
47
  end
48
+
49
+ # Send a concatenated message with a body of > 160 characters as multiple messages.
50
+ def send_concat_mt(message_id, source_addr, destination_addr, message, options = {})
51
+ logger.debug "Sending concatenated MT: #{message}"
52
+ if @state == :bound
53
+ # Split the message into parts of 153 characters. (160 - 7 characters for UDH)
54
+ parts = []
55
+ while message.size > 0 do
56
+ parts << message.slice!(0..152)
57
+ end
58
+
59
+ 0.upto(parts.size-1) do |i|
60
+ udh = sprintf("%c", 5) # UDH is 5 bytes.
61
+ udh << sprintf("%c%c", 0, 3) # This is a concatenated message
62
+ udh << sprintf("%c", message_id) # The ID for the entire concatenated message
63
+ udh << sprintf("%c", parts.size) # How many parts this message consists of
64
+ udh << sprintf("%c", i+1) # This is part i+1
65
+
66
+ options = {
67
+ :esm_class => 64, # This message contains a UDH header.
68
+ :udh => udh
69
+ }
70
+
71
+ pdu = Pdu::SubmitSm.new(source_addr, destination_addr, parts[i], options)
72
+ write_pdu pdu
73
+
74
+ # This is definately a bit hacky - multiple PDUs are being associated with a single
75
+ # message_id.
76
+ @ack_ids[pdu.sequence_number] = message_id
77
+ end
78
+ else
79
+ raise InvalidStateException, "Transceiver is unbound. Connot send MT messages."
80
+ end
81
+ end
82
+
40
83
  # Send MT SMS message for multiple dest_address
41
84
  # Author: Abhishek Parolkar (abhishek[at]parolkar.com)
42
85
  # USAGE: $tx.send_multi_mt(123, "9100000000", ["9199000000000","91990000000001","9199000000002"], "Message here")
@@ -54,34 +97,42 @@ class Smpp::Transceiver < Smpp::Base
54
97
  end
55
98
  end
56
99
 
57
-
58
- # a PDU is received
100
+ # a PDU is received. Parse it and invoke delegate methods.
59
101
  def process_pdu(pdu)
60
102
  case pdu
61
103
  when Pdu::DeliverSm
62
104
  write_pdu(Pdu::DeliverSmResponse.new(pdu.sequence_number))
63
105
  logger.debug "ESM CLASS #{pdu.esm_class}"
64
106
  if pdu.esm_class != 4
65
- # MO message; invoke MO proc
66
- @mo_proc.call(pdu.source_addr, pdu.destination_addr, pdu.short_message)
107
+ # MO message
108
+ if @delegate.respond_to?(:mo_received)
109
+ @delegate.mo_received(self, pdu.source_addr, pdu.destination_addr, pdu.short_message)
110
+ end
67
111
  else
68
- # Invoke DR proc (let the owner of the block do the mapping from msg_reference to mt_id)
69
- @dr_proc.call(pdu.msg_reference.to_s, pdu.stat)
112
+ # Delivery report
113
+ if @delegate.respond_to?(:delivery_report_received)
114
+ @delegate.delivery_report_received(self, pdu.msg_reference.to_s, pdu.stat, pdu)
115
+ end
70
116
  end
71
117
  when Pdu::BindTransceiverResponse
72
118
  case pdu.command_status
73
119
  when Pdu::Base::ESME_ROK
74
120
  logger.debug "Bound OK."
75
121
  @state = :bound
122
+ if @delegate.respond_to?(:bound)
123
+ @delegate.bound(self)
124
+ end
76
125
  when Pdu::Base::ESME_RINVPASWD
77
126
  logger.warn "Invalid password."
78
- EventMachine::stop_event_loop
127
+ # scheduele the connection to close, which eventually will cause the unbound() delegate
128
+ # method to be invoked.
129
+ close_connection
79
130
  when Pdu::Base::ESME_RINVSYSID
80
131
  logger.warn "Invalid system id."
81
- EventMachine::stop_event_loop
132
+ close_connection
82
133
  else
83
134
  logger.warn "Unexpected BindTransceiverResponse. Command status: #{pdu.command_status}"
84
- EventMachine::stop_event_loop
135
+ close_connection
85
136
  end
86
137
  when Pdu::SubmitSmResponse
87
138
  mt_message_id = @ack_ids[pdu.sequence_number]
@@ -92,9 +143,12 @@ class Smpp::Transceiver < Smpp::Base
92
143
  logger.error "Error status in SubmitSmResponse: #{pdu.command_status}"
93
144
  else
94
145
  logger.info "Got OK SubmitSmResponse (#{pdu.message_id} -> #{mt_message_id})"
146
+ if @delegate.respond_to?(:message_accepted)
147
+ @delegate.message_accepted(self, mt_message_id, pdu.message_id)
148
+ end
95
149
  end
96
- # Now we got the SMSC message id; create pending delivery report
97
- @pdr_storage[pdu.message_id] = mt_message_id
150
+ # Now we got the SMSC message id; create pending delivery report.
151
+ @pdr_storage[pdu.message_id] = mt_message_id
98
152
  when Pdu::SubmitMultiResponse
99
153
  mt_message_id = @ack_ids[pdu.sequence_number]
100
154
  if !mt_message_id
@@ -104,6 +158,9 @@ class Smpp::Transceiver < Smpp::Base
104
158
  logger.error "Error status in SubmitMultiResponse: #{pdu.command_status}"
105
159
  else
106
160
  logger.info "Got OK SubmitMultiResponse (#{pdu.message_id} -> #{mt_message_id})"
161
+ if @delegate.respond_to?(:message_accepted)
162
+ @delegate.message_accepted(self, mt_message_id, pdu.message_id)
163
+ end
107
164
  end
108
165
  else
109
166
  super
@@ -112,14 +169,14 @@ class Smpp::Transceiver < Smpp::Base
112
169
 
113
170
  # Send BindTransceiverResponse PDU.
114
171
  def send_bind
115
- raise IOError, 'Receiver already bound.' unless @state == :unbound
172
+ raise IOError, 'Receiver already bound.' unless unbound?
116
173
  pdu = Pdu::BindTransceiver.new(
117
- @config[:system_id],
118
- @config[:password],
119
- @config[:system_type],
120
- @config[:source_ton],
121
- @config[:source_npi],
122
- @config[:source_address_range])
174
+ @config[:system_id],
175
+ @config[:password],
176
+ @config[:system_type],
177
+ @config[:source_ton],
178
+ @config[:source_npi],
179
+ @config[:source_address_range])
123
180
  write_pdu(pdu)
124
181
  end
125
182
  end
data/lib/smpp/version.rb CHANGED
@@ -2,7 +2,7 @@ module Smpp #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
4
  MINOR = 1
5
- TINY = 1
5
+ TINY = 2
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
data/lib/smpp.rb CHANGED
@@ -11,6 +11,8 @@ $:.unshift(File.dirname(__FILE__))
11
11
  require 'smpp/base.rb'
12
12
  require 'smpp/transceiver.rb'
13
13
  require 'smpp/pdu/base.rb'
14
+ require 'smpp/pdu/bind_base.rb'
15
+ require 'smpp/pdu/bind_resp_base.rb'
14
16
 
15
17
  # Load all PDUs
16
18
  Dir.glob(File.join(File.dirname(__FILE__), 'smpp', 'pdu', '*.rb')) do |f|
@@ -18,4 +20,4 @@ Dir.glob(File.join(File.dirname(__FILE__), 'smpp', 'pdu', '*.rb')) do |f|
18
20
  end
19
21
 
20
22
  # Default logger. Invoke this call in your client to use another logger.
21
- Smpp::Base.logger = Logger.new(STDOUT)
23
+ Smpp::Base.logger = Logger.new(STDOUT)
data/ruby-smpp.gemspec ADDED
@@ -0,0 +1,36 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = %q{ruby-smpp}
3
+ s.version = "0.1.2"
4
+
5
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
6
+ s.authors = ["August Z. Flatby"]
7
+ s.date = %q{2009-01-04}
8
+ s.description = %q{Ruby-implementation of the SMPP protocol, based on EventMachine. SMPP is a protocol that allows ordinary people outside the mobile network to exchange SMS messages directly with mobile operators.}
9
+ s.email = ["august@apparat.no"]
10
+ s.extra_rdoc_files = ["History.txt", "License.txt", "Manifest.txt", "README.txt", "CONTRIBUTORS.txt"]
11
+ s.files = ["History.txt", "License.txt", "Manifest.txt", "README.txt", "CONTRIBUTORS.txt", "Rakefile", "examples/PDU1.example", "examples/PDU2.example", "examples/sample_gateway.rb", "examples/sample_smsc.rb", "config/hoe.rb", "config/requirements.rb", "lib/smpp.rb", "lib/smpp/base.rb", "lib/smpp/pdu/base.rb", "lib/smpp/pdu/bind_base.rb", "lib/smpp/pdu/bind_receiver.rb", "lib/smpp/pdu/bind_receiver_response.rb", "lib/smpp/pdu/bind_resp_base.rb", "lib/smpp/pdu/bind_transceiver.rb", "lib/smpp/pdu/bind_transceiver_response.rb", "lib/smpp/pdu/bind_transmitter.rb", "lib/smpp/pdu/bind_transmitter_response.rb", "lib/smpp/pdu/deliver_sm.rb", "lib/smpp/pdu/deliver_sm_response.rb", "lib/smpp/pdu/enquire_link.rb", "lib/smpp/pdu/enquire_link_response.rb", "lib/smpp/pdu/generic_nack.rb", "lib/smpp/pdu/submit_multi.rb", "lib/smpp/pdu/submit_multi_response.rb", "lib/smpp/pdu/submit_sm.rb", "lib/smpp/pdu/submit_sm_response.rb", "lib/smpp/pdu/unbind.rb", "lib/smpp/pdu/unbind_response.rb", "lib/smpp/server.rb", "lib/smpp/transceiver.rb", "lib/smpp/version.rb", "lib/sms.rb", "script/console", "script/destroy", "script/generate", "script/txt2html", "setup.rb", "tasks/deployment.rake", "tasks/environment.rake", "test/smpp_test.rb", "test/test_helper.rb"]
12
+ s.has_rdoc = true
13
+ s.homepage = %q{http://ruby-smpp.rubyforge.org}
14
+ s.rdoc_options = ["--main", "README.txt"]
15
+ s.require_paths = ["lib"]
16
+ s.rubyforge_project = %q{ruby-smpp}
17
+ s.rubygems_version = %q{1.3.0}
18
+ s.summary = %q{Ruby-implementation of the SMPP protocol, based on EventMachine. SMPP is a protocol that allows ordinary people outside the mobile network to exchange SMS messages directly with mobile operators.}
19
+ s.test_files = ["test/test_helper.rb"]
20
+
21
+ if s.respond_to? :specification_version then
22
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
23
+ s.specification_version = 2
24
+
25
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
26
+ s.add_runtime_dependency(%q<eventmachine>, [">= 0.10.0"])
27
+ s.add_development_dependency(%q<hoe>, [">= 1.8.0"])
28
+ else
29
+ s.add_dependency(%q<eventmachine>, [">= 0.10.0"])
30
+ s.add_dependency(%q<hoe>, [">= 1.8.0"])
31
+ end
32
+ else
33
+ s.add_dependency(%q<eventmachine>, [">= 0.10.0"])
34
+ s.add_dependency(%q<hoe>, [">= 1.8.0"])
35
+ end
36
+ end