1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 """Jabber Multi-User Chat implementation.
18
19 Normative reference:
20 - `JEP 45 <http://www.jabber.org/jeps/jep-0045.html>`__
21 """
22
23 __revision__="$Id: muc.py 714 2010-04-05 10:20:10Z jajcus $"
24 __docformat__="restructuredtext en"
25
26 import logging
27
28 from pyxmpp.presence import Presence
29 from pyxmpp.message import Message
30 from pyxmpp.iq import Iq
31 from pyxmpp.jid import JID
32
33 from pyxmpp.xmlextra import xml_element_ns_iter
34
35 from pyxmpp.jabber.muccore import MucPresence,MucUserX,MucItem,MucStatus
36 from pyxmpp.jabber.muccore import MUC_OWNER_NS
37
38 from pyxmpp.jabber.dataforms import DATAFORM_NS, Form
39
40 import weakref
41
43 """
44 Base class for MUC room handlers.
45
46 Methods of this class will be called for various events in the room.
47
48 :Ivariables:
49 - `room_state`: MucRoomState object describing room state and its
50 participants.
51
52 """
54 """Initialize a `MucRoomHandler` object."""
55 self.room_state=None
56 self.__logger=logging.getLogger("pyxmpp.jabber.MucRoomHandler")
57
59 """Assign a state object to this `MucRoomHandler` instance.
60
61 :Parameters:
62 - `state_obj`: the state object.
63 :Types:
64 - `state_obj`: `MucRoomState`"""
65 self.room_state=state_obj
66
68 """
69 Called when the room has been created.
70
71 Default action is to request an "instant room" by accepting the default
72 configuration. Instead the application may want to request a
73 configuration form and submit it.
74
75 :Parameters:
76 - `stanza`: the stanza received.
77
78 :Types:
79 - `stanza`: `pyxmpp.stanza.Stanza`
80 """
81 _unused = stanza
82 self.room_state.request_instant_room()
83
97
103
105 """
106 Called when a new participant joins the room.
107
108 :Parameters:
109 - `user`: the user joining.
110 - `stanza`: the stanza received.
111
112 :Types:
113 - `user`: `MucRoomUser`
114 - `stanza`: `pyxmpp.stanza.Stanza`
115 """
116 pass
117
119 """
120 Called when a participant leaves the room.
121
122 :Parameters:
123 - `user`: the user leaving.
124 - `stanza`: the stanza received.
125
126 :Types:
127 - `user`: `MucRoomUser`
128 - `stanza`: `pyxmpp.stanza.Stanza`
129 """
130 pass
131
133 """
134 Called when a role of an user has been changed.
135
136 :Parameters:
137 - `user`: the user (after update).
138 - `old_role`: user's role before update.
139 - `new_role`: user's role after update.
140 - `stanza`: the stanza received.
141
142 :Types:
143 - `user`: `MucRoomUser`
144 - `old_role`: `unicode`
145 - `new_role`: `unicode`
146 - `stanza`: `pyxmpp.stanza.Stanza`
147 """
148 pass
149
151 """
152 Called when a affiliation of an user has been changed.
153
154 `user` MucRoomUser object describing the user (after update).
155 `old_aff` is user's affiliation before update.
156 `new_aff` is user's affiliation after update.
157 `stanza` the stanza received.
158 """
159 pass
160
162 """
163 Called when user nick change is started.
164
165 :Parameters:
166 - `user`: the user (before update).
167 - `new_nick`: the new nick.
168 - `stanza`: the stanza received.
169
170 :Types:
171 - `user`: `MucRoomUser`
172 - `new_nick`: `unicode`
173 - `stanza`: `pyxmpp.stanza.Stanza`
174 """
175 pass
176
178 """
179 Called after a user nick has been changed.
180
181 :Parameters:
182 - `user`: the user (after update).
183 - `old_nick`: the old nick.
184 - `stanza`: the stanza received.
185
186 :Types:
187 - `user`: `MucRoomUser`
188 - `old_nick`: `unicode`
189 - `stanza`: `pyxmpp.stanza.Stanza`
190 """
191 pass
192
194 """
195 Called whenever user's presence changes (includes nick, role or
196 affiliation changes).
197
198 :Parameters:
199 - `user`: MucRoomUser object describing the user.
200 - `stanza`: the stanza received.
201
202 :Types:
203 - `user`: `MucRoomUser`
204 - `stanza`: `pyxmpp.stanza.Stanza`
205 """
206 pass
207
209 """
210 Called when the room subject has been changed.
211
212 :Parameters:
213 - `user`: the user changing the subject.
214 - `stanza`: the stanza used to change the subject.
215
216 :Types:
217 - `user`: `MucRoomUser`
218 - `stanza`: `pyxmpp.stanza.Stanza`
219 """
220 pass
221
223 """
224 Called when groupchat message has been received.
225
226 :Parameters:
227 - `user`: the sender.
228 - `stanza`: is the message stanza received.
229
230 :Types:
231 - `user`: `MucRoomUser`
232 - `stanza`: `pyxmpp.stanza.Stanza`
233 """
234 pass
235
237 """
238 Called when an error stanza is received in reply to a room
239 configuration request.
240
241 By default `self.error` is called.
242
243 :Parameters:
244 - `stanza`: the stanza received.
245 :Types:
246 - `stanza`: `pyxmpp.stanza.Stanza`
247 """
248 self.error(stanza)
249
251 """
252 Called when an error stanza is received.
253
254 :Parameters:
255 - `stanza`: the stanza received.
256 :Types:
257 - `stanza`: `pyxmpp.stanza.Stanza`
258 """
259 err=stanza.get_error()
260 self.__logger.debug("Error from: %r Condition: %r"
261 % (stanza.get_from(),err.get_condition))
262
264 """
265 Describes a user of a MUC room.
266
267 The attributes of this object should not be changed directly.
268
269 :Ivariables:
270 - `presence`: last presence stanza received for the user.
271 - `role`: user's role.
272 - `affiliation`: user's affiliation.
273 - `room_jid`: user's room jid.
274 - `real_jid`: user's real jid or None if not available.
275 - `nick`: user's nick (resource part of `room_jid`)
276 :Types:
277 - `presence`: `MucPresence`
278 - `role`: `str`
279 - `affiliation`: `str`
280 - `room_jid`: `JID`
281 - `real_jid`: `JID`
282 - `nick`: `unicode`
283 """
284 - def __init__(self,presence_or_user_or_jid):
285 """
286 Initialize a `MucRoomUser` object.
287
288 :Parameters:
289 - `presence_or_user_or_jid`: a MUC presence stanza with user
290 information, a user object to copy or a room JID of a user.
291 :Types:
292 - `presence_or_user_or_jid`: `MucPresence` or `MucRoomUser` or
293 `JID`
294
295 When `presence_or_user_or_jid` is a JID user's
296 role and affiliation are set to "none".
297 """
298 if isinstance(presence_or_user_or_jid,MucRoomUser):
299 self.presence=presence_or_user_or_jid.presence
300 self.role=presence_or_user_or_jid.role
301 self.affiliation=presence_or_user_or_jid.affiliation
302 self.room_jid=presence_or_user_or_jid.room_jid
303 self.real_jid=presence_or_user_or_jid.real_jid
304 self.nick=presence_or_user_or_jid.nick
305 self.new_nick=None
306 else:
307 self.affiliation="none"
308 self.presence=None
309 self.real_jid=None
310 self.new_nick=None
311 if isinstance(presence_or_user_or_jid,JID):
312 self.nick=presence_or_user_or_jid.resource
313 self.room_jid=presence_or_user_or_jid
314 self.role="none"
315 elif isinstance(presence_or_user_or_jid,Presence):
316 self.nick=None
317 self.room_jid=None
318 self.role="participant"
319 self.update_presence(presence_or_user_or_jid)
320 else:
321 raise TypeError,"Bad argument type for MucRoomUser constructor"
322
324 """
325 Update user information.
326
327 :Parameters:
328 - `presence`: a presence stanza with user information update.
329 :Types:
330 - `presence`: `MucPresence`
331 """
332 self.presence=MucPresence(presence)
333 t=presence.get_type()
334 if t=="unavailable":
335 self.role="none"
336 self.affiliation="none"
337 self.room_jid=self.presence.get_from()
338 self.nick=self.room_jid.resource
339 mc=self.presence.get_muc_child()
340 if isinstance(mc,MucUserX):
341 items=mc.get_items()
342 for item in items:
343 if not isinstance(item,MucItem):
344 continue
345 if item.role:
346 self.role=item.role
347 if item.affiliation:
348 self.affiliation=item.affiliation
349 if item.jid:
350 self.real_jid=item.jid
351 if item.nick:
352 self.new_nick=item.nick
353 break
354
356 """Check if two `MucRoomUser` objects describe the same user in the
357 same room.
358
359 :Parameters:
360 - `other`: the user object to compare `self` with.
361 :Types:
362 - `other`: `MucRoomUser`
363
364 :return: `True` if the two object describe the same user.
365 :returntype: `bool`"""
366 return self.room_jid==other.room_jid
367
369 """
370 Describes the state of a MUC room, handles room events
371 and provides an interface for room actions.
372
373 :Ivariables:
374 - `own_jid`: real jid of the owner (client using this class).
375 - `room_jid`: room jid of the owner.
376 - `handler`: MucRoomHandler object containing callbacks to be called.
377 - `manager`: MucRoomManager object managing this room.
378 - `joined`: True if the channel is joined.
379 - `subject`: current subject of the room.
380 - `users`: dictionary of users in the room. Nicknames are the keys.
381 - `me`: MucRoomUser instance of the owner.
382 - `configured`: `False` if the room requires configuration.
383 """
384 - def __init__(self,manager,own_jid,room_jid,handler):
385 """
386 Initialize a `MucRoomState` object.
387
388 :Parameters:
389 - `manager`: an object to manage this room.
390 - `own_jid`: real JID of the owner (client using this class).
391 - `room_jid`: room JID of the owner (provides the room name and
392 the nickname).
393 - `handler`: an object to handle room events.
394 :Types:
395 - `manager`: `MucRoomManager`
396 - `own_jid`: JID
397 - `room_jid`: JID
398 - `handler`: `MucRoomHandler`
399 """
400 self.own_jid=own_jid
401 self.room_jid=room_jid
402 self.handler=handler
403 self.manager=weakref.proxy(manager)
404 self.joined=False
405 self.subject=None
406 self.users={}
407 self.me=MucRoomUser(room_jid)
408 self.configured = None
409 self.configuration_form = None
410 handler.assign_state(self)
411 self.__logger=logging.getLogger("pyxmpp.jabber.MucRoomState")
412
413 - def get_user(self,nick_or_jid,create=False):
414 """
415 Get a room user with given nick or JID.
416
417 :Parameters:
418 - `nick_or_jid`: the nickname or room JID of the user requested.
419 - `create`: if `True` and `nick_or_jid` is a JID, then a new
420 user object will be created if there is no such user in the room.
421 :Types:
422 - `nick_or_jid`: `unicode` or `JID`
423 - `create`: `bool`
424
425 :return: the named user or `None`
426 :returntype: `MucRoomUser`
427 """
428 if isinstance(nick_or_jid,JID):
429 if not nick_or_jid.resource:
430 return None
431 for u in self.users.values():
432 if nick_or_jid in (u.room_jid,u.real_jid):
433 return u
434 if create:
435 return MucRoomUser(nick_or_jid)
436 else:
437 return None
438 return self.users.get(nick_or_jid)
439
441 """
442 Called when current stream changes.
443
444 Mark the room not joined and inform `self.handler` that it was left.
445
446 :Parameters:
447 - `stream`: the new stream.
448 :Types:
449 - `stream`: `pyxmpp.stream.Stream`
450 """
451 _unused = stream
452 if self.joined and self.handler:
453 self.handler.user_left(self.me,None)
454 self.joined=False
455
456 - def join(self, password=None, history_maxchars = None,
457 history_maxstanzas = None, history_seconds = None, history_since = None):
458 """
459 Send a join request for the room.
460
461 :Parameters:
462 - `password`: password to the room.
463 - `history_maxchars`: limit of the total number of characters in
464 history.
465 - `history_maxstanzas`: limit of the total number of messages in
466 history.
467 - `history_seconds`: send only messages received in the last
468 `history_seconds` seconds.
469 - `history_since`: Send only the messages received since the
470 dateTime specified (UTC).
471 :Types:
472 - `password`: `unicode`
473 - `history_maxchars`: `int`
474 - `history_maxstanzas`: `int`
475 - `history_seconds`: `int`
476 - `history_since`: `datetime.datetime`
477 """
478 if self.joined:
479 raise RuntimeError,"Room is already joined"
480 p=MucPresence(to_jid=self.room_jid)
481 p.make_join_request(password, history_maxchars, history_maxstanzas,
482 history_seconds, history_since)
483 self.manager.stream.send(p)
484
486 """
487 Send a leave request for the room.
488 """
489 if self.joined:
490 p=MucPresence(to_jid=self.room_jid,stanza_type="unavailable")
491 self.manager.stream.send(p)
492
494 """
495 Send a message to the room.
496
497 :Parameters:
498 - `body`: the message body.
499 :Types:
500 - `body`: `unicode`
501 """
502 m=Message(to_jid=self.room_jid.bare(),stanza_type="groupchat",body=body)
503 self.manager.stream.send(m)
504
506 """
507 Send a subject change request to the room.
508
509 :Parameters:
510 - `subject`: the new subject.
511 :Types:
512 - `subject`: `unicode`
513 """
514 m=Message(to_jid=self.room_jid.bare(),stanza_type="groupchat",subject=subject)
515 self.manager.stream.send(m)
516
518 """
519 Send a nick change request to the room.
520
521 :Parameters:
522 - `new_nick`: the new nickname requested.
523 :Types:
524 - `new_nick`: `unicode`
525 """
526 new_room_jid=JID(self.room_jid.node,self.room_jid.domain,new_nick)
527 p=Presence(to_jid=new_room_jid)
528 self.manager.stream.send(p)
529
531 """
532 Get own room JID or a room JID for given `nick`.
533
534 :Parameters:
535 - `nick`: a nick for which the room JID is requested.
536 :Types:
537 - `nick`: `unicode`
538
539 :return: the room JID.
540 :returntype: `JID`
541 """
542 if nick is None:
543 return self.room_jid
544 return JID(self.room_jid.node,self.room_jid.domain,nick)
545
547 """
548 Get own nick.
549
550 :return: own nick.
551 :returntype: `unicode`
552 """
553 return self.room_jid.resource
554
556 """
557 Process <presence/> received from the room.
558
559 :Parameters:
560 - `stanza`: the stanza received.
561 :Types:
562 - `stanza`: `MucPresence`
563 """
564 fr=stanza.get_from()
565 if not fr.resource:
566 return
567 nick=fr.resource
568 user=self.users.get(nick)
569 if user:
570 old_user=MucRoomUser(user)
571 user.update_presence(stanza)
572 user.nick=nick
573 else:
574 old_user=None
575 user=MucRoomUser(stanza)
576 self.users[user.nick]=user
577 self.handler.presence_changed(user,stanza)
578 if fr==self.room_jid and not self.joined:
579 self.joined=True
580 self.me=user
581 mc=stanza.get_muc_child()
582 if isinstance(mc,MucUserX):
583 status = [i for i in mc.get_items() if isinstance(i,MucStatus) and i.code==201]
584 if status:
585 self.configured = False
586 self.handler.room_created(stanza)
587 if self.configured is None:
588 self.configured = True
589 if not old_user or old_user.role=="none":
590 self.handler.user_joined(user,stanza)
591 else:
592 if old_user.nick!=user.nick:
593 self.handler.nick_changed(user,old_user.nick,stanza)
594 if old_user.room_jid==self.room_jid:
595 self.room_jid=fr
596 if old_user.role!=user.role:
597 self.handler.role_changed(user,old_user.role,user.role,stanza)
598 if old_user.affiliation!=user.affiliation:
599 self.handler.affiliation_changed(user,old_user.affiliation,user.affiliation,stanza)
600
639
640
658
660 """
661 Process <message type="error"/> received from the room.
662
663 :Parameters:
664 - `stanza`: the stanza received.
665 :Types:
666 - `stanza`: `Message`
667 """
668 self.handler.error(stanza)
669
671 """
672 Process <presence type="error"/> received from the room.
673
674 :Parameters:
675 - `stanza`: the stanza received.
676 :Types:
677 - `stanza`: `Presence`
678 """
679 self.handler.error(stanza)
680
701
712
729
731 """
732 Process success response for a room configuration request.
733
734 :Parameters:
735 - `stanza`: the stanza received.
736 :Types:
737 - `stanza`: `Presence`
738 """
739 _unused = stanza
740 self.configured = True
741 self.handler.room_configured()
742
744 """
745 Process error response for a room configuration request.
746
747 :Parameters:
748 - `stanza`: the stanza received.
749 :Types:
750 - `stanza`: `Presence`
751 """
752 self.handler.room_configuration_error(stanza)
753
781
783 """
784 Request an "instant room" -- the default configuration for a MUC room.
785
786 :return: id of the request stanza.
787 :returntype: `unicode`
788 """
789 if self.configured:
790 raise RuntimeError, "Instant room may be requested for unconfigured room only"
791 form = Form("submit")
792 return self.configure_room(form)
793
795 """
796 Manage collection of MucRoomState objects and dispatch events.
797
798 :Ivariables:
799 - `rooms`: a dictionary containing known MUC rooms. Unicode room JIDs are the
800 keys.
801 - `stream`: the stream associated with the room manager.
802
803 """
805 """
806 Initialize a `MucRoomManager` object.
807
808 :Parameters:
809 - `stream`: a stream to be initially assigned to `self`.
810 :Types:
811 - `stream`: `pyxmpp.stream.Stream`
812 """
813 self.rooms={}
814 self.stream,self.jid=(None,)*2
815 self.set_stream(stream)
816 self.__logger=logging.getLogger("pyxmpp.jabber.MucRoomManager")
817
819 """
820 Change the stream assigned to `self`.
821
822 :Parameters:
823 - `stream`: the new stream to be assigned to `self`.
824 :Types:
825 - `stream`: `pyxmpp.stream.Stream`
826 """
827 self.jid=stream.me
828 self.stream=stream
829 for r in self.rooms.values():
830 r.set_stream(stream)
831
833 """
834 Assign MUC stanza handlers to the `self.stream`.
835
836 :Parameters:
837 - `priority`: priority for the handlers.
838 :Types:
839 - `priority`: `int`
840 """
841 self.stream.set_message_handler("groupchat",self.__groupchat_message,None,priority)
842 self.stream.set_message_handler("error",self.__error_message,None,priority)
843 self.stream.set_presence_handler("available",self.__presence_available,None,priority)
844 self.stream.set_presence_handler("unavailable",self.__presence_unavailable,None,priority)
845 self.stream.set_presence_handler("error",self.__presence_error,None,priority)
846
847 - def join(self, room, nick, handler, password = None, history_maxchars = None,
848 history_maxstanzas = None, history_seconds = None, history_since = None):
849 """
850 Create and return a new room state object and request joining
851 to a MUC room.
852
853 :Parameters:
854 - `room`: the name of a room to be joined
855 - `nick`: the nickname to be used in the room
856 - `handler`: is an object to handle room events.
857 - `password`: password for the room, if any
858 - `history_maxchars`: limit of the total number of characters in
859 history.
860 - `history_maxstanzas`: limit of the total number of messages in
861 history.
862 - `history_seconds`: send only messages received in the last
863 `history_seconds` seconds.
864 - `history_since`: Send only the messages received since the
865 dateTime specified (UTC).
866
867 :Types:
868 - `room`: `JID`
869 - `nick`: `unicode`
870 - `handler`: `MucRoomHandler`
871 - `password`: `unicode`
872 - `history_maxchars`: `int`
873 - `history_maxstanzas`: `int`
874 - `history_seconds`: `int`
875 - `history_since`: `datetime.datetime`
876
877 :return: the room state object created.
878 :returntype: `MucRoomState`
879 """
880
881 if not room.node or room.resource:
882 raise ValueError,"Invalid room JID"
883
884 room_jid = JID(room.node, room.domain, nick)
885
886 cur_rs = self.rooms.get(room_jid.bare().as_unicode())
887 if cur_rs and cur_rs.joined:
888 raise RuntimeError,"Room already joined"
889
890 rs=MucRoomState(self, self.stream.me, room_jid, handler)
891 self.rooms[room_jid.bare().as_unicode()]=rs
892 rs.join(password, history_maxchars, history_maxstanzas,
893 history_seconds, history_since)
894 return rs
895
897 """Get the room state object of a room.
898
899 :Parameters:
900 - `room`: JID or the room which state is requested.
901 :Types:
902 - `room`: `JID`
903
904 :return: the state object.
905 :returntype: `MucRoomState`"""
906 return self.rooms.get(room.bare().as_unicode())
907
909 """
910 Remove a room from the list of managed rooms.
911
912 :Parameters:
913 - `rs`: the state object of the room.
914 :Types:
915 - `rs`: `MucRoomState`
916 """
917 try:
918 del self.rooms[rs.room_jid.bare().as_unicode()]
919 except KeyError:
920 pass
921
923 """Process a groupchat message from a MUC room.
924
925 :Parameters:
926 - `stanza`: the stanza received.
927 :Types:
928 - `stanza`: `Message`
929
930 :return: `True` if the message was properly recognized as directed to
931 one of the managed rooms, `False` otherwise.
932 :returntype: `bool`"""
933 fr=stanza.get_from()
934 key=fr.bare().as_unicode()
935 rs=self.rooms.get(key)
936 if not rs:
937 self.__logger.debug("groupchat message from unknown source")
938 return False
939 rs.process_groupchat_message(stanza)
940 return True
941
943 """Process an error message from a MUC room.
944
945 :Parameters:
946 - `stanza`: the stanza received.
947 :Types:
948 - `stanza`: `Message`
949
950 :return: `True` if the message was properly recognized as directed to
951 one of the managed rooms, `False` otherwise.
952 :returntype: `bool`"""
953 fr=stanza.get_from()
954 key=fr.bare().as_unicode()
955 rs=self.rooms.get(key)
956 if not rs:
957 return False
958 rs.process_error_message(stanza)
959 return True
960
962 """Process an presence error from a MUC room.
963
964 :Parameters:
965 - `stanza`: the stanza received.
966 :Types:
967 - `stanza`: `Presence`
968
969 :return: `True` if the stanza was properly recognized as generated by
970 one of the managed rooms, `False` otherwise.
971 :returntype: `bool`"""
972 fr=stanza.get_from()
973 key=fr.bare().as_unicode()
974 rs=self.rooms.get(key)
975 if not rs:
976 return False
977 rs.process_error_presence(stanza)
978 return True
979
981 """Process an available presence from a MUC room.
982
983 :Parameters:
984 - `stanza`: the stanza received.
985 :Types:
986 - `stanza`: `Presence`
987
988 :return: `True` if the stanza was properly recognized as generated by
989 one of the managed rooms, `False` otherwise.
990 :returntype: `bool`"""
991 fr=stanza.get_from()
992 key=fr.bare().as_unicode()
993 rs=self.rooms.get(key)
994 if not rs:
995 return False
996 rs.process_available_presence(MucPresence(stanza))
997 return True
998
1000 """Process an unavailable presence from a MUC room.
1001
1002 :Parameters:
1003 - `stanza`: the stanza received.
1004 :Types:
1005 - `stanza`: `Presence`
1006
1007 :return: `True` if the stanza was properly recognized as generated by
1008 one of the managed rooms, `False` otherwise.
1009 :returntype: `bool`"""
1010 fr=stanza.get_from()
1011 key=fr.bare().as_unicode()
1012 rs=self.rooms.get(key)
1013 if not rs:
1014 return False
1015 rs.process_unavailable_presence(MucPresence(stanza))
1016 return True
1017
1018
1019