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.
MUC room roster
of [String] Nick => [Presence]
Initialize a MUCClient
Call #join after you have registered your callbacks to avoid reception of stanzas after joining and before registration of callbacks.
to operate on
# 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
Is the MUC client active?
This is false after initialization, true after joining and false after exit/kick
# File lib/xmpp4r/muc/helper/mucclient.rb, line 161 def active? @active end
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 #roster.
# File lib/xmpp4r/muc/helper/mucclient.rb, line 268 def add_join_callback(prio = 0, ref = nil, &block) @join_cbs.add(prio, ref, block) end
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 #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.
# File lib/xmpp4r/muc/helper/mucclient.rb, line 284 def add_leave_callback(prio = 0, ref = nil, &block) @leave_cbs.add(prio, ref, block) end
Add a callback for <message/> stanza directed to the whole room.
See #add_private_message_callback for private messages between MUC participants.
# File lib/xmpp4r/muc/helper/mucclient.rb, line 301 def add_message_callback(prio = 0, ref = nil, &block) @message_cbs.add(prio, ref, block) end
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.
# File lib/xmpp4r/muc/helper/mucclient.rb, line 292 def add_presence_callback(prio = 0, ref = nil, &block) @presence_cbs.add(prio, ref, block) end
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.
# File lib/xmpp4r/muc/helper/mucclient.rb, line 310 def add_private_message_callback(prio = 0, ref = nil, &block) @private_message_cbs.add(prio, ref, block) end
Use this method to configure a MUC room of which you are the owner.
where keys are the features of the room you wish
to configure. See www.xmpp.org/extensions/xep-0045.html#registrar-formtype-owner
# File lib/xmpp4r/muc/helper/mucclient.rb, line 399 def configure(options={}) get_room_configuration submit_room_configuration(options) end
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.
Optional custom exit message
# File lib/xmpp4r/muc/helper/mucclient.rb, line 130 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
# File lib/xmpp4r/muc/helper/mucclient.rb, line 404 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 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.
# File lib/xmpp4r/muc/helper/mucclient.rb, line 72 def join(jid, password=nil) 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 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
The MUCClient's own nick (= resource)
Nickname
# File lib/xmpp4r/muc/helper/mucclient.rb, line 169 def nick @jid ? @jid.resource : nil end
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.
# File lib/xmpp4r/muc/helper/mucclient.rb, line 182 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
# File lib/xmpp4r/muc/helper/mucclient.rb, line 390 def owner? @affiliation == :owner end
The room name (= node)
Room name
# File lib/xmpp4r/muc/helper/mucclient.rb, line 236 def room @jid ? @jid.node : nil end
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.
to send
Stanza destination recipient, or room if nil
# File lib/xmpp4r/muc/helper/mucclient.rb, line 248 def send(stanza, to=nil) if stanza.kind_of? Message stanza.type = to ? :chat : :groupchat end stanza.from = @my_jid stanza.to = JID.new(jid.node, jid.domain, to) @stream.send(stanza) end
Push a list of new affiliations to the room
of, or single [IqQueryMUCAdminItem]
# File lib/xmpp4r/muc/helper/mucclient.rb, line 448 def send_affiliations(items) iq = Iq.new(:set, jid.strip) iq.from = my_jid iq.add(IqQueryMUCAdmin.new) items = [item] unless items.kind_of? Array items.each { |item| iq.query.add(item) } @stream.send_with_id(iq) end
# File lib/xmpp4r/muc/helper/mucclient.rb, line 426 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