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