pyxmpp.roster

Package pyxmpp :: Module roster |
[hide private]

# Source Code for Module pyxmpp.roster

      1  #
      2  # (C) Copyright 2003-2006 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  __revision__="$Id: roster.py 647 2006-08-26 18:27:39Z 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
     38 -class RosterItem(StanzaPayloadObject):
     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
     73 -    def from_xml(self,node):
     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
    108 -    def complete_xml_element(self, xmlnode, _unused):
    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
    129 -    def __str__(self):
    130          n=self.as_xml(doc=common_doc)
    131          r=n.serialize()
    132          n.unlinkNode()
    133          n.freeNode()
    134          return r
    135
    136 -    def make_roster_push(self):
    137          """
    138          Make "roster push" IQ stanza from the item representing roster update
    139          request.
    140          """
    141          iq=Iq(stanza_type="set")
    142          q=iq.new_query(ROSTER_NS)
    143          self.as_xml(parent=q, doc=common_doc)
    144          return iq
    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
    184 -    def from_xml(self,node,strict=True):
    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
    213 -    def complete_xml_element(self, xmlnode, doc):
    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
    228 -    def __str__(self):
    229          n=self.as_xml(doc=common_doc)
    230          r=n.serialize()
    231          n.unlinkNode()
    232          n.freeNode()
    233          return r
    234
    235 -    def __iter__(self):
    236          return self.items_dict.itervalues()
    237
    238 -    def __contains__(self, jid):
    239          return jid in self.items_dict
    240
    241 -    def __getitem__(self, jid):
    242          return self.items_dict[jid]
    243
    244 -    def get_items(self):
    245          """Return a list of items in the roster."""
    246          return self.items_dict.values()
    247
    248      items = property(get_items)
    249
    250 -    def get_groups(self):
    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
    262 -    def get_items_by_name(self, name, case_sensitive = True):
    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
    280 -    def get_items_by_group(self,group,case_sensitive=True):
    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
    302 -    def get_item_by_jid(self,jid):
    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
    335 -    def remove_item(self,jid):
    336          """Remove item from the roster."""
    337          del self.items_dict[jid]
    338          return RosterItem(jid,"remove")
    339
    340 -    def update(self,query):
    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  # vi: sts=4 et sw=4
    377