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