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