aboutsummaryrefslogtreecommitdiff
path: root/lib/mauve/notifiers/xmpp-smack.rb
blob: a160a35208700e4a8b1478923da437857cd82ff4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
# encoding: utf-8

# Ruby.
require 'pp'
require 'log4r'
require 'monitor'

# Java.  Note that paths are mangeled in jmauve_starter.
require 'java'
require 'smack.jar'
require 'smackx.jar'
include_class "org.jivesoftware.smack.XMPPConnection"
include_class "org.jivesoftware.smackx.muc.MultiUserChat"
include_class "org.jivesoftware.smack.RosterListener"

module Mauve

  module Notifiers    

    module Xmpp
      
      class XMPPSmackException < StandardError
      end

      ## Main wrapper to smack java library.
      #
      # @author Yann Golanski
      # @see http://www.igniterealtime.org/builds/smack/docs/3.1.0/javadoc/
      #
      # This is a singleton which is not idea but works well for mauve's 
      # configuration file set up. 
      #
      # In general, this class is meant to be intialized then the method 
      # create_slave_thread must be called.  The latter will spawn a new 
      # thread that will do the connecting and sending of messages to 
      # the XMPP server.  Once this is done, messages can be send via the 
      # send_msg() method.  Those will be queued and depending on the load,
      # should be send quickly enough.  This is done so that the main thread
      # can not worry about sending messages and can do important work. 
      #
      # @example
      #  bot = Mauve::Notifiers::Xmpp::XMPPSmack.new()
      #  bot.run_slave_thread("chat.bytemark.co.uk", 'mauvealert', 'TopSecret')
      #  msg = "What fresh hell is this? -- Dorothy Parker."
      #  bot.send_msg("yann@chat.bytemark.co.uk", msg)
      #  bot.send_msg("muc:test@conference.chat.bytemark.co.uk", msg)
      #
      # @FIXME  This won't quiet work with how mauve is set up. 
      #
      class XMPPSmack
        
        # Globals are evil.
        @@instance = nil

        # Default constructor.
        #
        # A queue (@queue) is used to pass information between master/slave.
        def initialize ()
          extend(MonitorMixin)
          @logger =  Log4r::Logger.new "mauve::XMPP_smack<#{Process.pid}>"
          @queue = Queue.new 
          @xmpp = nil
          @name = "mauve alert"
          @slave_thread = nil
          @regexp_muc = Regexp.compile(/^muc\:/)
          @regexp_tail = Regexp.compile(/\/.*$/)
          @jid_created_chat = Hash.new()
          @separator = '<->'
          @logger.info("Created XMPPSmack singleton")
        end

        # Returns the instance of the XMPPSmack singleton.
        #
        # @param [String] login The JID as a full address.
        # @param [String] pwd The password corresponding to the JID.
        # @return [XMPPSmack] The singleton instance.
        def self.instance (login, pwd)
          if true == @@instance.nil?
            @@instance = XMPPSmack.new
            jid, tmp = login.split(/@/)
            srv, name = tmp.split(/\//)
            name = "Mauve Alert Bot" if true == name.nil?
            @@instance.run_slave_thread(srv, jid, pwd, name)
            sleep 5 # FIXME: This really should be synced... But how?
          end
          return @@instance
        end

        # Create the thread that sends messages to the server.
        #
        # @param [String] srv The server address.
        # @param [String] jid The JID.
        # @param [String] pwd The password corresponding to the JID.
        # @param [String] name The bot name.
        # @return [NULL] nada
        def run_slave_thread (srv, jid, pwd, name)
          @srv = srv
          @jid = jid
          @pwd = pwd
          @name = name
          @logger.info("Creating slave thread on #{@jid}@#{@srv}/#{@name}.")
          @slave_thread = Thread.new do 
            self.create_slave_thread()
          end
          return nil
        end

        # Returns whether instance is connected and authenticated.
        #
        # @return [Boolean] True or false.
        def is_connected_and_authenticated? ()
          return false if true == @xmpp.nil?
          return (@xmpp.isConnected() and @xmpp.isAuthenticated())
        end

        # Creates the thread that does the actual sending to XMPP.
        # @return [NULL] nada
        def create_slave_thread ()
          begin
            @logger.info("Slave thread is now alive.")
            self.open()
            loop do
              rcp, msg = @queue.deq().split(@separator, 2)
              @logger.debug("New message for '#{rcp}' saying '#{msg}'.")
              if rcp.match(@regexp_muc)
                room = rcp.gsub(@regexp_muc, '').gsub(@regexp_tail, '')
                self.send_to_muc(room, msg)
              else
                self.send_to_jid(rcp, msg)
              end
            end
          rescue XMPPSmackException
            @logger.fatal("Something is wrong")
          ensure 
            @logger.info("XMPP bot disconnect.")
            @xmpp.disconnect()
          end
          return nil
        end

        # Send a message to the recipient.
        #
        # @param [String] rcp The recipent MUC or JID.
        # @param [String] msg The message.
        # @return [NULL] nada
        def send_msg(rcp, msg)
          #if @slave_thread.nil? or not self.is_connected_and_authenticated?()
          #  str = "There is either no slave thread running or a disconnect..."
          #  @logger.warn(str)
          #  self.reconnect()
          #end
          @queue.enq(rcp + @separator + msg)
          return nil
        end

        # Sends a message to a room.
        #
        # @param [String] room The name of the room.
        # @param [String] mgs The message to send.
        # @return [NULL] nada
        def send_to_muc (room, msg)
          if not @jid_created_chat.has_key?(room)
            @jid_created_chat[room] = MultiUserChat.new(@xmpp, room)
            @jid_created_chat[room].join(@name)
          end
          @logger.debug("Sending to MUC '#{room}' message '#{msg}'.")
          @jid_created_chat[room].sendMessage(msg)
          return nil
        end

        # Sends a message to a jid.
        #
        # Do not destroy the chat, we can reuse it when the user log back in again. 
        # Maybe?
        #
        # @param [String] jid The JID of the recipient.
        # @param [String] mgs The message to send.
        # @return [NULL] nada
        def send_to_jid (jid, msg)
          if true == jid_is_available?(jid)
            if not @jid_created_chat.has_key?(jid)
              @jid_created_chat[jid] = @xmpp.getChatManager.createChat(jid, nil)
            end
            @logger.debug("Sending to JID '#{jid}' message '#{msg}'.")
            @jid_created_chat[jid].sendMessage(msg)
          end
          return nil
        end

        # Check to see if the jid is available or not.
        #
        # @param [String] jid The JID of the recipient.
        # @return [Boolean] Whether we can send a message or not.
        def jid_is_available?(jid)
          if true == @xmpp.getRoster().getPresence(jid).isAvailable()
            @logger.debug("#{jid} is available. Status is " +
                          "#{@xmpp.getRoster().getPresence(jid).getStatus()}")
            return true
          else
            @logger.warn("#{jid} is not available. Status is " +
                         "#{@xmpp.getRoster().getPresence(jid).getStatus()}")
            return false
          end
        end

        # Opens a connection to the xmpp server at given port.
        #
        # @return [NULL] nada
        def open()
          @logger.info("XMPP bot is being created.")
          self.open_connection()
          self.open_authentication()
          self.create_roster()
          sleep 5
          return nil
        end

        # Connect to server.
        #
        # @return [NULL] nada
        def open_connection()
          @xmpp = XMPPConnection.new(@srv)
          if false == self.connect()
            str = "Connection refused"
            @logger.error(str)
            raise XMPPSmackException.new(str)
          end
          @logger.debug("XMPP bot connected successfully.")
          return nil
        end

        # Authenticat connection.
        #
        # @return [NULL] nada
        def open_authentication()
          if false == self.login(@jid, @pwd)
            str = "Authentication failed"
            @logger.error(str)
            raise XMPPSmackException.new(str)
          end
          @logger.debug("XMPP bot authenticated successfully.")
          return nil
        end

        # Create a new roster and listener.
        #
        # @return [NULL] nada
        def create_roster
          @xmpp.getRoster().addRosterListener(RosterListener.new())
          @xmpp.getRoster().reload()
          @xmpp.getRoster().getPresence(@xmpp.getUser).setStatus(
            "Purple alert! Purple alert!")
          @logger.debug("XMPP bot roster aquired successfully.")
          return nil
        end

        # Connects to the server.
        #
        # @return [Boolean] true (aka sucess) or false (aka failure).
        def connect ()
          @xmpp.connect()
          return @xmpp.isConnected()
        end
        
        # Login onto the server.
        #
        # @param [String] jid The JID.
        # @param [String] pwd The password corresponding to the JID.
        # @return [Boolean] true (aka sucess) or false (aka failure).
        def login (jid, pwd)
          @xmpp.login(jid, pwd, @name)
          return @xmpp.isAuthenticated()
        end

        # Reconnects in case of errors.
        #
        # @return [NULL] nada
        def reconnect()
          @xmpp.disconnect
          @slave_thread = Thread.new do 
            self.create_slave_thread()
          end
          return nil
        end

        def presenceChanged ()
        end

      end # XMPPSmack


      ## This is the class that gets called in person.rb. 
      #
      # This class is a wrapper to XMPPSmack which does the hard work. It is
      # done this way to conform to the mauve configuration file way of 
      # defining notifications.
      #
      # @author Yann Golanski
      class Default

        # Name of the class.
        attr_reader :name

        # Atrtribute.
        attr_accessor :jid

        # Atrtribute.
        attr_accessor :password

        # Atrtribute.
        attr_accessor :initial_jid

        # Atrtribute.
        attr_accessor :initial_messages
        
        # Default constructor.
        #
        # @param [String] name The name of the notifier.
        def initialize (name)
          extend(MonitorMixin)
          @name = name
          @logger = Log4r::Logger.new "mauve::XMPP_default<#{Process.pid}>"
        end

        # Sends a message to the relevant jid or muc.
        #
        # We have no way to know if a messages was recieved, only that 
        # we send it.
        # 
        # @param [String] destionation
        # @param [Alert] alert A mauve alert class
        # @param [Array] all_alerts subset of current alerts
        # @param [Hash] conditions Supported conditions, see above.
        # @return [Boolean] Whether a message can be send or not. 
        def send_alert(destination, alert, all_alerts, conditions = nil)
          synchronize { 
            client = XMPPSmack.instance(@jid, @password) 
            if not destination.match(/^muc:/)
              if false == client.jid_is_available?(destination.gsub(/^muc:/, ''))
                return false
              end
            end
            client.send_msg(destination, convert_alert_to_message(alert))
            return true
          }
        end

        # Takes an alert and converts it into a message.
        #
        # @param [Alert] alert The alert to convert.
        # @return [String] The message, either as HTML.
        def convert_alert_to_message(alert)
          arr = alert.summary_three_lines
          str = arr[0] + ": " + arr[1]
          str += " -- " + arr[2] if false == arr[2].nil?
          str += "."
          return str
          #return alert.summary_two_lines.join(" -- ")
          #return "<p>" + alert.summary_two_lines.join("<br />") + "</p>"
        end

        # This is so unit tests can run fine.
        include Debug

      end # Default

    end
  end
end

# This is a simple example of usage.  Run with:
#   ../../../jmauve_starter.rb xmpp-smack.rb 
# Clearly, the mauve jabber password is not correct.  
#
#   /!\ WARNING:   DO NOT COMMIT THE REAL PASSWORD TO MERCURIAL!!!
#
def send_msg()
  bot = Mauve::Notifiers::Xmpp::XMPPSmack.instance(
    "mauvealert@chat.bytemark.co.uk/testing1234", '')
  msg = "What fresh hell is this? -- Dorothy Parker."
  bot.send_msg("yann@chat.bytemark.co.uk", msg)
  bot.send_msg("muc:test@conference.chat.bytemark.co.uk", msg)
  sleep 2
end

if __FILE__ == './'+$0
  Thread.abort_on_exception = true
  logger = Log4r::Logger.new('mauve')
  logger.level = Log4r::DEBUG
  logger.add Log4r::Outputter.stdout
  send_msg()
  send_msg()
  logger.info("START")
  logger.info("END")
end