Package pyxmpp :: Module roster
[hide private]

Source Code for Module pyxmpp.roster

  1  # 
  2  # (C) Copyright 2003-2010 Jacek Konieczny <jajcus@jajcus.net> 
  3  # 
  4  # This program is free software; you can redistribute it and/or modify 
  5  # it under the terms of the GNU Lesser General Public License Version 
  6  # 2.1 as published by the Free Software Foundation. 
  7  # 
  8  # This program is distributed in the hope that it will be useful, 
  9  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 10  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 11  # GNU Lesser General Public License for more details. 
 12  # 
 13  # You should have received a copy of the GNU Lesser General Public 
 14  # License along with this program; if not, write to the Free Software 
 15  # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 
 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   
37 -class RosterItem(StanzaPayloadObject):
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
72 - def from_xml(self,node):
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
107 - def complete_xml_element(self, xmlnode, _unused):
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
128 - def __str__(self):
129 n=self.as_xml(doc=common_doc) 130 r=n.serialize() 131 n.unlinkNode() 132 n.freeNode() 133 return r
134
135 - def make_roster_push(self):
136 """ 137 Make "roster push" IQ stanza from the item representing roster update 138 request. 139 """ 140 iq=Iq(stanza_type="set") 141 q=iq.new_query(ROSTER_NS) 142 self.as_xml(parent=q, doc=common_doc) 143 return iq
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
183 - def from_xml(self,node,strict=True):
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
212 - def complete_xml_element(self, xmlnode, doc):
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
227 - def __str__(self):
228 n=self.as_xml(doc=common_doc) 229 r=n.serialize() 230 n.unlinkNode() 231 n.freeNode() 232 return r
233
234 - def __iter__(self):
235 return self.items_dict.itervalues()
236
237 - def __contains__(self, jid):
238 return jid in self.items_dict
239
240 - def __getitem__(self, jid):
241 return self.items_dict[jid]
242
243 - def get_items(self):
244 """Return a list of items in the roster.""" 245 return self.items_dict.values()
246 247 items = property(get_items) 248
249 - def get_groups(self):
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
261 - def get_items_by_name(self, name, case_sensitive = True):
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
279 - def get_items_by_group(self,group,case_sensitive=True):
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
301 - def get_item_by_jid(self,jid):
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
334 - def remove_item(self,jid):
335 """Remove item from the roster.""" 336 del self.items_dict[jid] 337 return RosterItem(jid,"remove")
338
339 - def update(self,query):
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 # vi: sts=4 et sw=4 376