Class: Jabber::MUC::MUCClient

Inherits:
Object
  • Object
show all
Defined in:
lib/xmpp4r/muc/helper/mucclient.rb

Overview

The MUCClient Helper handles low-level stuff of the Multi-User Chat (JEP 0045).

Use one instance per room.

Note that one client cannot join a single room multiple times. At least the clients' resources must be different. This is a protocol design issue. But don't consider it as a bug, it is just a clone-preventing feature.

Direct Known Subclasses

SimpleMUCClient

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(stream) ⇒ MUCClient

Initialize a MUCClient

Call MUCClient#join after you have registered your callbacks to avoid reception of stanzas after joining and before registration of callbacks.

stream
Stream

to operate on


45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/xmpp4r/muc/helper/mucclient.rb', line 45

def initialize(stream)
  # Attributes initialization
  @stream = stream
  @my_jid = nil
  @jid = nil
  @roster = {}
  @roster_lock = Mutex.new

  @active = false

  @join_cbs = CallbackList.new
  @leave_cbs = CallbackList.new
  @presence_cbs = CallbackList.new
  @message_cbs = CallbackList.new
  @private_message_cbs = CallbackList.new
end

Instance Attribute Details

#jidObject (readonly)

MUC JID

jid
JID

[email protected]/nick


36
37
38
# File 'lib/xmpp4r/muc/helper/mucclient.rb', line 36

def jid
  @jid
end

#my_jidObject

Sender JID, set this to use MUCClient from Components

my_jid
JID

Defaults to nil


26
27
28
# File 'lib/xmpp4r/muc/helper/mucclient.rb', line 26

def my_jid
  @my_jid
end

#rosterObject (readonly)

MUC room roster

roster
Hash

of [String] Nick => [Presence]


31
32
33
# File 'lib/xmpp4r/muc/helper/mucclient.rb', line 31

def roster
  @roster
end

Instance Method Details

#active?Boolean

Is the MUC client active?

This is false after initialization, true after joining and false after exit/kick

Returns:

  • (Boolean)

168
169
170
# File 'lib/xmpp4r/muc/helper/mucclient.rb', line 168

def active?
  @active
end

#add_join_callback(prio = 0, ref = nil, &block) ⇒ Object

Add a callback for <presence/> stanzas indicating availability of a MUC participant

This callback will not be called for initial presences when a client joins a room, but only for the presences afterwards.

The callback will be called from MUCClient#handle_presence with one argument: the <presence/> stanza. Note that this stanza will have been already inserted into MUCClient#roster.


279
280
281
# File 'lib/xmpp4r/muc/helper/mucclient.rb', line 279

def add_join_callback(prio = 0, ref = nil, &block)
  @join_cbs.add(prio, ref, block)
end

#add_leave_callback(prio = 0, ref = nil, &block) ⇒ Object

Add a callback for <presence/> stanzas indicating unavailability of a MUC participant

The callback will be called with one argument: the <presence/> stanza.

Note that this is called just before the stanza is removed from MUCClient#roster, so it is still possible to see the last presence in the given block.

If the presence's origin is your MUC JID, the MUCClient will be deactivated afterwards.


295
296
297
# File 'lib/xmpp4r/muc/helper/mucclient.rb', line 295

def add_leave_callback(prio = 0, ref = nil, &block)
  @leave_cbs.add(prio, ref, block)
end

#add_message_callback(prio = 0, ref = nil, &block) ⇒ Object

Add a callback for <message/> stanza directed to the whole room.

See MUCClient#add_private_message_callback for private messages between MUC participants.


312
313
314
# File 'lib/xmpp4r/muc/helper/mucclient.rb', line 312

def add_message_callback(prio = 0, ref = nil, &block)
  @message_cbs.add(prio, ref, block)
end

#add_presence_callback(prio = 0, ref = nil, &block) ⇒ Object

Add a callback for a <presence/> stanza which is neither a join nor a leave. This will be called when a room participant simply changes his status.


303
304
305
# File 'lib/xmpp4r/muc/helper/mucclient.rb', line 303

def add_presence_callback(prio = 0, ref = nil, &block)
  @presence_cbs.add(prio, ref, block)
end

#add_private_message_callback(prio = 0, ref = nil, &block) ⇒ Object

Add a callback for <message/> stanza with type='chat'.

These stanza are normally not broadcasted to all room occupants but are some sort of private messaging.


321
322
323
# File 'lib/xmpp4r/muc/helper/mucclient.rb', line 321

def add_private_message_callback(prio = 0, ref = nil, &block)
  @private_message_cbs.add(prio, ref, block)
end

#configure(options = {}) ⇒ Object

Use this method to configure a MUC room of which you are the owner.

options
Hash

where keys are the features of the room you wish

to configure. See www.xmpp.org/extensions/xep-0045.html#registrar-formtype-owner


410
411
412
413
# File 'lib/xmpp4r/muc/helper/mucclient.rb', line 410

def configure(options={})
  get_room_configuration
  submit_room_configuration(options)
end

#exit(reason = nil) ⇒ Object

Exit the room

  • Sends presence with type='unavailable' with an optional reason in <status/>,

  • then waits for a reply from the MUC component (will be processed by leave-callbacks),

  • then deletes callbacks from the stream.

reason
String

Optional custom exit message


137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/xmpp4r/muc/helper/mucclient.rb', line 137

def exit(reason=nil)
  unless active?
    raise "MUCClient hasn't yet joined"
  end

  pres = Presence.new
  pres.type = :unavailable
  pres.to = jid
  pres.from = @my_jid
  pres.status = reason if reason
  @stream.send(pres) { |r|
    Jabber::debuglog "exit: #{r.to_s.inspect}"
    if r.kind_of?(Presence) and r.type == :unavailable and r.from == jid
      @leave_cbs.process(r)
      true
    else
      false
    end
  }

  deactivate

  self
end

#from_room?(jid) ⇒ Boolean

Does this JID belong to that room?

jid
JID
result
true

or [false]

Returns:

  • (Boolean)

329
330
331
# File 'lib/xmpp4r/muc/helper/mucclient.rb', line 329

def from_room?(jid)
  @jid.strip == jid.strip
end

#get_room_configurationObject


415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
# File 'lib/xmpp4r/muc/helper/mucclient.rb', line 415

def get_room_configuration
  raise 'You are not the owner' unless owner?

  iq = Iq.new(:get, jid.strip)
  iq.from = my_jid
  iq.add(IqQueryMUCOwner.new)

  fields = []

  @stream.send_with_id(iq) do |answer|
    raise "Configuration not possible for this room" unless answer.query && answer.query.x(Dataforms::XData)

    answer.query.x(Dataforms::XData).fields.each do |field|
      if (var = field.attributes['var'])
        fields << var
      end
    end
  end

  fields
end

#join(jid, password = nil, opts = {}) ⇒ Object

Join a room

This registers its own callbacks on the stream provided to initialize and sends initial presence to the room. May throw ServerError if joining fails.

jid
JID

[email protected]/nick

password
String

Optional password

opts
Hash

If the parameter :history => false is passed then you will receive no history messages after initial presence

return
MUCClient

self (chain-able)


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
# File 'lib/xmpp4r/muc/helper/mucclient.rb', line 73

def join(jid, password=nil, opts={})
  if active?
    raise "MUCClient already active"
  end

  @jid = (jid.kind_of?(JID) ? jid : JID.new(jid))
  activate

  # Joining
  pres = Presence.new
  pres.to = @jid
  pres.from = @my_jid
  xmuc = XMUC.new
  xmuc.password = password

  # Add history/maxstanzas as requested. false=0
  xmuc.add_element REXML::Element.new('history').tap { |hist|
    hist.add_attribute 'maxstanzas', (opts[:history]||0).to_i.to_s
  } unless opts[:history].nil?

  pres.add(xmuc)

  # We don't use Stream#send_with_id here as it's unknown
  # if the MUC component *always* uses our stanza id.
  error = nil
  @stream.send(pres) { |r|
    if from_room?(r.from) and r.kind_of?(Presence) and r.type == :error
      # Error from room
      error = r.error
      true
    # type='unavailable' may occur when the MUC kills our previous instance,
    # but all join-failures should be type='error'
    elsif r.from == jid and r.kind_of?(Presence) and r.type != :unavailable
      # Our own presence reflected back - success
      if r.x(XMUCUser) and (i = r.x(XMUCUser).items.first)
        @affiliation = i.affiliation  # we're interested in if it's :owner
        @role = i.role                # :moderator ?
      end

      handle_presence(r, false)
      true
    else
      # Everything else
      false
    end
  }

  if error
    deactivate
    raise ServerError.new(error)
  end

  self
end

#nickObject

The MUCClient's own nick (= resource)

result
String

Nickname


176
177
178
# File 'lib/xmpp4r/muc/helper/mucclient.rb', line 176

def nick
  @jid ? @jid.resource : nil
end

#nick=(new_nick) ⇒ Object

Change nick

Threading is, again, suggested. This method waits for two <presence/> stanzas, one indicating unavailabilty of the old transient JID, one indicating availability of the new transient JID.

If the service denies nick-change, ServerError will be raised.


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
# File 'lib/xmpp4r/muc/helper/mucclient.rb', line 189

def nick=(new_nick)
  unless active?
    raise "MUCClient not active"
  end

  new_jid = JID.new(@jid.node, @jid.domain, new_nick)

  # Joining
  pres = Presence.new
  pres.to = new_jid
  pres.from = @my_jid

  error = nil
  # Keeping track of the two stanzas enables us to process stanzas
  # which don't arrive in the order specified by JEP-0045
  presence_unavailable = false
  presence_available = false
  # We don't use Stream#send_with_id here as it's unknown
  # if the MUC component *always* uses our stanza id.
  @stream.send(pres) { |r|
    if from_room?(r.from) and r.kind_of?(Presence) and r.type == :error
      # Error from room
      error = r.error
    elsif r.from == @jid and r.kind_of?(Presence) and r.type == :unavailable and
          r.x and r.x.kind_of?(XMUCUser) and r.x.status_code == 303
      # Old JID is offline, but wait for the new JID and let stanza be handled
      # by the standard callback
      presence_unavailable = true
      handle_presence(r)
    elsif r.from == new_jid and r.kind_of?(Presence) and r.type != :unavailable
      # Our own presence reflected back - success
      presence_available = true
      handle_presence(r)
    end

    if error or (presence_available and presence_unavailable)
      true
    else
      false
    end
  }

  if error
    raise ServerError.new(error)
  end

  # Apply new JID
  @jid = new_jid
end

#owner?Boolean

Returns:

  • (Boolean)

401
402
403
# File 'lib/xmpp4r/muc/helper/mucclient.rb', line 401

def owner?
  @affiliation == :owner
end

#roomObject

The room name (= node)

result
String

Room name


243
244
245
# File 'lib/xmpp4r/muc/helper/mucclient.rb', line 243

def room
  @jid ? @jid.node : nil
end

#send(stanza, to = nil, invite = false) ⇒ Object

Send a stanza to the room

If stanza is a Jabber::Message, stanza.type will be automatically set to :groupchat if directed to room or :chat if directed to participant.

stanza
XMPPStanza

to send

to
String

Stanza destination recipient, or room if nil


255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/xmpp4r/muc/helper/mucclient.rb', line 255

def send(stanza, to = nil, invite = false)
  if stanza.kind_of? Message
    if invite
      stanza.type = :normal
    else
      stanza.type = to ? :chat : :groupchat
    end
  end
  stanza.from = @my_jid
  stanza.to = JID.new(jid.node, jid.domain, to)
  @stream.send(stanza)
end

#send_affiliations(items) ⇒ Object

Push a list of new affiliations to the room

items
Array

of, or single [IqQueryMUCAdminItem]


459
460
461
462
463
464
465
466
467
468
469
470
# File 'lib/xmpp4r/muc/helper/mucclient.rb', line 459

def send_affiliations(items)
  iq = Iq.new(:set, jid.strip)
  iq.from = my_jid
  iq.add(IqQueryMUCAdmin.new)

  items = [items] unless items.kind_of? Array
  items.each { |item|
    iq.query.add(item)
  }

  @stream.send_with_id(iq)
end

#submit_room_configuration(options) ⇒ Object


437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
# File 'lib/xmpp4r/muc/helper/mucclient.rb', line 437

def submit_room_configuration(options)
  # fill out the reply form
  iq = Iq.new(:set, jid.strip)
  iq.from = my_jid
  query = IqQueryMUCOwner.new
  form = Dataforms::XData.new
  form.type = :submit
  options.each do |var, values|
    field = Dataforms::XDataField.new
    values = [values] unless values.is_a?(Array)
    field.var, field.values = var, values
    form.add(field)
  end
  query.add(form)
  iq.add(query)

  @stream.send_with_id(iq)
end