ruby-smpp 0.1.1 → 0.1.2

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.
@@ -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