1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 """XMPP-IM roster handling.
19
20 Normative reference:
21 - `RFC 3921 <http://www.ietf.org/rfc/rfc3921.txt>`__
22 """
23
24 __revision__="$Id: roster.py 714 2010-04-05 10:20:10Z jajcus $"
25 __docformat__="restructuredtext en"
26
27 import libxml2
28
29 from pyxmpp.xmlextra import common_doc, get_node_ns_uri
30 from pyxmpp.iq import Iq
31 from pyxmpp.jid import JID
32
33 from pyxmpp.utils import to_utf8,from_utf8
34 from pyxmpp.objects import StanzaPayloadObject
35
36 ROSTER_NS="jabber:iq:roster"
37
39 """
40 Roster item.
41
42 Represents part of a roster, or roster update request.
43 """
44
45 xml_element_name = "item"
46 xml_element_namespace = ROSTER_NS
47
48 - def __init__(self,node_or_jid,subscription="none",name=None,groups=(),ask=None):
49 """
50 Initialize a roster item from XML node or jid and optional attributes.
51
52 :Parameters:
53 - `node_or_jid`: XML node or JID
54 - `subscription`: subscription type ("none", "to", "from" or "both"
55 - `name`: item visible name
56 - `groups`: sequence of groups the item is member of
57 - `ask`: True if there was unreplied subsription or unsubscription
58 request sent."""
59 if isinstance(node_or_jid,libxml2.xmlNode):
60 self.from_xml(node_or_jid)
61 else:
62 node_or_jid=JID(node_or_jid)
63 if subscription not in ("none","from","to","both","remove"):
64 raise ValueError,"Bad subscription type: %r" % (subscription,)
65 if ask not in ("subscribe",None):
66 raise ValueError,"Bad ask type: %r" % (ask,)
67 self.jid=node_or_jid
68 self.ask=ask
69 self.subscription=subscription
70 self.name=name
71 self.groups=list(groups)
72
74 """Initialize RosterItem from XML node."""
75 if node.type!="element":
76 raise ValueError,"XML node is not a roster item (not en element)"
77 ns=get_node_ns_uri(node)
78 if ns and ns!=ROSTER_NS or node.name!="item":
79 raise ValueError,"XML node is not a roster item"
80 jid=JID(node.prop("jid").decode("utf-8"))
81 subscription=node.prop("subscription")
82 if subscription not in ("none","from","to","both","remove"):
83 subscription="none"
84 ask=node.prop("ask")
85 if ask not in ("subscribe",None):
86 ask=None
87 name=from_utf8(node.prop("name"))
88 groups=[]
89 n=node.children
90 while n:
91 if n.type!="element":
92 n=n.next
93 continue
94 ns=get_node_ns_uri(n)
95 if ns and ns!=ROSTER_NS or n.name!="group":
96 n=n.next
97 continue
98 group=n.getContent()
99 if group:
100 groups.append(from_utf8(group))
101 n=n.next
102 self.jid=jid
103 self.name=name
104 self.groups=groups
105 self.subscription=subscription
106 self.ask=ask
107
109 """Complete the XML node with `self` content.
110
111 Should be overriden in classes derived from `StanzaPayloadObject`.
112
113 :Parameters:
114 - `xmlnode`: XML node with the element being built. It has already
115 right name and namespace, but no attributes or content.
116 - `_unused`: document to which the element belongs.
117 :Types:
118 - `xmlnode`: `libxml2.xmlNode`
119 - `_unused`: `libxml2.xmlDoc`"""
120 xmlnode.setProp("jid",self.jid.as_utf8())
121 if self.name:
122 xmlnode.setProp("name",to_utf8(self.name))
123 xmlnode.setProp("subscription",self.subscription)
124 if self.ask:
125 xmlnode.setProp("ask",to_utf8(self.ask))
126 for g in self.groups:
127 xmlnode.newTextChild(None, "group", to_utf8(g))
128
135
145
146 -class Roster(StanzaPayloadObject):
147 """Class representing XMPP-IM roster.
148
149 Iteration over `Roster` object iterates over roster items.
150
151 ``for item in roster: ...`` may be used to iterate over roster items,
152 ``roster[jid]`` to get roster item by jid, ``jid in roster`` to test roster
153 for jid presence.
154
155 :Ivariables:
156 - `items_dict`: items indexed by JID.
157 :Properties:
158 - `items`: roster items.
159 :Types:
160 - `items_dict`: `dict` of `JID` -> `RosterItem`
161 - `items`: `list` of `RosterItem`"""
162
163 xml_element_name = "query"
164 xml_element_namespace = ROSTER_NS
165
166 - def __init__(self,node=None,server=False,strict=True):
167 """
168 Initialize Roster object.
169
170 `node` should be an XML representation of the roster (e.g. as sent
171 from server in response to roster request). When `node` is None empty
172 roster will be created.
173
174 If `server` is true the object is considered server-side roster.
175
176 If `strict` is False, than invalid items in the XML will be ignored.
177 """
178 self.items_dict={}
179 self.server=server
180 self.node=None
181 if node:
182 self.from_xml(node,strict)
183
185 """
186 Initialize Roster object from XML node.
187
188 If `strict` is False, than invalid items in the XML will be ignored.
189 """
190 self.items_dict={}
191 if node.type!="element":
192 raise ValueError,"XML node is not a roster (not en element)"
193 ns=get_node_ns_uri(node)
194 if ns and ns!=ROSTER_NS or node.name!="query":
195 raise ValueError,"XML node is not a roster"
196 n=node.children
197 while n:
198 if n.type!="element":
199 n=n.next
200 continue
201 ns=get_node_ns_uri(n)
202 if ns and ns!=ROSTER_NS or n.name!="item":
203 n=n.next
204 continue
205 try:
206 item=RosterItem(n)
207 self.items_dict[item.jid]=item
208 except ValueError:
209 if strict:
210 raise
211 n=n.next
212
214 """Complete the XML node with `self` content.
215
216 Should be overriden in classes derived from `StanzaPayloadObject`.
217
218 :Parameters:
219 - `xmlnode`: XML node with the element being built. It has already
220 right name and namespace, but no attributes or content.
221 - `doc`: document to which the element belongs.
222 :Types:
223 - `xmlnode`: `libxml2.xmlNode`
224 - `doc`: `libxml2.xmlDoc`"""
225 for it in self.items_dict.values():
226 it.as_xml(parent=xmlnode, doc=doc)
227
234
236 return self.items_dict.itervalues()
237
239 return jid in self.items_dict
240
242 return self.items_dict[jid]
243
245 """Return a list of items in the roster."""
246 return self.items_dict.values()
247
248 items = property(get_items)
249
251 """Return a list of groups in the roster."""
252 r={}
253 for it in self.items_dict.values():
254 it.groups=[g for g in it.groups if g]
255 if it.groups:
256 for g in it.groups:
257 r[g]=True
258 else:
259 r[None]=True
260 return r.keys()
261
263 """
264 Return a list of items with given `name`.
265
266 If `case_sensitive` is False the matching will be case insensitive.
267 """
268 if not case_sensitive and name:
269 name = name.lower()
270 r = []
271 for it in self.items_dict.values():
272 if it.name == name:
273 r.append(it)
274 elif it.name is None:
275 continue
276 elif not case_sensitive and it.name.lower() == name:
277 r.append(it)
278 return r
279
281 """
282 Return a list of groups with given name.
283
284 If `case_sensitive` is False the matching will be case insensitive.
285 """
286 r=[]
287 if not group:
288 for it in self.items_dict.values():
289 it.groups=[g for g in it.groups if g]
290 if not it.groups:
291 r.append(it)
292 return r
293 if not case_sensitive:
294 group=group.lower()
295 for it in self.items_dict.values():
296 if group in it.groups:
297 r.append(it)
298 elif not case_sensitive and group in [g.lower() for g in it.groups]:
299 r.append(it)
300 return r
301
303 """
304 Return roster item with given `jid`.
305
306 :raise KeyError: if the item is not found.
307 """
308 if not jid:
309 raise ValueError,"jid is None"
310 return self.items_dict[jid]
311
312 - def add_item(self,item_or_jid,subscription="none",name=None,groups=(),ask=None):
313 """
314 Add an item to the roster.
315
316 The `item_or_jid` argument may be a `RosterItem` object or a `JID`. If
317 it is a JID then `subscription`, `name`, `groups` and `ask` may also be
318 specified.
319 """
320 if isinstance(item_or_jid,RosterItem):
321 item=item_or_jid
322 if self.items_dict.has_key(item.jid):
323 raise ValueError,"Item already exists"
324 else:
325 if self.items_dict.has_key(item_or_jid):
326 raise ValueError,"Item already exists"
327 if not self.server or subscription not in ("none","from","to","both"):
328 subscription="none"
329 if not self.server:
330 ask=None
331 item=RosterItem(item_or_jid,subscription,name,groups,ask)
332 self.items_dict[item.jid]=item
333 return item
334
336 """Remove item from the roster."""
337 del self.items_dict[jid]
338 return RosterItem(jid,"remove")
339
341 """
342 Apply an update request to the roster.
343
344 `query` should be a query included in a "roster push" IQ received.
345 """
346 ctxt=common_doc.xpathNewContext()
347 ctxt.setContextNode(query)
348 ctxt.xpathRegisterNs("r",ROSTER_NS)
349 item=ctxt.xpathEval("r:item")
350 ctxt.xpathFreeContext()
351 if not item:
352 raise ValueError,"No item to update"
353 item=item[0]
354 item=RosterItem(item)
355 jid=item.jid
356 subscription=item.subscription
357 try:
358 local_item=self.get_item_by_jid(jid)
359 local_item.subscription=subscription
360 except KeyError:
361 if subscription=="remove":
362 return RosterItem(jid,"remove")
363 if self.server or subscription not in ("none","from","to","both"):
364 subscription="none"
365 local_item=RosterItem(jid,subscription)
366 if subscription=="remove":
367 del self.items_dict[local_item.jid]
368 return RosterItem(jid,"remove")
369 local_item.name=item.name
370 local_item.groups=list(item.groups)
371 if not self.server:
372 local_item.ask=item.ask
373 self.items_dict[local_item.jid]=local_item
374 return local_item
375
376
377