Page MenuHomePhabricator

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/sipsimple/account/xcap/__init__.py b/sipsimple/account/xcap/__init__.py
index 49cb9a6f..e94fa071 100644
--- a/sipsimple/account/xcap/__init__.py
+++ b/sipsimple/account/xcap/__init__.py
@@ -1,3561 +1,3561 @@
# Copyright (C) 2010-2011 AG Projects. See LICENSE for details.
#
"""High-level management of XCAP documents based on OMA specifications"""
__all__ = ['Contact', 'Service', 'CatchAllCondition', 'DomainCondition', 'DomainExcepton', 'UserException', 'Policy', 'Class', 'OccurenceID', 'DeviceID',
'ServiceURI', 'ServiceURIScheme', 'PresencePolicy', 'DialoginfoPolicy', 'FallbackPolicies', 'Icon', 'OfflineStatus', 'XCAPManager', 'XCAPTransaction']
import base64
import cPickle
import os
import random
import re
import string
import weakref
from cStringIO import StringIO
from collections import deque
from copy import deepcopy
from datetime import datetime
from itertools import chain, count
from time import time
from urllib2 import URLError
from application import log
from application.notification import IObserver, NotificationCenter
from application.python import Null, limit
from application.python.types import NullType
from eventlet import api, coros, proc
from eventlet.green.httplib import BadStatusLine
from twisted.internet.error import ConnectionLost
from xcaplib.green import XCAPClient
from xcaplib.error import HTTPError
from zope.interface import implements
from sipsimple.account.xcap.storage import IXCAPStorage, XCAPStorageError
from sipsimple.account.xcap.uri import XCAPURI
from sipsimple.configuration.settings import SIPSimpleSettings
from sipsimple.core import ContactHeader, FromHeader, PJSIPError, RouteHeader, ToHeader, SIPCoreError, SIPURI, Subscription
from sipsimple.lookup import DNSLookup, DNSLookupError
from sipsimple.payloads import ParserError
from sipsimple.payloads import dialogrules, omapolicy, pidf, policy as common_policy, prescontent, presrules, resourcelists, rlsservices, rpid, xcapcaps, xcapdiff
from sipsimple.threading import run_in_twisted_thread
from sipsimple.threading.green import Command
from sipsimple.util import All, Any, TimestampedNotificationData
class XCAPError(Exception): pass
class FetchRequiredError(XCAPError): pass
class SubscriptionError(Exception):
def __init__(self, error, timeout, refresh_interval=None):
self.error = error
self.refresh_interval = refresh_interval
self.timeout = timeout
class SIPSubscriptionDidFail(Exception):
def __init__(self, data):
self.data = data
class InterruptSubscription(Exception): pass
class TerminateSubscription(Exception): pass
class Document(object):
name = None
payload_type = None
application = None
global_tree = None
filename = None
cached = True
def __init__(self, manager):
self.manager = weakref.proxy(manager)
self.content = None
self.etag = None
self.fetch_time = datetime.fromtimestamp(0)
self.dirty = False
self.supported = False
@property
def relative_uri(self):
uri = self.uri[len(self.manager.xcap_root):]
if uri.startswith('/'):
uri = uri[1:]
return uri
@property
def uri(self):
return self.manager.client.get_url(self.application, None, globaltree=self.global_tree, filename=self.filename)
def load_from_cache(self):
if not self.cached:
return
try:
document = StringIO(self.manager.storage.load(self.name))
self.etag = document.readline().strip() or None
self.content = self.payload_type.parse(document)
except (XCAPStorageError, ParserError):
self.etag = None
self.content = None
def initialize(self, server_caps):
self.supported = self.application in server_caps.auids
def reset(self):
self.content = None
self.etag = None
def fetch(self):
try:
document = self.manager.client.get(self.application, etagnot=self.etag, globaltree=self.global_tree, headers={'Accept': self.payload_type.content_type}, filename=self.filename)
self.content = self.payload_type.parse(document)
self.etag = document.etag
except (BadStatusLine, ConnectionLost, URLError), e:
raise XCAPError("failed to fetch %s document: %s" % (self.name, e))
except HTTPError, e:
if e.status == 404: # Not Found
self.reset()
self.fetch_time = datetime.utcnow()
if self.cached:
try:
self.manager.storage.delete(self.name)
except XCAPStorageError:
pass
elif e.status != 304: # Other than Not Modified:
raise XCAPError("failed to fetch %s document: %s" % (self.name, e))
except ParserError, e:
raise XCAPError("failed to parse %s document: %s" % (self.name, e))
else:
self.fetch_time = datetime.utcnow()
if self.cached:
try:
self.manager.storage.save(self.name, (self.etag or '') + os.linesep + document)
except XCAPStorageError:
pass
def update(self):
if not self.dirty:
return
data = self.content.toxml() if self.content is not None else None
try:
kw = dict(etag=self.etag) if self.etag is not None else dict(etagnot='*')
if data is not None:
response = self.manager.client.put(self.application, data, globaltree=self.global_tree, filename=self.filename, headers={'Content-Type': self.payload_type.content_type}, **kw)
else:
response = self.manager.client.delete(self.application, data, globaltree=self.global_tree, filename=self.filename, **kw)
except (BadStatusLine, ConnectionLost, URLError), e:
raise XCAPError("failed to update %s document: %s" % (self.name, e))
except HTTPError, e:
if e.status == 412: # Precondition Failed
raise FetchRequiredError("document %s was modified externally" % self.name)
elif e.status == 404 and data is None: # attempted to delete a document that did't exist in the first place
pass
else:
raise XCAPError("failed to update %s document: %s" % (self.name, e))
self.etag = response.etag if data is not None else None
self.dirty = False
if self.cached:
try:
if data is not None:
self.manager.storage.save(self.name, self.etag + os.linesep + data)
else:
self.manager.storage.delete(self.name)
except XCAPStorageError:
pass
class DialogRulesDocument(Document):
name = 'dialog-rules'
- payload_type = dialogrules.DialogRules
+ payload_type = dialogrules.DialogRulesDocument
application = 'org.openxcap.dialog-rules'
global_tree = False
filename = 'index'
class PresRulesDocument(Document):
name = 'pres-rules'
- payload_type = presrules.PresRules
+ payload_type = presrules.PresRulesDocument
application = 'pres-rules'
global_tree = False
filename = 'index'
def initialize(self, server_caps):
self.application = 'org.openmobilealliance.pres-rules' if 'org.openmobilealliance.pres-rules' in server_caps.auids else 'pres-rules'
super(PresRulesDocument, self).initialize(server_caps)
class ResourceListsDocument(Document):
name = 'resource-lists'
- payload_type = resourcelists.ResourceLists
+ payload_type = resourcelists.ResourceListsDocument
application = 'resource-lists'
global_tree = False
filename = 'index'
class RLSServicesDocument(Document):
name = 'rls-services'
- payload_type = rlsservices.RLSServices
+ payload_type = rlsservices.RLSServicesDocument
application = 'rls-services'
global_tree = False
filename = 'index'
class XCAPCapsDocument(Document):
name = 'xcap-caps'
- payload_type = xcapcaps.XCAPCapabilities
+ payload_type = xcapcaps.XCAPCapabilitiesDocument
application = 'xcap-caps'
global_tree = True
filename = 'index'
cached = False
def initialize(self):
self.supported = True
class StatusIconDocument(Document):
name = 'status-icon'
- payload_type = prescontent.PresenceContent
+ payload_type = prescontent.PresenceContentDocument
application = 'org.openmobilealliance.pres-content'
global_tree = False
filename = 'oma_status-icon/index'
def __init__(self, manager):
super(StatusIconDocument, self).__init__(manager)
self.alternative_location = None
def fetch(self):
try:
document = self.manager.client.get(self.application, etagnot=self.etag, globaltree=self.global_tree, headers={'Accept': self.payload_type.content_type}, filename=self.filename)
self.content = self.payload_type.parse(document)
self.etag = document.etag
except (BadStatusLine, ConnectionLost, URLError), e:
raise XCAPError("failed to fetch %s document: %s" % (self.name, e))
except HTTPError, e:
if e.status == 404: # Not Found
self.reset()
self.fetch_time = datetime.utcnow()
if self.cached:
try:
self.manager.storage.delete(self.name)
except XCAPStorageError:
pass
elif e.status == 304: # Not Modified:
self.alternative_location = e.headers.get('X-AGP-Alternative-Location', None)
else:
raise XCAPError("failed to fetch %s document: %s" % (self.name, e))
except ParserError, e:
raise XCAPError("failed to parse %s document: %s" % (self.name, e))
else:
self.fetch_time = datetime.utcnow()
self.alternative_location = e.headers.get('X-AGP-Alternative-Location', None)
if self.cached:
try:
self.manager.storage.save(self.name, (self.etag or '') + os.linesep + document)
except XCAPStorageError:
pass
def update(self):
if not self.dirty:
return
data = self.content.toxml() if self.content is not None else None
try:
kw = dict(etag=self.etag) if self.etag is not None else dict(etagnot='*')
if data is not None:
response = self.manager.client.put(self.application, data, globaltree=self.global_tree, filename=self.filename, headers={'Content-Type': self.payload_type.content_type}, **kw)
else:
response = self.manager.client.delete(self.application, data, globaltree=self.global_tree, filename=self.filename, **kw)
except (BadStatusLine, ConnectionLost, URLError), e:
raise XCAPError("failed to update %s document: %s" % (self.name, e))
except HTTPError, e:
if e.status == 412: # Precondition Failed
raise FetchRequiredError("document %s was modified externally" % self.name)
elif e.status == 404 and data is None: # attempted to delete a document that did't exist in the first place
pass
else:
raise XCAPError("failed to update %s document: %s" % (self.name, e))
self.alternative_location = response.headers.get('X-AGP-Alternative-Location', None)
self.etag = response.etag if data is not None else None
self.dirty = False
if self.cached:
try:
if data is not None:
self.manager.storage.save(self.name, self.etag + os.linesep + data)
else:
self.manager.storage.delete(self.name)
except XCAPStorageError:
pass
def reset(self):
super(StatusIconDocument, self).reset()
self.alternative_location = None
class PIDFManipulationDocument(Document):
name = 'pidf-manipulation'
- payload_type = pidf.PIDF
+ payload_type = pidf.PIDFDocument
application = 'pidf-manipulation'
global_tree = False
filename = 'index'
class Contact(object):
def __init__(self, name, uri, group, **attributes):
self.name = name
self.uri = uri
self.group = group
self.attributes = attributes
self.presence_policies = []
self.dialoginfo_policies = []
self.subscribe_to_presence = True
self.subscribe_to_dialoginfo = True
def __eq__(self, other):
if isinstance(other, Contact):
return (self.name == other.name and self.uri == other.uri and self.group == other.group and self.attributes == other.attributes and
self.presence_policies == other.presence_policies and self.dialoginfo_policies == other.dialoginfo_policies and
self.subscribe_to_presence == other.subscribe_to_presence and self.subscribe_to_dialoginfo == other.subscribe_to_dialoginfo)
return NotImplemented
def __ne__(self, other):
equal = self.__eq__(other)
return NotImplemented if equal is NotImplemented else not equal
class Service(object):
def __init__(self, uri, packages, entries=None):
self.uri = uri
self.packages = packages
self.entries = entries or []
class CatchAllCondition(object):
def __init__(self, exceptions=None):
self.exceptions = exceptions or []
def __eq__(self, other):
if isinstance(other, CatchAllCondition):
return self.exceptions == other.exceptions
return NotImplemented
def __ne__(self, other):
equal = self.__eq__(other)
return NotImplemented if equal is NotImplemented else not equal
class DomainCondition(object):
def __init__(self, domain, exceptions=None):
self.domain = domain
self.exceptions = exceptions or []
def __eq__(self, other):
if isinstance(other, DomainCondition):
return self.domain == other.domain and self.exceptions == other.exceptions
return NotImplemented
def __ne__(self, other):
equal = self.__eq__(other)
return NotImplemented if equal is NotImplemented else not equal
class DomainException(object):
def __init__(self, domain):
self.domain = domain
def __eq__(self, other):
if isinstance(other, DomainException):
return self.domain == other.domain
return NotImplemented
def __ne__(self, other):
equal = self.__eq__(other)
return NotImplemented if equal is NotImplemented else not equal
class UserException(object):
def __init__(self, uri):
self.uri = uri
def __eq__(self, other):
if isinstance(other, UserException):
return self.uri == other.uri
return NotImplemented
def __ne__(self, other):
equal = self.__eq__(other)
return NotImplemented if equal is NotImplemented else not equal
class Policy(object):
def __init__(self, id, action, name=None, validity=None, sphere=None, multi_identity_conditions=None):
self.id = id
self.action = action
self.name = name
self.validity = validity
self.sphere = sphere
self.multi_identity_conditions = multi_identity_conditions
def check_validity(self, timestamp, sphere=Any):
if sphere is not Any and sphere != self.sphere:
return False
if self.validity is None:
return True
for from_timestamp, until_timestamp in self.validity:
if from_timestamp <= timestamp <= until_timestamp:
return True
else:
return False
def __eq__(self, other):
if isinstance(other, Policy):
return (self.id == other.id and self.action == other.action and self.name == other.name and self.validity == other.validity and
self.sphere == other.sphere and self.multi_identity_conditions == other.multi_identity_conditions)
return NotImplemented
def __ne__(self, other):
equal = self.__eq__(other)
return NotImplemented if equal is NotImplemented else not equal
# elements to represent provide_(devices|persons|services) components for pres-rules
class Class(unicode): pass
class OccurenceID(unicode): pass
class DeviceID(unicode): pass
class ServiceURI(unicode): pass
class ServiceURIScheme(unicode): pass
class PresencePolicy(Policy):
def __init__(self, id, action, name=None, validity=None, sphere=None, multi_identity_conditions=None):
super(PresencePolicy, self).__init__(id, action, name, validity, sphere, multi_identity_conditions)
self.provide_devices = All
self.provide_persons = All
self.provide_services = All
self.provide_activities = None
self.provide_class = None
self.provide_device_id = None
self.provide_mood = None
self.provide_place_is = None
self.provide_place_type = None
self.provide_privacy = None
self.provide_relationship = None
self.provide_status_icon = None
self.provide_sphere = None
self.provide_time_offset = None
self.provide_user_input = None
self.provide_unknown_attributes = None
self.provide_all_attributes = True
def __eq__(self, other):
if isinstance(other, PresencePolicy):
return (super(PresencePolicy, self).__eq__(other) and self.provide_devices == other.provide_devices and
self.provide_persons == other.provide_persons and self.provide_services == other.provide_services and
self.provide_activities == other.provide_activities and self.provide_class == other.provide_class and
self.provide_device_id == other.provide_device_id and self.provide_mood == other.provide_mood and
self.provide_place_is == other.provide_place_is and self.provide_place_type == other.provide_place_type and
self.provide_privacy == other.provide_privacy and self.provide_relationship == other.provide_relationship and
self.provide_status_icon == other.provide_status_icon and self.provide_sphere == other.provide_sphere and
self.provide_time_offset == other.provide_time_offset and self.provide_user_input == other.provide_user_input and
self.provide_unknown_attributes == other.provide_unknown_attributes and self.provide_all_attributes == other.provide_all_attributes)
elif isinstance(other, Policy):
return super(PresencePolicy, self).__eq__(other)
else:
return NotImplemented
def __ne__(self, other):
equal = self.__eq__(other)
return NotImplemented if equal is NotImplemented else not equal
class DialoginfoPolicy(Policy):
pass
class FallbackPoliciesType(NullType):
__name__ = 'FallbackPolicies'
FallbackPolicies = FallbackPoliciesType()
class Icon(object):
def __init__(self, data, mime_type, description=None, location=None):
self.data = data
self.mime_type = mime_type
self.description = description
self.location = location
class OfflineStatus(object):
def __init__(self, activity=None, note=None):
self.activity = activity
self.note = note
class Operation(object):
name = None
documents = []
params = []
def __init__(self, **params):
self.__dict__.update(dict.fromkeys(self.params))
self.__dict__.update(params)
self.applied = False
self.timestamp = datetime.utcnow()
class NormalizeOperation(Operation):
name = 'normalize'
documents = ['dialog-rules', 'pres-rules', 'resource-lists', 'rls-services']
params = []
class AddGroupOperation(Operation):
name = 'add_group'
documents = ['resource-lists']
params = ['group']
class RenameGroupOperation(Operation):
name = 'rename_group'
documents = ['resource-lists']
params = ['old_name', 'new_name']
class RemoveGroupOperation(Operation):
name = 'remove_group'
documents = ['resource-lists']
params = ['group']
class AddContactOperation(Operation):
name = 'add_contact'
documents = ['dialog-rules', 'pres-rules', 'resource-lists', 'rls-services']
params = ['contact']
class UpdateContactOperation(Operation):
name = 'update_contact'
documents = ['dialog-rules', 'pres-rules', 'resource-lists', 'rls-services']
params = ['contact', 'attributes']
class RemoveContactOperation(Operation):
name = 'remove_contact'
documents = ['dialog-rules', 'pres-rules', 'resource-lists', 'rls-services']
params = ['contact']
class AddPresencePolicyOperation(Operation):
name = 'add_presence_policy'
documents = ['pres-rules']
params = ['policy']
class UpdatePresencePolicyOperation(Operation):
name = 'update_presence_policy'
documents = ['pres-rules']
params = ['policy', 'attributes']
class RemovePresencePolicyOperation(Operation):
name = 'remove_presence_policy'
documents = ['pres-rules']
params = ['policy']
class AddDialoginfoPolicyOperation(Operation):
name = 'add_dialoginfo_policy'
documents = ['pres-rules']
params = ['policy']
class UpdateDialoginfoPolicyOperation(Operation):
name = 'update_dialoginfo_policy'
documents = ['pres-rules']
params = ['policy', 'attributes']
class RemoveDialoginfoPolicyOperation(Operation):
name = 'remove_dialoginfo_policy'
documents = ['pres-rules']
params = ['policy']
class SetStatusIconOperation(Operation):
name = 'set_status_icon'
documents = ['status-icon']
params = ['icon']
class SetOfflineStatusOperation(Operation):
name = 'set_offline_status'
documents = ['pidf-manipulation']
params = ['status']
class XCAPSubscriber(object):
implements(IObserver)
def __init__(self, account, documents):
self.account = account
self.documents = documents
self.active = False
self.subscribed = False
self._command_proc = None
self._command_channel = coros.queue()
self._data_channel = coros.queue()
self._subscription = None
self._subscription_proc = None
self._subscription_timer = None
self._wakeup_timer = None
def start(self):
notification_center = NotificationCenter()
notification_center.add_observer(self, name='DNSNameserversDidChange')
notification_center.add_observer(self, name='SystemIPAddressDidChange')
notification_center.add_observer(self, name='SystemDidWakeUpFromSleep')
self._command_proc = proc.spawn(self._run)
def stop(self):
notification_center = NotificationCenter()
notification_center.remove_observer(self, name='DNSNameserversDidChange')
notification_center.remove_observer(self, name='SystemIPAddressDidChange')
notification_center.remove_observer(self, name='SystemDidWakeUpFromSleep')
self._command_proc.kill()
self._command_proc = None
def activate(self):
self.active = True
command = Command('subscribe')
self._command_channel.send(command)
def deactivate(self):
self.active = False
command = Command('unsubscribe')
self._command_channel.send(command)
command.wait()
def resubscribe(self):
command = Command('subscribe')
self._command_channel.send(command)
def _run(self):
while True:
command = self._command_channel.wait()
handler = getattr(self, '_CH_%s' % command.name)
handler(command)
def _CH_subscribe(self, command):
if self._subscription_timer is not None and self._subscription_timer.active():
self._subscription_timer.cancel()
self._subscription_timer = None
if self._subscription_proc is not None:
subscription_proc = self._subscription_proc
subscription_proc.kill(InterruptSubscription)
subscription_proc.wait()
self._subscription_proc = proc.spawn(self._subscription_handler, command)
def _CH_unsubscribe(self, command):
# Cancel any timer which would restart the subscription process
if self._subscription_timer is not None and self._subscription_timer.active():
self._subscription_timer.cancel()
self._subscription_timer = None
if self._wakeup_timer is not None and self._wakeup_timer.active():
self._wakeup_timer.cancel()
self._wakeup_timer = None
if self._subscription_proc is not None:
subscription_proc = self._subscription_proc
subscription_proc.kill(TerminateSubscription)
subscription_proc.wait()
self._subscription_proc = None
command.signal()
def _subscription_handler(self, command):
notification_center = NotificationCenter()
settings = SIPSimpleSettings()
refresh_interval = getattr(command, 'refresh_interval', None) or self.account.sip.subscribe_interval
try:
if self.account.sip.outbound_proxy is not None:
uri = SIPURI(host=self.account.sip.outbound_proxy.host,
port=self.account.sip.outbound_proxy.port,
parameters={'transport': self.account.sip.outbound_proxy.transport})
else:
uri = SIPURI(host=self.account.id.domain)
lookup = DNSLookup()
try:
routes = lookup.lookup_sip_proxy(uri, settings.sip.transport_list).wait()
except DNSLookupError, e:
timeout = random.uniform(15, 30)
raise SubscriptionError(error='DNS lookup failed: %s' % e, timeout=timeout)
rlist = resourcelists.List()
for document in (doc for doc in self.documents if doc.supported):
rlist.add(resourcelists.Entry(document.relative_uri))
body = resourcelists.ResourceLists([rlist]).toxml()
- content_type = resourcelists.ResourceLists.content_type
+ content_type = resourcelists.ResourceListsDocument.content_type
timeout = time() + 30
for route in routes:
remaining_time = timeout - time()
if remaining_time > 0:
try:
contact_uri = self.account.contact[route]
except KeyError:
continue
subscription = Subscription(self.account.uri, FromHeader(self.account.uri, self.account.display_name),
ToHeader(self.account.uri, self.account.display_name),
ContactHeader(contact_uri),
'xcap-diff',
RouteHeader(route.get_uri()),
credentials=self.account.credentials,
refresh=refresh_interval)
notification_center.add_observer(self, sender=subscription)
try:
subscription.subscribe(body=body, content_type=content_type, timeout=limit(remaining_time, min=1, max=5))
except (PJSIPError, SIPCoreError):
notification_center.remove_observer(self, sender=subscription)
timeout = 5
raise SubscriptionError(error='Internal error', timeout=timeout)
self._subscription = subscription
try:
while True:
notification = self._data_channel.wait()
if notification.sender is subscription and notification.name == 'SIPSubscriptionDidStart':
break
except SIPSubscriptionDidFail, e:
notification_center.remove_observer(self, sender=subscription)
self._subscription = None
if e.data.code == 407:
# Authentication failed, so retry the subscription in some time
timeout = random.uniform(60, 120)
raise SubscriptionError(error='authentication failed', timeout=timeout)
elif e.data.code == 423:
# Get the value of the Min-Expires header
timeout = random.uniform(60, 120)
if e.data.min_expires is not None and e.data.min_expires > refresh_interval:
raise SubscriptionError(error='Interval too short', timeout=timeout, refresh_interval=e.data.min_expires)
else:
raise SubscriptionError(error='Interval too short', timeout=timeout)
elif e.data.code in (405, 406, 489):
timeout = random.uniform(60, 120)
raise SubscriptionError(error='Subscription error', timeout=timeout)
else:
# Otherwise just try the next route
continue
else:
self.subscribed = True
command.signal()
break
else:
# No more routes to try, reschedule the subscription
timeout = random.uniform(60, 180)
raise SubscriptionError(error='no more routes to try', timeout=timeout)
# At this point it is subscribed. Handle notifications and ending/failures.
notification_center.post_notification('XCAPSubscriptionDidStart', sender=self, data=TimestampedNotificationData())
try:
while True:
notification = self._data_channel.wait()
if notification.sender is not self._subscription:
continue
if notification.name == 'SIPSubscriptionGotNotify':
notification_center.post_notification('XCAPSubscriptionGotNotify', sender=self, data=notification.data)
elif notification.name == 'SIPSubscriptionDidEnd':
break
except SIPSubscriptionDidFail:
notification_center.post_notification('XCAPSubscriptionDidFail', sender=self, data=TimestampedNotificationData())
self._command_channel.send(Command('subscribe'))
notification_center.remove_observer(self, sender=self._subscription)
except InterruptSubscription, e:
if not self.subscribed:
command.signal(e)
if self._subscription is not None:
notification_center.remove_observer(self, sender=self._subscription)
try:
self._subscription.end(timeout=2)
except SIPCoreError:
pass
except TerminateSubscription, e:
if not self.subscribed:
command.signal(e)
if self._subscription is not None:
try:
self._subscription.end(timeout=2)
except SIPCoreError:
pass
else:
try:
while True:
notification = self._data_channel.wait()
if notification.sender is self._subscription and notification.name == 'SIPSubscriptionDidEnd':
break
except SIPSubscriptionDidFail:
pass
finally:
notification_center.remove_observer(self, sender=self._subscription)
except SubscriptionError, e:
from twisted.internet import reactor
notification_center.post_notification('XCAPSubscriptionDidFail', sender=self, data=TimestampedNotificationData())
self._subscription_timer = reactor.callLater(e.timeout, self._command_channel.send, Command('subscribe', command.event, refresh_interval=e.refresh_interval))
finally:
self.subscribed = False
self._subscription = None
self._subscription_proc = None
@run_in_twisted_thread
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, Null)
handler(notification)
def _NH_SIPSubscriptionDidStart(self, notification):
self._data_channel.send(notification)
def _NH_SIPSubscriptionDidEnd(self, notification):
self._data_channel.send(notification)
def _NH_SIPSubscriptionDidFail(self, notification):
self._data_channel.send_exception(SIPSubscriptionDidFail(notification.data))
def _NH_SIPSubscriptionGotNotify(self, notification):
self._data_channel.send(notification)
def _NH_DNSNameserversDidChange(self, notification):
if self.active:
self.resubscribe()
def _NH_SystemIPAddressDidChange(self, notification):
if self.active:
self.resubscribe()
def _NH_SystemDidWakeUpFromSleep(self, notification):
if self._wakeup_timer is None:
from twisted.internet import reactor
def wakeup_action():
if self.active:
self.resubscribe()
self._wakeup_timer = None
self._wakeup_timer = reactor.callLater(5, wakeup_action) # wait for system to stabilize
class XCAPManager(object):
implements(IObserver)
def __init__(self, account):
from sipsimple.application import SIPApplication
if SIPApplication.storage is None:
raise RuntimeError("SIPApplication.storage must be defined before instantiating XCAPManager")
self.account = account
self.storage = None
self.storage_factory = SIPApplication.storage.xcap_storage_factory
self.client = None
self.command_proc = None
self.command_channel = coros.queue()
self.journal = []
self.last_fetch_time = datetime.fromtimestamp(0)
self.not_executed_fetch = None
self.oma_compliant = False
self.state = 'stopped'
self.timer = None
self.transaction_level = 0
self.xcap_subscriber = None
self.server_caps = XCAPCapsDocument(self)
self.dialog_rules = DialogRulesDocument(self)
self.pidf_manipulation = PIDFManipulationDocument(self)
self.pres_rules = PresRulesDocument(self)
self.resource_lists = ResourceListsDocument(self)
self.rls_services = RLSServicesDocument(self)
self.status_icon = StatusIconDocument(self)
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=account, name='CFGSettingsObjectDidChange')
notification_center.add_observer(self, sender=account, name='CFGSettingsObjectWasDeleted')
def _get_state(self):
return self.__dict__['state']
def _set_state(self, value):
old_value = self.__dict__.get('state', Null)
self.__dict__['state'] = value
if old_value != value and old_value is not Null:
notification_center = NotificationCenter()
notification_center.post_notification('XCAPManagerDidChangeState', sender=self, data=TimestampedNotificationData(prev_state=old_value, state=value))
state = property(_get_state, _set_state)
del _get_state, _set_state
@property
def documents(self):
return [self.dialog_rules, self.pidf_manipulation, self.pres_rules, self.resource_lists, self.rls_services, self.status_icon]
@property
def document_names(self):
return [document.name for document in self.documents]
@property
def contactlist_supported(self):
return self.resource_lists.supported and self.rls_services.supported
@property
def presence_policies_supported(self):
return self.pres_rules.supported
@property
def dialoginfo_policies_supported(self):
return self.dialog_rules.supported
@property
def status_icon_supported(self):
return self.status_icon.supported
@property
def offline_status_supported(self):
return self.pidf_manipulation.supported
@property
def namespaces(self):
- return dict((document.application, document.payload_type._xml_namespace) for document in self.documents)
+ return dict((document.application, document.payload_type.root_element._xml_namespace) for document in self.documents)
@property
def xcap_root(self):
return self.client.root if self.client else None
def load(self):
"""
Initializes the XCAP manager, by loading any saved data from disk. Needs
to be called before any other method and in a green thread.
"""
if self.storage is not None:
raise RuntimeError("XCAPManager cache already loaded")
storage = self.storage_factory(self.account.id)
if not IXCAPStorage.providedBy(storage):
raise TypeError("storage must implement the IXCAPStorage interface")
self.storage = storage
for document in self.documents:
document.load_from_cache()
try:
self.journal = cPickle.loads(storage.load('journal'))
except (XCAPStorageError, cPickle.UnpicklingError):
self.journal = []
else:
for operation in self.journal:
operation.applied = False
self.command_proc = proc.spawn(self._run)
def start(self):
"""
Starts the XCAP manager. This method needs to be called in a green
thread.
"""
command = Command('start')
self.command_channel.send(command)
command.wait()
def stop(self):
"""
Stops the XCAP manager. This method blocks until all the operations are
stopped and needs to be called in a green thread.
"""
command = Command('stop')
self.command_channel.send(command)
command.wait()
def transaction(self):
return XCAPTransaction(self)
@run_in_twisted_thread
def start_transaction(self):
self.transaction_level += 1
@run_in_twisted_thread
def commit_transaction(self):
if self.transaction_level == 0:
return
self.transaction_level -= 1
if self.transaction_level == 0 and self.journal:
self._save_journal()
self.command_channel.send(Command('update'))
@run_in_twisted_thread
def add_group(self, group):
operation = AddGroupOperation(group=group)
self.journal.append(operation)
if self.transaction_level == 0:
self._save_journal()
self.command_channel.send(Command('update'))
@run_in_twisted_thread
def rename_group(self, old_name, new_name):
operation = RenameGroupOperation(old_name=old_name, new_name=new_name)
self.journal.append(operation)
if self.transaction_level == 0:
self._save_journal()
self.command_channel.send(Command('update'))
@run_in_twisted_thread
def remove_group(self, group):
operation = RemoveGroupOperation(group=group)
self.journal.append(operation)
if self.transaction_level == 0:
self._save_journal()
self.command_channel.send(Command('update'))
@run_in_twisted_thread
def add_contact(self, contact):
operation = AddContactOperation(contact=contact)
self.journal.append(operation)
if self.transaction_level == 0:
self._save_journal()
self.command_channel.send(Command('update'))
@run_in_twisted_thread
def update_contact(self, contact, **attributes):
operation = UpdateContactOperation(contact=contact, attributes=attributes)
self.journal.append(operation)
if self.transaction_level == 0:
self._save_journal()
self.command_channel.send(Command('update'))
@run_in_twisted_thread
def remove_contact(self, contact):
operation = RemoveContactOperation(contact=contact)
self.journal.append(operation)
if self.transaction_level == 0:
self._save_journal()
self.command_channel.send(Command('update'))
@run_in_twisted_thread
def add_presence_policy(self, policy):
operation = AddPresencePolicyOperation(policy=policy)
self.journal.append(operation)
if self.transaction_level == 0:
self._save_journal()
self.command_channel.send(Command('update'))
@run_in_twisted_thread
def update_presence_policy(self, policy, **attributes):
operation = UpdatePresencePolicyOperation(policy=policy, attributes=attributes)
self.journal.append(operation)
if self.transaction_level == 0:
self._save_journal()
self.command_channel.send(Command('update'))
@run_in_twisted_thread
def remove_presence_policy(self, policy):
operation = RemovePresencePolicyOperation(policy=policy)
self.journal.append(operation)
if self.transaction_level == 0:
self._save_journal()
self.command_channel.send(Command('update'))
@run_in_twisted_thread
def add_dialoginfo_policy(self, policy):
operation = AddDialoginfoPolicyOperation(policy=policy)
self.journal.append(operation)
if self.transaction_level == 0:
self._save_journal()
self.command_channel.send(Command('update'))
@run_in_twisted_thread
def update_dialoginfo_policy(self, policy, **attributes):
operation = UpdateDialoginfoPolicyOperation(policy=policy, attributes=attributes)
self.journal.append(operation)
if self.transaction_level == 0:
self._save_journal()
self.command_channel.send(Command('update'))
@run_in_twisted_thread
def remove_dialoginfo_policy(self, policy):
operation = RemoveDialoginfoPolicyOperation(policy=policy)
self.journal.append(operation)
if self.transaction_level == 0:
self._save_journal()
self.command_channel.send(Command('update'))
@run_in_twisted_thread
def set_status_icon(self, icon):
operation = SetStatusIconOperation(icon=icon)
self.journal.append(operation)
if self.transaction_level == 0:
self._save_journal()
self.command_channel.send(Command('update'))
@run_in_twisted_thread
def set_offline_status(self, status):
operation = SetOfflineStatusOperation(status=status)
self.journal.append(operation)
if self.transaction_level == 0:
self._save_journal()
self.command_channel.send(Command('update'))
def _run(self):
while True:
command = self.command_channel.wait()
try:
handler = getattr(self, '_CH_%s' % command.name)
handler(command)
except:
self.command_proc = None
raise
# command handlers
#
def _CH_start(self, command):
if self.state != 'stopped':
command.signal()
return
self.state = 'initializing'
self.xcap_subscriber = XCAPSubscriber(self.account, self.documents)
notification_center = NotificationCenter()
notification_center.post_notification('XCAPManagerWillStart', sender=self, data=TimestampedNotificationData())
notification_center.add_observer(self, sender=self.xcap_subscriber)
notification_center.add_observer(self, sender=SIPSimpleSettings(), name='CFGSettingsObjectDidChange')
self.xcap_subscriber.start()
self.command_channel.send(Command('initialize'))
notification_center.post_notification('XCAPManagerDidStart', sender=self, data=TimestampedNotificationData())
command.signal()
def _CH_stop(self, command):
if self.state in ('stopped', 'terminated'):
command.signal()
return
notification_center = NotificationCenter()
notification_center.post_notification('XCAPManagerWillEnd', sender=self, data=TimestampedNotificationData())
notification_center.remove_observer(self, sender=self.xcap_subscriber)
notification_center.remove_observer(self, sender=SIPSimpleSettings(), name='CFGSettingsObjectDidChange')
if self.timer is not None and self.timer.active():
self.timer.cancel()
self.timer = None
self.xcap_subscriber.deactivate()
self.xcap_subscriber.stop()
self.xcap_subscriber = None
self.client = None
self.state = 'stopped'
self._save_journal()
notification_center.post_notification('XCAPManagerDidEnd', sender=self, data=TimestampedNotificationData())
command.signal()
def _CH_cleanup(self, command):
if self.state != 'stopped':
command.signal()
return
try:
self.storage.purge()
except XCAPStorageError:
pass
self.journal = []
self.state = 'terminated'
command.signal()
raise proc.ProcExit
def _CH_initialize(self, command):
self.state = 'initializing'
if self.timer is not None and self.timer.active():
self.timer.cancel()
self.timer = None
if self.account.xcap.xcap_root:
self.client = XCAPClient(self.account.xcap.xcap_root, self.account.id, password=self.account.auth.password, auth=None)
else:
try:
lookup = DNSLookup()
uri = random.choice(lookup.lookup_xcap_server(self.account.uri).wait())
except DNSLookupError:
self.timer = self._schedule_command(60, Command('initialize', command.event))
return
else:
self.client = XCAPClient(uri, self.account.id, password=self.account.auth.password, auth=None)
try:
self.server_caps.fetch()
except XCAPError:
self.timer = self._schedule_command(60, Command('initialize', command.event))
return
else:
if self.server_caps.content is None:
# XCAP server must always return some content for xcap-caps
self.timer = self._schedule_command(60, Command('initialize', command.event))
return
if not set(self.server_caps.content.auids).issuperset(('pres-rules', 'resource-lists', 'rls-services')):
# Server must support at least pres-rules, resource-lists and rls-services
self.timer = self._schedule_command(60, Command('initialize', command.event))
return
self.oma_compliant = 'org.openmobilealliance.pres-rules' in self.server_caps.content.auids
self.server_caps.initialize()
for document in self.documents:
document.initialize(self.server_caps.content)
notification_center = NotificationCenter()
notification_center.post_notification('XCAPManagerDidDiscoverServerCapabilities', sender=self,
data=TimestampedNotificationData(contactlist_supported = self.contactlist_supported,
presence_policies_supported = self.presence_policies_supported,
dialoginfo_policies_supported = self.dialoginfo_policies_supported,
status_icon_supported = self.status_icon_supported,
offline_status_supported = self.offline_status_supported))
self.state = 'fetching'
self.command_channel.send(Command('fetch', documents=self.document_names))
self.xcap_subscriber.activate()
def _CH_reload(self, command):
if self.state == 'terminated':
command.signal()
return
if '__id__' in command.modified:
try:
self.storage.purge()
except XCAPStorageError:
pass
self.storage = self.storage_factory(self.account.id)
self.journal = []
self._save_journal()
if set(['__id__', 'xcap.xcap_root']).intersection(command.modified):
for document in self.documents:
document.reset()
if self.state == 'stopped':
command.signal()
return
if set(['__id__', 'auth.username', 'auth.password', 'xcap.xcap_root']).intersection(command.modified):
self.state = 'initializing'
self.command_channel.send(Command('initialize'))
elif self.xcap_subscriber.active:
self.xcap_subscriber.resubscribe()
command.signal()
def _CH_fetch(self, command):
if self.state not in ('insync', 'fetching'):
self.not_executed_fetch = command
return
if self.not_executed_fetch is not None:
self.not_executed_fetch = None
if self.last_fetch_time > command.timestamp:
self.state = 'insync'
return
self.state = 'fetching'
if self.timer is not None and self.timer.active():
command.documents = list(set(command.documents) | set(self.timer.command.documents))
self.timer.cancel()
self.timer = None
try:
for document in (doc for doc in self.documents if doc.name in command.documents and doc.supported):
document.fetch()
except XCAPError:
self.timer = self._schedule_command(60, Command('fetch', command.event, documents=command.documents))
return
if self.last_fetch_time > datetime.fromtimestamp(0) and all(doc.fetch_time < command.timestamp for doc in self.documents):
self.last_fetch_time = datetime.utcnow()
self.state = 'insync'
return
else:
self.last_fetch_time = datetime.utcnow()
self.state = 'updating'
if not self.journal or type(self.journal[0]) is not NormalizeOperation:
self.journal.insert(0, NormalizeOperation())
self.command_channel.send(Command('update', command.event))
def _CH_update(self, command):
if self.state not in ('insync', 'updating'):
return
if self.transaction_level != 0:
return
self.state = 'updating'
if self.timer is not None and self.timer.active():
self.timer.cancel()
self.timer = None
journal = self.journal[:]
for operation in (operation for operation in journal if not operation.applied):
handler = getattr(self, '_OH_%s' % operation.name)
try:
handler(operation)
except Exception:
# Error while applying operation, needs to be logged -Luci
log.err()
operation.applied = True
api.sleep(0) # Operations are quite CPU intensive
try:
for document in (doc for doc in self.documents if doc.dirty and doc.supported):
document.update()
except FetchRequiredError:
for document in (doc for doc in self.documents if doc.dirty and doc.supported):
document.reset()
for operation in journal:
operation.applied = False
self.state = 'fetching'
self.command_channel.send(Command('fetch', documents=self.document_names)) # Try to fetch them all just in case
except XCAPError:
self.timer = self._schedule_command(60, Command('update'))
else:
del self.journal[:len(journal)]
if not self.journal:
self.state = 'insync'
self._load_data()
command.signal()
if self.not_executed_fetch is not None:
self.command_channel.send(self.not_executed_fetch)
self.not_executed_fetch = None
self._save_journal()
# operation handlers
#
def _OH_normalize(self, operation):
# Normalize resource-lists document :
# * create one if it doesn't exist
# * use OMA suggested names for lists (create them if inexistent)
# * if no OMA suggested lists existed, then assume all lists contain buddies and add them to the oma_buddylist
if self.resource_lists.supported:
if self.resource_lists.content is None:
self.resource_lists.content = resourcelists.ResourceLists()
self.resource_lists.dirty = True
resource_lists = self.resource_lists.content
try:
oma_buddylist = (child for child in resource_lists if isinstance(child, resourcelists.List) and child.name=='oma_buddylist').next()
except StopIteration:
oma_buddylist = None
try:
oma_grantedcontacts = (child for child in resource_lists if isinstance(child, resourcelists.List) and child.name=='oma_grantedcontacts').next()
except StopIteration:
oma_grantedcontacts = None
try:
oma_blockedcontacts = (child for child in resource_lists if isinstance(child, resourcelists.List) and child.name=='oma_blockedcontacts').next()
except StopIteration:
oma_blockedcontacts = None
try:
oma_allcontacts = (child for child in resource_lists if isinstance(child, resourcelists.List) and child.name=='oma_allcontacts').next()
except StopIteration:
oma_allcontacts = None
buddy_lists = []
if oma_buddylist is None:
oma_buddylist = resourcelists.List(name='oma_buddylist')
# If no other oma defined list exists, assume all the current lists in the resource list document contain buddies
if (oma_grantedcontacts, oma_blockedcontacts, oma_allcontacts) == (None, None, None):
for rlist in (child for child in resource_lists if isinstance(child, resourcelists.List)):
path = self.resource_lists.uri + '/~~' + resource_lists.get_xpath(rlist)
oma_buddylist.add(resourcelists.External(path))
buddy_lists.append(rlist)
resource_lists.add(oma_buddylist)
self.resource_lists.dirty = True
else:
for child in oma_buddylist:
if isinstance(child, resourcelists.List):
buddy_lists.append(child)
elif isinstance(child, resourcelists.External):
try:
buddy_lists.extend(self._follow_rl_external(resource_lists, child))
except ValueError:
continue
if oma_grantedcontacts is None:
oma_grantedcontacts = resourcelists.List(name='oma_grantedcontacts')
resource_lists.add(oma_grantedcontacts)
self.resource_lists.dirty = True
else:
# Make sure there are no references to oma_buddylist (like OMA suggests)
for child in list(oma_grantedcontacts):
if isinstance(child, resourcelists.External):
try:
if oma_buddylist in self._follow_rl_external(resource_lists, child):
oma_grantedcontacts.remove(child)
oma_grantedcontacts.update(resourcelists.External(self.resource_lists.uri+'/~~'+resource_lists.get_xpath(blist)) for blist in buddy_lists)
self.resource_lists.dirty = True
break
except ValueError:
continue
if oma_blockedcontacts is None:
oma_blockedcontacts = resourcelists.List(name='oma_blockedcontacts')
resource_lists.add(oma_blockedcontacts)
self.resource_lists.dirty = True
if oma_allcontacts is None:
oma_allcontacts = resourcelists.List(name='oma_allcontacts')
for rlist in (oma_buddylist, oma_grantedcontacts, oma_blockedcontacts):
path = self.resource_lists.uri + '/~~' + resource_lists.get_xpath(rlist)
oma_allcontacts.add(resourcelists.External(path))
resource_lists.add(oma_allcontacts)
self.resource_lists.dirty = True
# Remove any external references which don't point to anything
notexpanded = deque([resource_lists])
visited = set(notexpanded)
remove_elements = []
while notexpanded:
rlist = notexpanded.popleft()
for child in rlist:
if isinstance(child, resourcelists.List) and child not in visited:
visited.add(child)
notexpanded.append(child)
elif isinstance(child, resourcelists.External):
try:
ref_lists = self._follow_rl_external(resource_lists, child)
except ValueError:
continue
if not ref_lists:
remove_elements.append((child, rlist))
for element, parent in remove_elements:
parent.remove(element)
# Normalize rls-services document:
# * create one if it doesn't exist
# * if it is empty, start by assuming we want to subscribe to all the buddies
if self.rls_services.supported:
if self.rls_services.content is None:
self.rls_services.content = rlsservices.RLSServices()
self.rls_services.dirty = True
rls_services = self.rls_services.content
if len(rls_services) == 0 and self.resource_lists.supported and buddy_lists:
rlist = resourcelists.List()
service = rlsservices.Service('sip:buddies@%s' % self.account.id.domain, list=rlist, packages=rlsservices.Packages(['presence', 'dialog']))
for blist in buddy_lists:
path = self.resource_lists.uri + '/~~' + resource_lists.get_xpath(blist)
rlist.add(resourcelists.External(path))
rls_services.add(service)
self.rls_services.dirty = True
# Normalize pres-rules document:
# * create one if it doesn't exist
# * if OMA support is enabled on the presence server, create some OMA suggested rules
# * also normalize the document so that it is OMA compliant
# * get rid of any wp_prs_(allow_)one_* rules because they can't be supported
if self.pres_rules.supported:
if self.pres_rules.content is None:
self.pres_rules.content = presrules.PresRules()
self.pres_rules.dirty = True
if self.oma_compliant and self.resource_lists.supported:
pres_rules = self.pres_rules.content
resource_lists = self.resource_lists.content
path = self.resource_lists.uri + '/~~/resource-lists/list[@name="oma_grantedcontacts"]'
try:
wp_prs_grantedcontacts = (child for child in pres_rules if isinstance(child, common_policy.Rule) and child.id=='wp_prs_grantedcontacts').next()
except StopIteration:
wp_prs_grantedcontacts = common_policy.Rule('wp_prs_grantedcontacts', conditions=[omapolicy.ExternalList([path])], actions=[presrules.SubHandling('allow')])
#wp_prs_grantedcontacts.display_name = 'Allow' # Rule display-name extension
pres_rules.add(wp_prs_grantedcontacts)
self.pres_rules.dirty = True
else:
if wp_prs_grantedcontacts.conditions != common_policy.Conditions([omapolicy.ExternalList([path])]):
wp_prs_grantedcontacts.conditions = [omapolicy.ExternalList([path])]
self.pres_rules.dirty = True
if wp_prs_grantedcontacts.actions != common_policy.Actions([presrules.SubHandling('allow')]):
wp_prs_grantedcontacts.actions = [presrules.SubHandling('allow')]
self.pres_rules.dirty = True
path = self.resource_lists.uri + '/~~/resource-lists/list[@name="oma_blockedcontacts"]'
try:
wp_prs_blockedcontacts = (child for child in pres_rules if isinstance(child, common_policy.Rule) and child.id=='wp_prs_blockedcontacts').next()
except StopIteration:
wp_prs_blockedcontacts = common_policy.Rule('wp_prs_blockedcontacts', conditions=[omapolicy.ExternalList([path])], actions=[presrules.SubHandling('polite-block')])
#wp_prs_blockedcontacts.display_name = 'Block' # Rule display-name extension
pres_rules.add(wp_prs_blockedcontacts)
self.pres_rules.dirty = True
else:
if wp_prs_blockedcontacts.conditions != common_policy.Conditions([omapolicy.ExternalList([path])]):
wp_prs_blockedcontacts.conditions = [omapolicy.ExternalList([path])]
self.pres_rules.dirty = True
if wp_prs_blockedcontacts.actions not in [common_policy.Actions([presrules.SubHandling('block')]), common_policy.Actions([presrules.SubHandling('polite-block')])]:
wp_prs_blockedcontacts.actions = [presrules.SubHandling('polite-block')]
self.pres_rules.dirty = True
if wp_prs_blockedcontacts.transformations:
wp_prs_blockedcontacts.transformations = None
self.pres_rules.dirty = True
wp_prs_unlisted = [child for child in pres_rules if isinstance(child, common_policy.Rule) and child.id in ('wp_prs_unlisted', 'wp_prs_allow_unlisted')]
if len(wp_prs_unlisted) == 0:
wp_prs_unlisted = common_policy.Rule('wp_prs_unlisted', conditions=[omapolicy.OtherIdentity()], actions=[presrules.SubHandling('confirm')])
pres_rules.add(wp_prs_unlisted)
self.pres_rules.dirty = True
else:
for rule in wp_prs_unlisted[1:]:
pres_rules.remove(rule)
wp_prs_unlisted = wp_prs_unlisted[0]
if wp_prs_unlisted.conditions != common_policy.Conditions([omapolicy.OtherIdentity()]):
wp_prs_unlisted.conditions = [omapolicy.OtherIdentity()]
self.pres_rules.dirty = True
if wp_prs_unlisted.id == 'wp_prs_unlisted' and wp_prs_unlisted.actions not in [common_policy.Actions([presrules.SubHandling('confirm')]), common_policy.Actions([presrules.SubHandling('block')]), common_policy.Actions([presrules.SubHandling('polite-block')])]:
wp_prs_unlisted.actions = [presrules.SubHandling('confirm')]
self.pres_rules.dirty = True
elif wp_prs_unlisted.id == 'wp_prs_allow_unlisted' and wp_prs_unlisted.actions != common_policy.Actions([presrules.SubHandling('allow')]):
wp_prs_unlisted.actions = [presrules.SubHandling('allow')]
self.pres_rules.dirty = True
if wp_prs_unlisted.id == 'wp_prs_unlisted' and wp_prs_unlisted.transformations:
wp_prs_unlisted.transformations = None
self.pres_rules.dirty = True
try:
wp_prs_block_anonymous = (child for child in pres_rules if isinstance(child, common_policy.Rule) and child.id=='wp_prs_block_anonymous').next()
except StopIteration:
wp_prs_block_anonymous = common_policy.Rule('wp_prs_block_anonymous', conditions=[omapolicy.AnonymousRequest()], actions=[presrules.SubHandling('block')])
pres_rules.add(wp_prs_block_anonymous)
self.pres_rules.dirty = True
else:
if wp_prs_block_anonymous.conditions != common_policy.Conditions([omapolicy.AnonymousRequest()]):
wp_prs_block_anonymous.conditions = [omapolicy.AnonymousRequest()]
self.pres_rules.dirty = True
if wp_prs_block_anonymous.actions not in [common_policy.Actions([presrules.SubHandling('block')]), common_policy.Actions([presrules.SubHandling('polite-block')])]:
wp_prs_block_anonymous.actions = [presrules.SubHandling('block')]
self.pres_rules.dirty = True
if wp_prs_block_anonymous.transformations:
wp_prs_block_anonymous.transformations = None
self.pres_rules.dirty = True
identity = 'sip:'+self.account.id
try:
wp_prs_allow_own = (child for child in pres_rules if isinstance(child, common_policy.Rule) and child.id=='wp_prs_allow_own').next()
except StopIteration:
wp_prs_allow_own = common_policy.Rule('wp_prs_allow_own', conditions=[common_policy.Identity([common_policy.IdentityOne(identity)])], actions=[presrules.SubHandling('allow')])
pres_rules.add(wp_prs_allow_own)
self.pres_rules.dirty = True
else:
if wp_prs_allow_own.conditions != common_policy.Conditions([common_policy.Identity([common_policy.IdentityOne(identity)])]):
wp_prs_allow_own.conditions = [common_policy.Identity([common_policy.IdentityOne(identity)])]
self.pres_rules.dirty = True
if wp_prs_allow_own.actions != common_policy.Actions([presrules.SubHandling('allow')]):
wp_prs_allow_own.actions = [presrules.SubHandling('allow')]
self.pres_rules.dirty = True
# We cannot work with wp_prs_allow_one_* rules because they don't allow adding new identities to the rule
for rule in (child for child in pres_rules if isinstance(child, common_policy.Rule) and child.id.startswith('wp_prs_allow_one_')):
# Create a new list just for this identity
rlist = resourcelists.List(name=self.unique_name('presrules_group', (list.name for list in resource_lists), skip_preferred_name=True).next())
resource_lists.add(rlist)
# Change the name to wp_prs_allow_onelist_<group name>
rule.id = self.unique_name('wp_prs_allow_onelist_'+rlist.name, (rule.id for rule in pres_rules)).next()
# Change the condition
for condition in (rule.conditions or []):
if isinstance(condition, common_policy.Identity):
for identity in (identity_condition for identity_condition in condition if isinstance(identity_condition, common_policy.IdentityOne)):
rlist.add(resourcelists.Entry(uri=identity.id))
elif isinstance(condition, omapolicy.ExternalList):
# This shouldn't happen but accept it anyway
for uri in condition:
rlist.add(resourcelists.External(uri))
path = self.resource_lists.uri + '/~~' + resource_lists.get_xpath(rlist)
rule.conditions = [omapolicy.ExternalList([path])]
# Make sure the action is allow
if rule.actions != common_policy.Actions([presrules.SubHandling('allow')]):
rule.actions = [presrules.SubHandling('allow')]
self.resource_lists.dirty = True
self.pres_rules.dirty = True
for rule in (child for child in pres_rules if isinstance(child, common_policy.Rule) and child.id.startswith('wp_prs_allow_onelist_')):
if rule.actions != common_policy.Actions([presrules.SubHandling('allow')]):
rule.actions = [presrules.SubHandling('allow')]
self.pres_rules.dirty = True
# We cannot work with wp_prs_one_* rules because they don't allow adding new identities to the rule
for rule in (child for child in pres_rules if isinstance(child, common_policy.Rule) and child.id.startswith('wp_prs_one_')):
# Create a new list just for this identity
rlist = resourcelists.List(name=self.unique_name('presrules_group', (list.name for list in resource_lists), skip_preferred_name=True).next())
resource_lists.add(rlist)
# Change the name to wp_prs_onelist_<group name>
rule.id = self.unique_name('wp_prs_onelist_'+rlist.name, (rule.id for rule in pres_rules)).next()
# Change the condition
for condition in (rule.conditions or []):
if isinstance(condition, common_policy.Identity):
for identity in (identity_condition for identity_condition in condition if isinstance(identity_condition, common_policy.IdentityOne)):
rlist.add(resourcelists.Entry(uri=identity.id))
elif isinstance(condition, omapolicy.ExternalList):
# This shouldn't happen but accept it anyway
for uri in condition:
rlist.add(resourcelists.External(uri))
path = self.resource_lists.uri + '/~~' + resource_lists.get_xpath(rlist)
rule.conditions = [omapolicy.ExternalList([path])]
# Make sure the action is one of confirm, block or polite-block
if rule.actions not in [common_policy.Actions([presrules.SubHandling('confirm')]), common_policy.Actions([presrules.SubHandling('block')]), common_policy.Actions([presrules.SubHandling('polite-block')])]:
rule.actions = [presrules.SubHandling('confirm')]
# Make sure there are no transformations
if rule.transformations:
rule.transformations = None
self.resource_lists.dirty = True
self.pres_rules.dirty = True
for rule in (child for child in pres_rules if isinstance(child, common_policy.Rule) and child.id.startswith('wp_prs_onelist_')):
if rule.actions not in [common_policy.Actions([presrules.SubHandling('confirm')]), common_policy.Actions([presrules.SubHandling('block')]), common_policy.Actions([presrules.SubHandling('polite-block')])]:
rule.actions = [presrules.SubHandling('confirm')]
self.pres_rules.dirty = True
if rule.transformations:
rule.transformations = None
self.pres_rules.dirty = True
# Normalize dialog-rules document:
# * create one if it doesn't exist
# * create a rule of each of the four possible actions except 'confirm'
if self.dialog_rules.supported:
if self.dialog_rules.content is None:
self.dialog_rules.content = dialogrules.DialogRules()
self.dialog_rules.dirty = True
dialog_rules = self.dialog_rules.content
for action in ('allow', 'block', 'polite-block'):
try:
(child for child in dialog_rules if isinstance(child, common_policy.Rule) and any(isinstance(a, dialogrules.SubHandling) and a.value==action for a in (child.actions or []))).next()
except StopIteration:
name = self.unique_name('dialog_%s' % action, (rule.id for rule in dialog_rules)).next()
rule = common_policy.Rule(name, conditions=[], actions=[dialogrules.SubHandling(action)])
dialog_rules.add(rule)
self.dialog_rules.dirty = True
def _OH_add_group(self, operation):
if not self.resource_lists.supported:
return
resource_lists = self.resource_lists.content
try:
oma_buddylist = (child for child in resource_lists if isinstance(child, resourcelists.List) and child.name=='oma_buddylist').next()
except StopIteration:
# This should never happen as the document is normalized
return
# See if there already is a group with the specified name: a group reachable from within oma_buddylist
notexpanded = deque([oma_buddylist])
visited = set(notexpanded)
while notexpanded:
rlist = notexpanded.popleft()
for child in rlist:
if isinstance(child, resourcelists.List) and child not in visited:
if child.display_name is not None and child.display_name.value == operation.group:
return
visited.add(child)
notexpanded.append(child)
elif isinstance(child, resourcelists.External):
try:
ref_lists = set(l for l in self._follow_rl_external(resource_lists, child) if l not in visited)
except ValueError:
ref_lists = set()
if child.display_name is not None and child.display_name.value == operation.group and ref_lists:
# Only assume the group exists if there is at least one list obtained by dereferencing this external element
return
if child.display_name is None and any(l.display_name is not None and l.display_name.value == operation.group for l in ref_lists):
# The lists obtained by dereferencing this external element inherit its display name if it exists, otherwise they use their own display name
return
visited.update(ref_lists)
notexpanded.extend(ref_lists)
# A list with the specified name was not found, create it
name = self.unique_name('group', (list.name for list in resource_lists), skip_preferred_name=True).next()
rlist = resourcelists.List(name=name, display_name=operation.group)
resource_lists.add(rlist)
path = self.resource_lists.uri + '/~~' + resource_lists.get_xpath(rlist)
oma_buddylist.add(resourcelists.External(path))
self.resource_lists.dirty = True
def _OH_rename_group(self, operation):
if not self.resource_lists.supported:
return
resource_lists = self.resource_lists.content
try:
oma_buddylist = (child for child in resource_lists if isinstance(child, resourcelists.List) and child.name=='oma_buddylist').next()
except StopIteration:
# This should never happen as the document is normalized
return
notexpanded = deque([oma_buddylist])
visited = set(notexpanded)
while notexpanded:
rlist = notexpanded.popleft()
for child in rlist:
if isinstance(child, resourcelists.List) and child not in visited:
if child.display_name is not None and child.display_name.value == operation.old_name:
child.display_name = operation.new_name
self.resource_lists.dirty = True
visited.add(child)
notexpanded.append(child)
elif isinstance(child, resourcelists.External):
try:
ref_lists = self._follow_rl_external(resource_lists, child)
except ValueError:
ref_lists = []
if child.display_name is not None and child.display_name.value == operation.old_name and ref_lists:
child.display_name = operation.new_name
ref_lists = set(l for l in ref_lists if l not in visited)
if child.display_name is None:
for ref_list in ref_lists:
if ref_list.display_name is not None and ref_list.display_name.value == operation.old_name:
ref_list.display_name = operation.new_name
self.resource_lists.dirty = True
visited.update(ref_lists)
notexpanded.extend(ref_lists)
def _OH_remove_group(self, operation):
if not self.resource_lists.supported:
return
resource_lists = self.resource_lists.content
try:
oma_buddylist = (child for child in resource_lists if isinstance(child, resourcelists.List) and child.name=='oma_buddylist').next()
except StopIteration:
# This should never happen as the document is normalized
return
remove_elements = [] # (element, parent)
move_elements = [] # (element, new_parent)
notexpanded = deque([(oma_buddylist, False, None)])
visited = set([oma_buddylist])
while notexpanded:
rlist, list_removed, new_parent = notexpanded.popleft()
for child in rlist:
if isinstance(child, resourcelists.List) and child not in visited:
if child.display_name is not None and child.display_name.value == operation.group:
# Needs to be removed
if not list_removed:
# Only remove the child if the parent (rlist) will not be removed
remove_elements.append((child, rlist))
notexpanded.append((child, True, new_parent if list_removed else rlist))
else:
if list_removed and child.display_name is not None:
# If the parent is removed, the child needs to be moved
move_elements.append((child, new_parent))
# Will not be removed
notexpanded.append((child, list_removed and child.display_name is None, new_parent))
visited.add(child)
elif isinstance(child, resourcelists.External):
try:
all_references = self._follow_rl_external(resource_lists, child)
except ValueError:
all_references = []
ref_lists = set(l for l in all_references if l not in visited)
if child.display_name is not None and child.display_name.value == operation.group and all_references:
# The whole reference needs to be removed
if not list_removed:
# Only remove the child if the parent (rlist) will not be removed
remove_elements.append((child, rlist))
# The referenced lists may also need to be removed from their parents as they can also have a display name
for l in ref_lists:
if l.display_name is not None and l.display_name.value == operation.group:
parent = resource_lists.find_parent(l)
if parent is not None:
remove_elements.append((l, parent))
notexpanded.append((l, True, parent))
else:
notexpanded.append((l, False, None))
else:
notexpanded.append((l, False, None))
visited.update(ref_lists)
elif child.display_name is None and all_references:
# If the display name is None, the list's display name applies
for l in ref_lists:
if l.display_name is not None and l.display_name.value == operation.group:
# Also remove from all_references so that we can check whether to remove the external reference or not
all_references.remove(l)
parent = resource_lists.find_parent(l)
if parent is not None:
remove_elements.append((l, parent))
notexpanded.append((l, True, parent))
else:
notexpanded.append((l, False, None))
else:
notexpanded.append((l, False, None))
if not all_references and not list_removed:
# If the external reference would not point to any other *thing*, remove it
remove_elements.append((child, rlist))
elif list_removed and isinstance(new_parent, resourcelists.List):
# If the parent is removed, the child needs to be moved
move_elements.append((child, new_parent))
visited.update(ref_lists)
else:
if list_removed and all_references and isinstance(new_parent, resourcelists.List):
# If the parent is removed, the child needs to be moved
move_elements.append((child, new_parent))
notexpanded.extend((l, False, None) for l in ref_lists)
# Do not update visited with ref_lists since these lists might also have a display name and be referenced some other way
for element, parent in move_elements:
parent.append(element)
for element, parent in remove_elements:
parent.remove(element)
if move_elements or remove_elements:
self.resource_lists.dirty = True
# Remove any external references which no longer point to anything
notexpanded = deque([resource_lists])
visited = set(notexpanded)
del remove_elements[:]
while notexpanded:
rlist = notexpanded.popleft()
for child in rlist:
if isinstance(child, resourcelists.List) and child not in visited:
visited.add(child)
notexpanded.append(child)
elif isinstance(child, resourcelists.External):
try:
ref_lists = self._follow_rl_external(resource_lists, child)
except ValueError:
continue
if not ref_lists:
remove_elements.append((child, rlist))
for element, parent in remove_elements:
parent.remove(element)
def _OH_add_contact(self, operation):
if not self.resource_lists.supported:
return
resource_lists = self.resource_lists.content
try:
oma_buddylist = (child for child in resource_lists if isinstance(child, resourcelists.List) and child.name=='oma_buddylist').next()
except StopIteration:
# This should never happen as the document is normalized
return
if operation.contact.presence_policies is None:
operation.contact.presence_policies = []
elif self.oma_compliant:
# Filter out policies we mustn't add contacts to
operation.contact.presence_policies = [policy for policy in operation.contact.presence_policies if policy.id not in ('wp_prs_unlisted', 'wp_prs_allow_unlisted', 'wp_prs_block_anonymous', 'wp_prs_allow_own')]
if operation.contact.dialoginfo_policies is None:
operation.contact.dialoginfo_policies = []
# First see if this contact (uniquely identified by uri) is already a buddy; if it is, don't do anything
notexpanded = deque([oma_buddylist])
visited = set(notexpanded)
while notexpanded:
rlist = notexpanded.popleft()
for child in rlist:
if isinstance(child, resourcelists.List) and child not in visited:
visited.add(child)
notexpanded.append(child)
elif isinstance(child, resourcelists.External):
try:
ref_lists = set(l for l in self._follow_rl_external(resource_lists, child) if l not in visited)
except ValueError:
ref_lists = set()
visited.update(ref_lists)
notexpanded.extend(ref_lists)
elif isinstance(child, resourcelists.EntryRef):
try:
entries = self._follow_rl_entry_ref(resource_lists, child)
except ValueError:
continue
if any(entry.uri == operation.contact.uri for entry in entries):
return
elif isinstance(child, resourcelists.Entry):
if child.uri == operation.contact.uri:
return
# Then find all the lists where we can add this new contact
if operation.contact.group is not None:
# Only add contacts to lists directly referenced from oma_buddylist by an external element.
candidate_lists = []
for child in oma_buddylist:
if isinstance(child, resourcelists.External):
try:
ref_lists = set(l for l in self._follow_rl_external(resource_lists, child))
except ValueError:
ref_lists = set()
if child.display_name is not None:
if child.display_name.value == operation.contact.group:
candidate_lists.extend(ref_lists)
else:
continue
else:
candidate_lists.extend(l for l in ref_lists if l.display_name is not None and l.display_name.value == operation.contact.group)
else:
# Add contacts which are not buddies to top-level lists which are not referenced by oma_buddylist
candidate_lists = [l for l in resource_lists if l not in visited]
# Add a fallback candidate which should never, ever be referenced from anywhere
fallback_candidate = resourcelists.List(name=self.unique_name('group', (l.name for l in resource_lists), skip_preferred_name=True).next(), display_name=operation.contact.group)
candidate_lists.append(fallback_candidate)
# We may need the XPath to this list
resource_lists.add(fallback_candidate)
presence_lists = set() # Lists to which, if added, the rls services document need not be modified for presence subscription
dialoginfo_lists = set() # Lists to which, if added, the rls services document need not be modified for dialoginfo subscription
presrules_lists = set() # Lists to which, if added, the presrules document need not be modified
not_wanted_lists = set() # Lists we don't want to add the contact to because they are referenced from other places
remove_from_lists = set() # Lists we need to remove the contact from
to_remove = [] # (child, parent)
# Filter candidates based on subscribe_to_* flags
if self.rls_services.supported:
# While filtering candidates, also remove any reference to the uri from unwanted services
rls_services = self.rls_services.content
for service in rls_services:
packages = set(service.packages or [])
if isinstance(service.list, rlsservices.RLSList):
expanded_list = [service.list]
elif isinstance(service.list, rlsservices.ResourceList):
try:
expanded_list = self._follow_rls_resource_list(resource_lists, service.list)
except ValueError:
expanded_list = []
else:
expanded_list = []
if any(package not in ('presence', 'dialog') for package in packages):
# We do not want lists referenced by this service because we do not know what we'd add the contact to.
# However, if the contact is already in this list, allow it to stay here
not_wanted_lists.update(expanded_list)
if not operation.contact.subscribe_to_presence and 'presence' in packages:
not_wanted_lists.update(expanded_list)
remove_from_lists.update(expanded_list)
elif not operation.contact.subscribe_to_dialoginfo and 'dialog' in packages:
not_wanted_lists.update(expanded_list)
remove_from_lists.update(expanded_list)
elif set(['presence', 'dialog']).issuperset(packages):
if 'presence' in packages:
presence_lists.update(expanded_list)
if 'dialog' in packages:
dialoginfo_lists.update(expanded_list)
else:
# Any list will do
presence_lists.update(l for l in candidate_lists if l is not fallback_candidate)
dialoginfo_lists.update(l for l in candidate_lists if l is not fallback_candidate)
# Filter out condidates based on presence policy
if self.pres_rules.supported:
pres_rules = self.pres_rules.content
# While filtering candidates based on presence policy also remove any reference to the uri
remaining_policies = [policy.id for policy in operation.contact.presence_policies]
policy_lists = {}
for rule in pres_rules:
# Action is used to ignore rules whose actions we don't understand
try:
action = (action.value for action in (rule.actions or []) if isinstance(action, presrules.SubHandling)).next()
except StopIteration:
action = None
if not operation.contact.presence_policies or rule.id not in (policy.id for policy in operation.contact.presence_policies) or action is None:
# We do not want to add the contact to lists referenced by this rule (we also want to remove the contact from them if we understand the rule)
for condition in (rule.conditions or []):
if isinstance(condition, omapolicy.ExternalList):
try:
ref_lists = self._follow_policy_external_list(resource_lists, condition)
not_wanted_lists.update(ref_lists)
if action is not None:
remove_from_lists.update(ref_lists)
except ValueError:
continue
elif isinstance(condition, common_policy.Identity) and action is not None and operation.contact.presence_policies is not FallbackPolicies:
for identity in condition:
if isinstance(identity, common_policy.IdentityOne) and identity.id == operation.contact.uri:
to_remove.append((identity, condition))
self.pres_rules.dirty = True
elif isinstance(identity, common_policy.IdentityMany) and identity.matches(operation.contact.uri):
identity.add(common_policy.IdentityExcept(operation.contact.uri))
self.pres_rules.dirty = True
elif operation.contact.presence_policies and action is not None:
# This is one of the rules we want to add the contact to
remaining_policies.remove(rule.id)
policy_lists[rule.id] = set()
for condition in (rule.conditions or []):
if isinstance(condition, omapolicy.ExternalList):
try:
ref_lists = self._follow_policy_external_list(resource_lists, condition)
policy_lists[rule.id].update(ref_lists)
except ValueError:
continue
elif isinstance(condition, common_policy.Identity):
if condition.matches(operation.contact.uri):
policy_lists[rule.id].update(l for l in candidate_lists if l is not fallback_candidate) # Any list will do
if remaining_policies:
# Some policies do not exist, they need to be added; therefore, no list will do
pass
elif operation.contact.presence_policies:
lists = policy_lists.popitem()[1]
while policy_lists:
lists = lists.intersection(policy_lists.popitem()[1])
presrules_lists.update(lists)
else:
presrules_lists.update(l for l in candidate_lists if l is not fallback_candidate) # Any list will do
else:
presrules_lists.update(l for l in candidate_lists if l is not fallback_candidate) # Any list will do
notexpanded = deque(presence_lists|dialoginfo_lists|presrules_lists|not_wanted_lists|remove_from_lists)
visited = set(notexpanded)
add_to_presence_list = operation.contact.subscribe_to_presence
add_to_dialoginfo_list = operation.contact.subscribe_to_dialoginfo
while notexpanded:
rlist = notexpanded.popleft()
if rlist in not_wanted_lists and rlist in candidate_lists:
candidate_lists.remove(rlist)
# Some children will need to be revisited so that their descendents are added to appropriate sets
for child in rlist:
if isinstance(child, resourcelists.List):
revisit = False
if rlist in presence_lists:
revisit = (child not in presence_lists) or revisit
presence_lists.add(child)
if rlist in dialoginfo_lists:
revisit = (child not in dialoginfo_lists) or revisit
dialoginfo_lists.add(child)
if rlist in presrules_lists:
revisit = (child not in presrules_lists) or revisit
presrules_lists.add(child)
if rlist in not_wanted_lists:
revisit = (child not in not_wanted_lists) or revisit
not_wanted_lists.add(child)
if rlist in remove_from_lists:
revisit = (child not in remove_from_lists) or revisit
remove_from_lists.add(child)
if child not in visited or revisit:
visited.add(child)
notexpanded.append(child)
elif isinstance(child, resourcelists.External):
try:
ref_lists = self._follow_rl_external(resource_lists, child)
except ValueError:
ref_lists = []
revisit = set()
if rlist in presence_lists:
revisit.update(l for l in ref_lists if l not in presence_lists)
presence_lists.update(ref_lists)
if rlist in dialoginfo_lists:
revisit.update(l for l in ref_lists if l not in dialoginfo_lists)
dialoginfo_lists.update(ref_lists)
if rlist in presrules_lists:
revisit.update(l for l in ref_lists if l not in presrules_lists)
presrules_lists.update(ref_lists)
if rlist in not_wanted_lists:
revisit.update(l for l in ref_lists if l not in not_wanted_lists)
not_wanted_lists.update(ref_lists)
if rlist in remove_from_lists:
revisit.update(l for l in ref_lists if l not in remove_from_lists)
remove_from_lists.update(ref_lists)
visited.update(l for l in ref_lists if l not in visited)
notexpanded.extend(l for l in ref_lists if l not in visited or l in revisit)
elif isinstance(child, resourcelists.EntryRef):
try:
entries = self._follow_rl_entry_ref(resource_lists, child)
except ValueError:
continue
if any(entry.uri == operation.contact.uri for entry in entries):
if rlist in remove_from_lists:
to_remove.append((child, rlist))
self.resource_lists.dirty = True
self.rls_services.dirty = True
if operation.contact.subscribe_to_presence and rlist in presence_lists:
add_to_presence_list = False
if operation.contact.subscribe_to_dialoginfo and rlist in dialoginfo_lists:
add_to_dialoginfo_list = False
elif isinstance(child, resourcelists.Entry):
if child.uri == operation.contact.uri:
if rlist in remove_from_lists:
to_remove.append((child, rlist))
self.resource_lists.dirty = True
self.rls_services.dirty = True
if operation.contact.subscribe_to_presence and rlist in presence_lists:
add_to_presence_list = False
if operation.contact.subscribe_to_dialoginfo and rlist in dialoginfo_lists:
add_to_dialoginfo_list = False
# Update the dialoginfo rules
if self.dialog_rules.supported and operation.contact.dialoginfo_policies is not FallbackPolicies:
dialog_rules = self.dialog_rules.content
# Create any non-existing rules
if operation.contact.dialoginfo_policies:
for policy in (p for p in operation.contact.dialoginfo_policies if p.id not in dialog_rules):
op = AddDialoginfoPolicyOperation(policy=policy)
handler = getattr(self, '_OH_%s' % op.name)
handler(op)
# Remove any reference to the uri and add it to the correct rule
for rule in dialog_rules:
if not operation.contact.dialoginfo_policies or rule.id not in (policy.id for policy in operation.contact.dialoginfo_policies):
for condition in (rule.conditions or []):
if isinstance(condition, common_policy.Identity):
for identity in condition:
if isinstance(identity, common_policy.IdentityOne) and identity.id == operation.contact.uri:
to_remove.append((identity, condition))
self.dialog_rules.dirty = True
elif isinstance(identity, common_policy.IdentityMany) and identity.matches(operation.contact.uri):
identity.add(common_policy.IdentityExcept(operation.contact.uri))
self.dialog_rules.dirty = True
elif operation.contact.dialoginfo_policies:
# This is one of the rules we want to add the contact to
if rule.conditions is None:
rule.conditions = common_policy.Conditions()
for condition in rule.conditions:
if isinstance(condition, common_policy.Identity):
if not condition.matches(operation.contact.uri):
# First see if there is an exception added for this uri
for identity_condition in condition:
if isinstance(identity_condition, common_policy.IdentityMany):
try:
except_condition = (child for child in identity_condition if isinstance(child, common_policy.IdentityExcept) and child.id==operation.contact.uri).next()
except StopIteration:
continue
else:
identity_condition.remove(except_condition)
self.dialog_rules.dirty = True
break
else:
# Otherwise just add a identity one
condition.add(common_policy.IdentityOne(operation.contact.uri))
self.dialog_rules.dirty = True
break
else:
# No identity condition found, add it
rule.conditions.add(common_policy.Identity([common_policy.IdentityOne(operation.contact.uri)]))
self.dialog_rules.dirty = True
# Remove the elements we wanted to remove
for child, parent in to_remove:
parent.remove(child)
# Identity elements can't be empty
if self.pres_rules.supported:
pres_rules = self.pres_rules.content
for rule in pres_rules:
try:
action = (action.value for action in (rule.actions or []) if isinstance(action, presrules.SubHandling)).next()
except StopIteration:
continue # Ignore rules whose actions we don't understand
for condition in (condition for condition in list(rule.conditions or []) if isinstance(condition, common_policy.Identity)):
if len(condition) == 0:
rule.conditions.remove(condition)
rule.conditions.add(common_policy.FalseCondition())
if self.dialog_rules.supported:
dialog_rules = self.dialog_rules.content
for rule in dialog_rules:
try:
action = (action.value for action in (rule.actions or []) if isinstance(action, dialogrules.SubHandling)).next()
except StopIteration:
continue # Ignore rules whose actions we don't understand
for condition in (condition for condition in list(rule.conditions or []) if isinstance(condition, common_policy.Identity)):
if len(condition) == 0:
rule.conditions.remove(condition)
rule.conditions.add(common_policy.FalseCondition())
# Preferred candidates are candidates where if added, the pres-rules and rls-services document need not be modified.
preferred_candidate_lists = presrules_lists.intersection(candidate_lists)
if operation.contact.subscribe_to_presence:
preferred_candidate_lists.intersection_update(presence_lists)
if operation.contact.subscribe_to_dialoginfo:
preferred_candidate_lists.intersection_update(dialoginfo_lists)
if preferred_candidate_lists:
rlist = preferred_candidate_lists.pop()
else:
rlist = candidate_lists[0]
if self.pres_rules.supported and operation.contact.presence_policies and rlist not in presrules_lists:
pres_rules = self.pres_rules.content
for policy in operation.contact.presence_policies:
try:
rule = pres_rules[policy.id]
except KeyError:
op = AddPresencePolicyOperation(policy=policy)
handler = getattr(self, '_OH_%s' % op.name)
handler(op)
if policy.id not in pres_rules:
continue
rule = pres_rules[policy.id]
if rule.conditions is None:
rule.conditions = []
else:
try:
action = (action.value for action in (rule.actions or []) if isinstance(action, presrules.SubHandling)).next()
except StopIteration:
continue # We don't understand this rule, ignore the request to add to this policy
# If we have an identity condition, use that one
identity_conditions = [condition for condition in rule.conditions if isinstance(condition, common_policy.Identity)]
if identity_conditions:
# First see if there is an exception added for this uri
for subcondition in chain(*identity_conditions):
if isinstance(subcondition, common_policy.IdentityMany):
try:
except_condition = (child for child in subcondition if isinstance(child, common_policy.IdentityExcept) and child.id==operation.contact.uri).next()
except StopIteration:
continue
else:
subcondition.remove(except_condition)
break
else:
# Otherwise just add a identity one
identity_conditions[0].add(common_policy.IdentityOne(operation.contact.uri))
elif self.oma_compliant:
external_lists = [condition for condition in rule.conditions if isinstance(condition, omapolicy.ExternalList)]
if external_lists or len(rlist) > 0 or rule.id in ('wp_prs_grantedcontacts', 'wp_prs_blockedcontacts'):
# We cannot modify the references of wp_prs_grantedcontacts and wp_prs_blockedcontacts
for external_list in external_lists:
try:
container_list = (l for l in self._follow_policy_external_list(resource_lists, external_list) if l not in not_wanted_lists).next()
except (ValueError, StopIteration):
continue
else:
break
else:
if rule.id in ('wp_prs_grantedcontacts', 'wp_prs_blockedcontacts'):
# The user wants to add this contact to one of these rules, but the lists referenced by them are not
# good because they are referenced from other places; create a new rule, similar to this one
container_list = resourcelists.List(name=self.unique_name('presrules_group', (list.name for list in resource_lists), skip_preferred_name=True).next())
resource_lists.add(container_list)
new_policy = deepcopy(policy)
new_policy.id = self.unique_name(('wp_prs_allow_onelist_' if new_policy.action=='allow' else 'wp_prs_onelist_')+container_list.name, (rule.id for rule in pres_rules)).next()
op = AddPresencePolicyOperation(policy=new_policy)
handler = getattr(self, '_OH_%s' % op.name)
handler(op)
rule = pres_rules[new_policy.id]
path = self.resource_lists.uri + '/~~/resource-lists/list[@name="%s"]' % container_list.name
rule.conditions.add(omapolicy.ExternalList([path]))
else:
for external_list in external_lists:
rule.conditions.remove(external_list)
container_list = resourcelists.List(name=self.unique_name('presrules_group', (list.name for list in resource_lists), skip_preferred_name=True).next())
container_list.update(resourcelists.External(uri) for uri in chain(*external_lists))
resource_lists.add(container_list)
path = self.resource_lists.uri + '/~~/resource-lists/list[@name="%s"]' % container_list.name
rule.conditions.add(omapolicy.ExternalList([path]))
if len(rlist) == 0:
path = self.resource_lists.uri + '/~~' + resource_lists.get_xpath(rlist)
container_list.add(resourcelists.External(path))
else:
container_list.add(resourcelists.Entry(operation.contact.uri))
else:
path = self.resource_lists.uri + '/~~' + resource_lists.get_xpath(rlist)
rule.conditions.add(omapolicy.ExternalList([path]))
else:
rule.conditions.add(common_policy.Identity([common_policy.IdentityOne(operation.contact.uri)]))
self.pres_rules.dirty = True
if self.rls_services.supported and (add_to_presence_list or add_to_dialoginfo_list):
if operation.contact.subscribe_to_presence and not operation.contact.subscribe_to_dialoginfo:
good_lists = presence_lists - dialoginfo_lists
elif not operation.contact.subscribe_to_presence and operation.contact.subscribe_to_dialoginfo:
good_lists = dialoginfo_lists - presence_lists
else:
good_lists = presence_lists & dialoginfo_lists
if rlist not in good_lists:
rls_services = self.rls_services.content
try:
# First, find those lists which are according to what we want (good_lists) and are not unwanted
container_lists = [(l for l in good_lists if l not in not_wanted_lists).next()]
except StopIteration:
# Then find separate lists which are not unwanted
container_lists = []
if add_to_presence_list:
try:
container_lists.append((l for l in presence_lists if l not in not_wanted_lists).next())
except StopIteration:
# Too bad, we have to create a new service
service_list = resourcelists.List(name=self.unique_name('subscribe_group', (list.name for list in resource_lists), skip_preferred_name=True).next())
resource_lists.add(service_list)
path = self.resource_lists.uri + '/~~/resource-lists/list[@name="%s"]' % service_list.name
# Find a new uri
uri_re = re.compile(r'sip:(buddies_.*)@%s' % self.account.id.domain)
taken_names = [m.group(1) for m in (uri_re.match(service.uri) for service in rls_services) if m]
uri = 'sip:%s@%s' % (self.unique_name('buddies', taken_names, skip_preferred_name=True).next(), self.account.id.domain)
# We'll also make it a dialog service just so that it can also be used as a container for dialog
service = rlsservices.Service(uri=uri, list=rlsservices.ResourceList(path), packages=['presence', 'dialog'] if add_to_dialoginfo_list and rlist not in dialoginfo_lists else ['presence'])
rls_services.add(service)
container_lists.append(service_list)
add_to_dialoginfo_list = False
if add_to_dialoginfo_list:
try:
container_lists.append((l for l in dialoginfo_lists if l not in not_wanted_lists).next())
except StopIteration:
# Too bad, we have to create a new service
service_list = resourcelists.List(name=self.unique_name('subscribe_group', (list.name for list in resource_lists), skip_preferred_name=True).next())
resource_lists.add(service_list)
path = self.resource_lists.uri + '/~~/resource-lists/list[@name="%s"]' % service_list.name
# Find a new uri
uri_re = re.compile(r'sip:(buddies_.*)@%s' % self.account.id.domain)
taken_names = [m.group(1) for m in (uri_re.match(service.uri) for service in rls_services) if m]
uri = 'sip:%s@%s' % (self.unique_name('buddies', taken_names, skip_preferred_name=True).next(), self.account.id.domain)
# We'll also make it a presence service just so that it can also be used as a container for presence
service = rlsservices.Service(uri=uri, list=rlsservices.ResourceList(path), packages=['presence', 'dialog'] if operation.contact.subscribe_to_presence and rlist not in presence_lists else ['dialog'])
rls_services.add(service)
container_lists = [service_list] # Don't use the previously determined presence service
for container_list in container_lists:
if container_list is rlist:
continue
container_list.add(resourcelists.Entry(operation.contact.uri))
self.rls_services.dirty = True
if rlist is not fallback_candidate:
resource_lists.remove(fallback_candidate)
elif operation.contact.group is not None:
path = self.resource_lists.uri + '/~~' + resource_lists.get_xpath(rlist)
oma_buddylist.add(resourcelists.External(path))
# After all this trouble, we've found a list that can take our new contact: add it to the list!
entry = resourcelists.Entry(uri=operation.contact.uri, display_name=operation.contact.name)
if operation.contact.attributes:
entry.attributes = resourcelists.Entry.attributes.type(operation.contact.attributes)
rlist.add(entry)
self.resource_lists.dirty = True
def _OH_update_contact(self, operation):
if not self.resource_lists.supported:
return
resource_lists = self.resource_lists.content
try:
oma_buddylist = (child for child in resource_lists if isinstance(child, resourcelists.List) and child.name=='oma_buddylist').next()
except StopIteration:
# This should never happen as the document is normalized
return
if not set(['uri', 'group', 'presence_policies', 'dialoginfo_policies', 'subscribe_to_presence', 'subscribe_to_dialoginfo']).intersection(operation.attributes):
# If none of these attributes are specified, then we only need to look at the resource-lists document
# and since we prefer keeping the name and additional attributes in the buddies entry, start with oma_buddylist
# and do a DFS.
notexpanded = deque([oma_buddylist]+[l for l in resource_lists if l is not oma_buddylist])
visited = set(notexpanded)
entries = []
while notexpanded:
rlist = notexpanded.popleft()
for child in rlist:
if isinstance(child, resourcelists.List) and child not in visited:
visited.add(child)
notexpanded.appendleft(child)
elif isinstance(child, resourcelists.External):
try:
ref_lists = set(l for l in self._follow_rl_external(resource_lists, child) if l not in visited)
except ValueError:
ref_lists = set()
visited.update(ref_lists)
notexpanded.extendleft(ref_lists)
elif isinstance(child, resourcelists.EntryRef):
try:
ref_entries = self._follow_rl_entry_ref(resource_lists, child)
except ValueError:
continue
entries.extend(entry for entry in ref_entries if entry.uri==operation.contact.uri)
elif isinstance(child, resourcelists.Entry) and child.uri == operation.contact.uri:
entries.append(child)
if entries:
entry = entries[0]
else:
# This contact might be referenced only from a rls-services, pres-rules or dialog-rules document; create a list
# for any additional information we want to store about the contact
rlist = resourcelists.List(name=self.unique_name('group', (list.name for list in resource_lists), skip_preferred_name=True).next())
resource_lists.add(rlist)
entry = resourcelists.Entry(uri=operation.contact.uri, display_name=operation.contact.name)
rlist.add(entry)
if 'name' in operation.attributes:
entry.display_name = operation.attributes.pop('name')
if operation.attributes and entry.attributes is None:
entry.attributes = resourcelists.Entry.attributes.type()
entry.attributes.update(operation.attributes)
self.resource_lists.dirty = True
else:
# We may need to move the contact to a different place; the logic would be too complicated, so first remove the contact and then add it.
# We retrieve any missing information from what currently exists
contact = Contact(operation.attributes.pop('name', Null), operation.attributes.pop('uri', operation.contact.uri), operation.attributes.pop('group', Null))
contact.presence_policies = operation.attributes.pop('presence_policies', Null)
contact.dialoginfo_policies = operation.attributes.pop('dialoginfo_policies', Null)
contact.subscribe_to_presence = operation.attributes.pop('subscribe_to_presence', Null)
contact.subscribe_to_dialoginfo = operation.attributes.pop('subscribe_to_dialoginfo', Null)
contact.attributes = dict(operation.attributes)
if contact.presence_policies is None:
contact.presence_policies = set()
elif contact.presence_policies is not Null and self.oma_compliant:
# Filter out policies we mustn't add contacts to
contact.presence_policies = set(policy for policy in contact.presence_policies if policy.id not in ('wp_prs_unlisted', 'wp_prs_allow_unlisted', 'wp_prs_block_anonymous', 'wp_prs_allow_own'))
if contact.dialoginfo_policies is None:
contact.dialoginfo_policies = set()
elif contact.dialoginfo_policies is not Null:
contact.dialoginfo_policies = set(contact.dialoginfo_policies)
presence_lists = set()
dialoginfo_lists = set()
list_policies = {} # Maps a resourcelists.List to a list of PresencePolicy objects
buddy_lists = set([oma_buddylist])
if self.rls_services.supported and Null in (contact.subscribe_to_presence, contact.subscribe_to_dialoginfo):
# Only bother reading the rls services document if we don't change at least one of the subsccribe_to_* flags
rls_services = self.rls_services.content
for service in rls_services:
packages = set(service.packages or [])
if isinstance(service.list, rlsservices.RLSList):
expanded_list = [service.list]
elif isinstance(service.list, rlsservices.ResourceList):
try:
expanded_list = self._follow_rls_resource_list(resource_lists, service.list)
except ValueError:
expanded_list = []
else:
expanded_list = []
if 'presence' in packages:
presence_lists.update(expanded_list)
if 'dialog' in packages:
dialoginfo_lists.update(expanded_list)
if self.pres_rules.supported and contact.presence_policies is Null:
pres_rules = self.pres_rules.content
contact.presence_policies = set()
for rule in pres_rules:
try:
action = (action.value for action in (rule.actions or []) if isinstance(action, presrules.SubHandling)).next()
except StopIteration:
continue # Ignore rules whose actions we don't understand
policy = PresencePolicy(rule.id, action)
for condition in (rule.conditions or []):
if isinstance(condition, omapolicy.ExternalList):
try:
ref_lists = self._follow_policy_external_list(resource_lists, condition)
except ValueError:
continue
else:
for ref_list in ref_lists:
list_policies.setdefault(ref_list, set()).add(policy)
elif isinstance(condition, common_policy.Identity):
if condition.matches(operation.contact.uri):
contact.presence_policies.add(policy)
break
if self.dialog_rules.supported and contact.dialoginfo_policies is Null:
dialog_rules = self.dialog_rules.content
contact.dialoginfo_policies = set()
for rule in dialog_rules:
try:
action = (action.value for action in (rule.actions or []) if isinstance(action, dialogrules.SubHandling)).next()
except StopIteration:
continue # Ignore rules whose actions we don't understand
policy = DialoginfoPolicy(rule.id, action)
for condition in (rule.conditions or []):
if isinstance(condition, common_policy.Identity) and condition.matches(operation.contact.uri):
contact.dialoginfo_policies.add(policy)
break
notexpanded = deque((l, l.display_name.value if l.display_name else None) for l in resource_lists if isinstance(l, resourcelists.List))
visited = set(notexpanded)
while notexpanded:
rlist, list_name = notexpanded.popleft()
# Some children will need to be revisited so that their descendents are added to appropriate sets
for child in rlist:
if isinstance(child, resourcelists.List):
revisit = False
if rlist in presence_lists:
revisit = (child not in presence_lists) or revisit
presence_lists.add(child)
if rlist in dialoginfo_lists:
revisit = (child not in dialoginfo_lists) or revisit
dialoginfo_lists.add(child)
if rlist in list_policies:
revisit = (child not in list_policies or not list_policies[child].issuperset(list_policies[rlist])) or revisit
list_policies.setdefault(child, set()).update(list_policies[rlist])
if rlist in buddy_lists:
revisit = (child not in buddy_lists) or revisit
buddy_lists.add(child)
if child not in visited or revisit:
visited.add(child)
notexpanded.append((child, child.display_name.value if child.display_name else list_name))
elif isinstance(child, resourcelists.External):
try:
ref_lists = self._follow_rl_external(resource_lists, child)
except ValueError:
ref_lists = []
revisit = set()
if rlist in presence_lists:
revisit.update(l for l in ref_lists if l not in presence_lists)
presence_lists.update(ref_lists)
if rlist in dialoginfo_lists:
revisit.update(l for l in ref_lists if l not in dialoginfo_lists)
dialoginfo_lists.update(ref_lists)
if rlist in list_policies:
revisit.update(l for l in ref_lists if l not in list_policies or not list_policies[l].issuperset(list_policies[rlist]))
for l in ref_lists:
list_policies.setdefault(l, set()).update(list_policies[rlist])
if rlist in buddy_lists:
revisit.update(l for l in ref_lists if l not in buddy_lists)
buddy_lists.update(ref_lists)
visited.update(l for l in ref_lists if l not in visited)
if child.display_name:
notexpanded.extend((l, child.display_name.value) for l in ref_lists if l not in visited or l in revisit)
else:
notexpanded.extend((l, l.display_name.value if l.display_name else list_name) for l in ref_lists if l not in visited or l in revisit)
elif isinstance(child, resourcelists.EntryRef):
try:
entries = self._follow_rl_entry_ref(resource_lists, child)
except ValueError:
continue
if any(entry.uri==operation.contact.uri for entry in entries):
if rlist in list_policies:
contact.presence_policies.update(list_policies[rlist])
if contact.name is Null and rlist in buddy_lists and child.display_name:
contact.name = child.display_name.value
for entry in (e for e in entries if e.uri==operation.contact.uri):
if contact.group is Null and rlist in buddy_lists and list_name is not None:
contact.group = list_name
if contact.name is Null and rlist in buddy_lists and entry.display_name:
contact.name = entry.display_name.value
if contact.subscribe_to_presence is Null and rlist in presence_lists:
contact.subscribe_to_presence = True
if contact.subscribe_to_dialoginfo is Null and rlist in dialoginfo_lists:
contact.subscribe_to_dialoginfo = True
if entry.attributes and rlist in buddy_lists:
for key in (key for key in entry.attributes if key not in contact.attributes):
contact.attributes[key] = entry.attributes[key]
elif isinstance(child, resourcelists.Entry):
if child.uri == operation.contact.uri:
if rlist in list_policies:
contact.presence_policies.update(list_policies[rlist])
if contact.group is Null and rlist in buddy_lists and list_name is not None:
contact.group = list_name
if contact.name is Null and rlist in buddy_lists and child.display_name:
contact.name = child.display_name.value
if contact.subscribe_to_presence is Null and rlist in presence_lists:
contact.subscribe_to_presence = True
if contact.subscribe_to_dialoginfo is Null and rlist in dialoginfo_lists:
contact.subscribe_to_dialoginfo = True
if child.attributes and rlist in buddy_lists:
for key in (key for key in child.attributes if key not in contact.attributes):
contact.attributes[key] = child.attributes[key]
if contact.name is Null:
contact.name = operation.contact.uri
if contact.group is Null:
# We still don't know where to add this contact, just assume we don't want it as a buddy
contact.group = operation.contact.group
contact.presence_policies = list(contact.presence_policies) if contact.presence_policies is not Null else operation.contact.presence_policies
contact.dialoginfo_policies = list(contact.dialoginfo_policies) if contact.dialoginfo_policies is not Null else operation.contact.dialoginfo_policies
if contact.subscribe_to_presence is Null:
contact.subscribe_to_presence = operation.contact.subscribe_to_presence
if contact.subscribe_to_dialoginfo is Null:
contact.subscribe_to_dialoginfo = operation.contact.subscribe_to_dialoginfo
# Now we can delete the contact and add it again
ops = [RemoveContactOperation(contact=operation.contact), AddContactOperation(contact=contact)]
for op in ops:
handler = getattr(self, '_OH_%s' % op.name)
handler(op)
def _OH_remove_contact(self, operation):
if not self.resource_lists.supported:
return
resource_lists = self.resource_lists.content
try:
oma_buddylist = (child for child in resource_lists if isinstance(child, resourcelists.List) and child.name=='oma_buddylist').next()
except StopIteration:
# This should never happen as the document is normalized
return
lists = set([oma_buddylist])
to_remove = []
# Get all rls lists
if self.rls_services.supported:
rls_services = self.rls_services.content
for service in rls_services:
packages = set(service.packages or [])
if isinstance(service.list, rlsservices.RLSList):
expanded_list = [service.list]
elif isinstance(service.list, rlsservices.ResourceList):
try:
expanded_list = self._follow_rls_resource_list(resource_lists, service.list)
except ValueError:
expanded_list = []
else:
expanded_list = []
if 'presence' in packages or 'dialog' in packages:
lists.update(expanded_list)
# Get all pres-rules lists and update the pres-rules document so that it doesn't contain references to this contact
if self.pres_rules.supported:
pres_rules = self.pres_rules.content
for rule in pres_rules:
try:
action = (action.value for action in (rule.actions or []) if isinstance(action, presrules.SubHandling)).next()
except StopIteration:
continue # Ignore rules whose actions we don't understand
for condition in (rule.conditions or []):
if isinstance(condition, omapolicy.ExternalList):
try:
ref_lists = self._follow_policy_external_list(resource_lists, condition)
except ValueError:
continue
else:
lists.update(ref_lists)
elif isinstance(condition, common_policy.Identity):
for identity in condition:
if isinstance(identity, common_policy.IdentityOne) and identity.id == operation.contact.uri:
to_remove.append((identity, condition))
self.pres_rules.dirty = True
elif isinstance(identity, common_policy.IdentityMany) and identity.matches(operation.contact.uri):
identity.add(common_policy.IdentityExcept(operation.contact.uri))
self.pres_rules.dirty = True
# Update the dialoginfo rules
if self.dialog_rules.supported:
dialog_rules = self.dialog_rules.content
# Remove any reference to the uri and add it to the correct rule
for rule in dialog_rules:
try:
action = (action.value for action in (rule.actions or []) if isinstance(action, dialogrules.SubHandling)).next()
except StopIteration:
continue # Ignore rules whose actions we don't understand
for condition in (rule.conditions or []):
if isinstance(condition, common_policy.Identity):
for identity in condition:
if isinstance(identity, common_policy.IdentityOne) and identity.id == operation.contact.uri:
to_remove.append((identity, condition))
self.dialog_rules.dirty = True
elif isinstance(identity, common_policy.IdentityMany) and identity.matches(operation.contact.uri):
identity.add(common_policy.IdentityExcept(operation.contact.uri))
self.dialog_rules.dirty = True
notexpanded = deque(lists)
visited = set(notexpanded)
while notexpanded:
rlist = notexpanded.popleft()
for child in rlist:
if isinstance(child, resourcelists.List) and child not in visited:
visited.add(child)
notexpanded.append(child)
elif isinstance(child, resourcelists.External):
try:
ref_lists = set(l for l in self._follow_rl_external(resource_lists, child) if l not in visited)
except ValueError:
ref_lists = set()
visited.update(ref_lists)
notexpanded.extend(ref_lists)
elif isinstance(child, resourcelists.EntryRef):
try:
entries = self._follow_rl_entry_ref(resource_lists, child)
except ValueError:
continue
if any(entry.uri == operation.contact.uri for entry in entries):
to_remove.append((child, rlist))
self.resource_lists.dirty = True
self.rls_services.dirty = True
elif isinstance(child, resourcelists.Entry):
if child.uri == operation.contact.uri:
to_remove.append((child, rlist))
self.resource_lists.dirty = True
self.rls_services.dirty = True
for child, parent in to_remove:
parent.remove(child)
# Identity elements can't be empty
if self.pres_rules.supported:
pres_rules = self.pres_rules.content
for rule in pres_rules:
try:
action = (action.value for action in (rule.actions or []) if isinstance(action, presrules.SubHandling)).next()
except StopIteration:
continue # Ignore rules whose actions we don't understand
for condition in (condition for condition in list(rule.conditions or []) if isinstance(condition, common_policy.Identity)):
if len(condition) == 0:
rule.conditions.remove(condition)
rule.conditions.add(common_policy.FalseCondition())
if self.dialog_rules.supported:
dialog_rules = self.dialog_rules.content
for rule in dialog_rules:
try:
action = (action.value for action in (rule.actions or []) if isinstance(action, dialogrules.SubHandling)).next()
except StopIteration:
continue # Ignore rules whose actions we don't understand
for condition in (condition for condition in list(rule.conditions or []) if isinstance(condition, common_policy.Identity)):
if len(condition) == 0:
rule.conditions.remove(condition)
rule.conditions.add(common_policy.FalseCondition())
def _OH_add_presence_policy(self, operation):
if not self.pres_rules.supported or operation.policy.id in ('wp_prs_unlisted', 'wp_prs_allow_unlisted', 'wp_prs_grantedcontacts', 'wp_prs_blockedcontacts', 'wp_prs_block_anonymous', 'wp_prs_allow_own'):
return
if operation.policy.id is not None and (operation.policy.id.startswith('wp_prs_allow_one_') or operation.policy.id.startswith('wp_prs_one_')):
return
if operation.policy.action is None:
return
pres_rules = self.pres_rules.content
if operation.policy.id is None and self.oma_compliant and not operation.policy.multi_identity_conditions:
operation.policy.id = self.unique_name('wp_prs_allow_onelist_presrules_group' if operation.policy.action=='allow' else 'wp_prs_onelist_presrules_group', (rule.id for rule in pres_rules), skip_preferred_name=True).next()
elif operation.policy.id is None:
operation.policy.id = self.unique_name('rule', (rule.id for rule in pres_rules), skip_preferred_name=True).next()
elif operation.policy.id in pres_rules:
return
elif (operation.policy.id.startswith('wp_prs_allow_onelist_') or operation.policy.id.startswith('wp_prs_onelist_')) and operation.policy.multi_identity_conditions:
operation.policy.id = self.unique_name('rule', (rule.id for rule in pres_rules), skip_preferred_name=True).next()
rule = common_policy.Rule(operation.policy.id, conditions=[], actions=[], transformations=[])
#rule.display_name = operation.policy.name # Rule display-name extension
rule.actions.add(presrules.SubHandling(operation.policy.action))
if operation.policy.sphere:
rule.conditions.add(common_policy.Sphere(operation.policy.sphere))
if operation.policy.validity:
rule.conditions.add(common_policy.Validity(operation.policy.validity))
if operation.policy.action == 'allow':
if operation.policy.provide_devices is All:
rule.transformations.add(presrules.ProvideDevices(all=True))
elif operation.policy.provide_devices:
provide_devices = presrules.ProvideDevices()
for component in provide_devices:
if isinstance(component, Class):
provide_devices.add(presrules.Class(component))
elif isinstance(component, OccurenceID):
provide_devices.add(presrules.OccurenceID(component))
elif isinstance(component, DeviceID):
provide_devices.add(presrules.DeviceID(component))
rule.transformations.add(provide_devices)
if operation.policy.provide_persons is All:
rule.transformations.add(presrules.ProvidePersons(all=True))
elif operation.policy.provide_persons:
provide_persons = presrules.ProvidePersons()
for component in provide_persons:
if isinstance(component, Class):
provide_persons.add(presrules.Class(component))
elif isinstance(component, OccurenceID):
provide_persons.add(presrules.OccurenceID(component))
rule.transformations.add(provide_persons)
if operation.policy.provide_services is All:
rule.transformations.add(presrules.ProvideServices(all=True))
else:
provide_services = presrules.ProvideServices()
for component in provide_services:
if isinstance(component, Class):
provide_services.add(presrules.Class(component))
elif isinstance(component, OccurenceID):
provide_services.add(presrules.OccurenceID(component))
elif isinstance(component, ServiceURI):
provide_services.add(presrules.ServiceURI(component))
elif isinstance(component, ServiceURIScheme):
provide_services.add(presrules.ServiceURIScheme(component))
rule.transformations.add(provide_services)
if operation.policy.provide_all_attributes:
rule.transformations.add(presrules.ProvideAllAttributes())
else:
attribute_class = {'provide_activities': presrules.ProvideActivities,
'provide_class': presrules.ProvideClass,
'provide_device_id': presrules.ProvideDeviceID,
'provide_mood': presrules.ProvideMood,
'provide_place_is': presrules.ProvidePlaceIs,
'provide_place_type': presrules.ProvidePlaceType,
'provide_privacy': presrules.ProvidePrivacy,
'provide_relationship': presrules.ProvideRelationship,
'provide_status_icon': presrules.ProvideStatusIcon,
'provide_sphere': presrules.ProvideSphere,
'provide_time_offset': presrules.ProvideTimeOffset,
'provide_user_input': presrules.ProvideUserInput,
'provide_unknown_attributes': presrules.ProvideUnknownAttribute}
for attribute, cls in attribute_class.iteritems():
value = getattr(operation.policy, attribute)
if value is not None:
rule.transformations.add(cls(value))
if self.oma_compliant and self.resource_lists.supported and (rule.id.startswith('wp_prs_allow_onelist_') or rule.id.startswith('wp_prs_onelist_')):
resource_lists = self.resource_lists.content
preferred_name = rule.id[len('wp_prs_allow_onelist_'):] if rule.id.startswith('wp_prs_allow_onelist') else rule.id[len('wp_prs_onelist_'):]
rlist = resourcelists.List(name=self.unique_name(preferred_name, (list.name for list in resource_lists)).next())
resource_lists.add(rlist)
path = self.resource_lists.uri + '/~~' + resource_lists.get_xpath(rlist)
rule.conditions.add(omapolicy.ExternalList([path]))
self.resource_lists.dirty = True
else:
identity_condition = common_policy.Identity()
for multi_identity_condition in (operation.policy.multi_identity_conditions or []):
if isinstance(multi_identity_condition, CatchAllCondition):
condition = common_policy.IdentityMany()
identity_condition.add(condition)
for exception in multi_identity_condition.exceptions:
if isinstance(exception, DomainException):
condition.add(common_policy.IdentityExcept(domain=exception.domain))
elif isinstance(exception, UserException):
condition.add(common_policy.IdentityExcept(id=exception.uri))
elif isinstance(multi_identity_condition, DomainCondition):
condition = common_policy.IdentityMany(domain=multi_identity_condition.domain)
identity_condition.add(condition)
for exception in multi_identity_condition.exceptions:
if isinstance(exception, UserException):
condition.add(common_policy.IdentityExcept(id=exception.uri))
if len(identity_condition) == 0:
rule.conditions.add(common_policy.FalseCondition())
else:
rule.conditions.add(identity_condition)
pres_rules.add(rule)
self.pres_rules.dirty = True
def _OH_update_presence_policy(self, operation):
if not self.pres_rules.supported or operation.policy.id in ('wp_prs_unlisted', 'wp_prs_allow_unlisted', 'wp_prs_grantedcontacts', 'wp_prs_blockedcontacts', 'wp_prs_block_anonymous', 'wp_prs_allow_own'):
return
pres_rules = self.pres_rules.content
if operation.policy.id is None or operation.policy.id not in pres_rules:
if 'action' not in operation.attributes:
# We cannot assume what this policy's action was
return
policy = PresencePolicy(operation.policy.id, operation.attributes.pop('action'))
if 'name' in operation.attributes:
policy.name = operation.attributes.pop('name')
if 'sphere' in operation.attributes:
policy.sphere = operation.attributes.pop('sphere')
if 'validity' in operation.attributes:
policy.validity = operation.attributes.pop('validity')
if 'multi_identity_conditions' in operation.attributes:
policy.multi_identity_conditions = operation.attributes.pop('multi_identity_conditions')
if 'provide_devices' in operation.attributes:
policy.provide_devices = operation.attributes.pop('provide_devices')
if 'provide_persons' in operation.attributes:
policy.provide_persons = operation.attributes.pop('provide_persons')
if 'provide_services' in operation.attributes:
policy.provide_services = operation.attributes.pop('provide_services')
if operation.attributes.get('provide_all_attributes', False):
policy.provide_all_attributes = operation.attributes.pop('provide_all_attributes')
else:
policy.provide_all_attributes = operation.attributes.pop('provide_all_attributes', False)
for key, value in operation.attributes.iteritems():
setattr(policy, key, value)
op = AddPresencePolicyOperation(policy=policy)
handler = getattr(self, '_OH_%s' % op.name)
handler(op)
return
rule = pres_rules[operation.policy.id]
if (operation.attributes.get('multi_identity_conditions', None) and (rule.id.startswith('wp_prs_allow_onelist_') or
rule.id.startswith('wp_prs_onelist') or any(isinstance(condition, omapolicy.ExternalList) for condition in (rule.conditions or [])))):
# We canot add multi identity conditions to this rule, so create a new one using whatever data from the old one
try:
action = (action.value for action in (rule.actions or []) if isinstance(action, presrules.SubHandling)).next()
except StopIteration:
action = None
policy = PresencePolicy(None, operation.attributes.pop('action', action))
policy.name = operation.attributes.pop('name', None) # rule.display_name.value if rule.display_name else None # Rule display-name extension
try:
sphere = (condition.value for condition in (rule.conditions or []) if isinstance(condition, common_policy.Sphere)).next()
except StopIteration:
sphere = None
policy.sphere = operation.attributes.pop('sphere', sphere)
try:
validity = list((condition for condition in (rule.conditions or []) if isinstance(condition, common_policy.Validity)).next())
except StopIteration:
validity = None
policy.validity = operation.attributes.pop('validity', validity)
transformation_attribute = {presrules.ProvideDeviceID: 'provide_device_id',
presrules.ProvidePlaceType: 'provide_place_type',
presrules.ProvidePrivacy: 'provide_privacy',
presrules.ProvideRelationship: 'provide_relationship',
presrules.ProvideUnknownAttribute: 'provide_unknown_attributes',
presrules.ProvidePlaceIs: 'provide_place_is',
presrules.ProvideClass: 'provide_class',
presrules.ProvideUserInput: 'provide_user_input',
presrules.ProvideTimeOffset: 'provide_time_offset',
presrules.ProvideStatusIcon: 'provide_status_icon',
presrules.ProvideMood: 'provide_mood',
presrules.ProvideActivities: 'provide_activities',
presrules.ProvideSphere: 'provide_sphere'}
policy.provide_all_attributes = rule.transformations is None
for transformation in (rule.transformations or []):
if transformation.__class__ in transformation_attribute:
setattr(policy, transformation_attribute[transformation.__class__], transformation.value)
elif isinstance(transformation, presrules.ProvideAllAttributes):
policy.provide_all_attributes = True
elif isinstance(transformation, presrules.ProvideDevices):
if All in transformation:
policy.provide_devices = All
else:
policy.provide_devices = []
for component in transformation:
if isinstance(component, presrules.Class):
policy.provide_devices.append(Class(component.value))
elif isinstance(component, presrules.OccurenceID):
policy.provide_devices.append(OccurenceID(component.value))
elif isinstance(component, presrules.DeviceID):
policy.provide_devices.append(DeviceID(component.value))
elif isinstance(transformation, presrules.ProvidePersons):
if All in transformation:
policy.provide_persons = All
else:
policy.provide_persons = []
for component in transformation:
if isinstance(component, presrules.Class):
policy.provide_persons.append(Class(component.value))
elif isinstance(component, presrules.OccurenceID):
policy.provide_persons.append(OccurenceID(component.value))
elif isinstance(transformation, presrules.ProvideServices):
if All in transformation:
policy.provide_services = All
else:
policy.provide_services = []
for component in transformation:
if isinstance(component, presrules.Class):
policy.provide_services.append(Class(component.value))
elif isinstance(component, presrules.OccurenceID):
policy.provide_services.append(OccurenceID(component.value))
elif isinstance(component, presrules.ServiceURI):
policy.provide_services.append(ServiceURI(component.value))
elif isinstance(component, presrules.ServiceURIScheme):
policy.provide_services.append(ServiceURIScheme(component.value))
policy.provide_devices = operation.attributes.pop('provide_devices', policy.provide_devices)
policy.provide_persons = operation.attributes.pop('provide_persons', policy.provide_persons)
policy.provide_services = operation.attributes.pop('provide_persons', policy.provide_persons)
if operation.attributes.get('provide_all_attributes', policy.provide_all_attributes):
policy.provide_all_attributes = operation.attributes.pop('provide_all_attributes', True)
else:
policy.provide_all_attributes = operation.attributes.pop('provide_all_attributes', False)
for key, value in operation.attributes.iteritems():
setattr(policy, key, value)
op = AddPresencePolicyOperation(policy=policy)
handler = getattr(self, '_OH_%s' % op.name)
handler(op)
return
if rule.conditions is None:
rule.conditions = []
if rule.transformations is None:
rule.transformations = []
if 'name' in operation.attributes:
# rule.display_name = operation.attributes.pop('name') # Rule display-name extension
operation.attributes.pop('name')
if 'action' in operation.attributes:
rule.actions = [presrules.SubHandling(operation.attributes.pop('action'))]
if 'sphere' in operation.attributes:
sphere = operation.attributes.pop('sphere')
try:
condition = (condition for condition in rule.conditions if isinstance(condition, common_policy.Sphere)).next()
except StopIteration:
if sphere is not None:
rule.conditions.add(common_policy.Sphere(sphere))
else:
if sphere is not None:
condition.value = sphere
else:
rule.conditions.remove(condition)
if 'validity' in operation.attributes:
validity = operation.attributes.pop('validity')
try:
condition = (condition for condition in rule.conditions if isinstance(condition, common_policy.Validity)).next()
except StopIteration:
if validity is not None:
rule.conditions.add(common_policy.Validity(validity))
else:
if validity is not None:
condition.clear()
condition.add(validity)
else:
rule.conditions.remove(condition)
try:
action = (action.value for action in (rule.actions or []) if isinstance(action, presrules.SubHandling)).next()
except StopIteration:
action = None
if action == 'allow':
attribute_class = {'provide_devices': presrules.ProvideDevices,
'provide_persons': presrules.ProvidePersons,
'provide_services': presrules.ProvideServices}
for attribute, cls in attribute_class.iteritems():
value = operation.attributes.pop('provide_devices', Null)
if value is Null:
continue
try:
transformation = (transformation for transformation in rule.transformations if isinstance(transformation, cls)).next()
except StopIteration:
if value is All:
rule.transformations.add(cls([All]))
elif value is not None:
rule.transformations.add(cls(value))
else:
if value is All:
transformation.add(All)
elif value is not None:
transformation.clear()
for component in value:
if isinstance(component, Class):
transformation.add(presrules.Class(component))
elif isinstance(component, OccurenceID):
transformation.add(presrules.OccurenceID(component))
elif isinstance(component, DeviceID) and attribute == 'provide_devices':
transformation.add(presrules.DeviceID(component))
elif isinstance(component, ServiceURI) and attribute == 'provide_services':
transformation.add(presrules.ServiceURI(component))
elif isinstance(component, ServiceURIScheme) and attribute == 'provide_services':
transformation.add(presrules.ServiceURIScheme(component))
else:
rule.transformations.remove(transformation)
provide_all_attributes = operation.attributes.pop('provide_all_attributes', Null)
if provide_all_attributes is True:
rule.transformations.clear()
rule.transformations.add(presrules.ProvideAllAttributes())
elif not (provide_all_attributes is Null and any(isinstance(transformation, presrules.ProvideAllAttributes) for transformation in rule.transformations)):
if provide_all_attributes is False:
for transformation in [t for t in rule.transformations if isinstance(t, presrules.ProvideAllAttributes)]:
rule.transformations.remove(transformation)
attribute_class = {'provide_activities': presrules.ProvideActivities,
'provide_class': presrules.ProvideClass,
'provide_device_id': presrules.ProvideDeviceID,
'provide_mood': presrules.ProvideMood,
'provide_place_is': presrules.ProvidePlaceIs,
'provide_place_type': presrules.ProvidePlaceType,
'provide_privacy': presrules.ProvidePrivacy,
'provide_relationship': presrules.ProvideRelationship,
'provide_status_icon': presrules.ProvideStatusIcon,
'provide_sphere': presrules.ProvideSphere,
'provide_time_offset': presrules.ProvideTimeOffset,
'provide_user_input': presrules.ProvideUserInput,
'provide_unknown_attributes': presrules.ProvideUnknownAttribute}
for attribute, cls in attribute_class.iteritems():
value = operation.attributes.pop(attribute, Null)
if value is Null:
continue
try:
transformation = (transformation for transformation in rule.transformations if isinstance(transformation, cls)).next()
except StopIteration:
if value is not None:
rule.transformations.add(cls(value))
else:
if value is not None:
transformation.value = value
else:
rule.transformations.remove(transformation)
elif self.oma_compliant:
# No transformations are allowed if action is not 'allow'
rule.transformations = []
if not any(isinstance(condition, (common_policy.Identity, omapolicy.ExternalList)) for condition in rule.conditions):
if self.oma_compliant and self.resource_lists.supported and (rule.id.startswith('wp_prs_allow_onelist_') or rule.id.startswith('wp_prs_onelist_')):
resource_lists = self.resource_lists.content
preferred_name = rule.id[len('wp_prs_allow_onelist_'):] if rule.id.startswith('wp_prs_allow_onelist') else rule.id[len('wp_prs_onelist_'):]
rlist = resourcelists.List(name=self.unique_name(preferred_name, (list.name for list in resource_lists)).next())
resource_lists.add(rlist)
path = self.resource_lists.uri + '/~~' + resource_lists.get_xpath(rlist)
rule.conditions.add(omapolicy.ExternalList([path]))
self.resource_lists.dirty = True
try:
identity_condition = (condition for condition in rule.conditions if isinstance(condition, common_policy.Identity)).next()
except StopIteration:
identity_condition = common_policy.Identity()
if 'multi_identity_conditions' in operation.attributes:
for multi_identity_condition in [id_condition for id_condition in identity_condition if isinstance(id_condition, common_policy.IdentityMany)]:
identity_condition.remove(multi_identity_condition)
for multi_identity_condition in (operation.attributes.pop('multi_identity_conditions') or []):
if isinstance(multi_identity_condition, CatchAllCondition):
condition = common_policy.IdentityMany()
identity_condition.add(condition)
for exception in multi_identity_condition.exceptions:
if isinstance(exception, DomainException):
condition.add(common_policy.IdentityExcept(domain=exception.domain))
elif isinstance(exception, UserException):
condition.add(common_policy.IdentityExcept(id=exception.uri))
elif isinstance(multi_identity_condition, DomainCondition):
condition = common_policy.IdentityMany(domain=multi_identity_condition.domain)
identity_condition.add(condition)
for exception in multi_identity_condition.exceptions:
if isinstance(exception, UserException):
condition.add(common_policy.IdentityExcept(id=exception.uri))
# Identity condition can't be empty
if not identity_condition:
rule.conditions.add(common_policy.FalseCondition())
else:
rule.conditions.add(identity_condition)
self.pres_rules.dirty = True
def _OH_remove_presence_policy(self, operation):
if not self.pres_rules.supported or operation.policy.id in (None, 'wp_prs_unlisted', 'wp_prs_allow_unlisted', 'wp_prs_grantedcontacts', 'wp_prs_blockedcontacts', 'wp_prs_block_anonymous', 'wp_prs_allow_own'):
return
pres_rules = self.pres_rules.content
try:
del pres_rules[operation.policy.id]
except KeyError:
return
self.pres_rules.dirty = True
def _OH_add_dialoginfo_policy(self, operation):
if not self.dialog_rules.supported:
return
dialog_rules = self.dialog_rules.content
if operation.policy.id is None:
operation.policy.id = self.unique_name('rule', (rule.id for rule in dialog_rules), skip_preferred_name=True).next()
elif operation.policy.id in dialog_rules:
return
rule = common_policy.Rule(operation.policy.id, conditions=[], actions=[])
#rule.display_name = operation.policy.name # Rule display-name extension
rule.actions.add(dialogrules.SubHandling(operation.policy.action))
if operation.policy.sphere:
rule.conditions.add(common_policy.Sphere(operation.policy.sphere))
if operation.policy.validity:
rule.conditions.add(common_policy.Validity(operation.policy.validity))
identity_condition = common_policy.Identity()
for multi_identity_condition in (operation.policy.multi_identity_conditions or []):
if isinstance(multi_identity_condition, CatchAllCondition):
condition = common_policy.IdentityMany()
identity_condition.add(condition)
for exception in multi_identity_condition.exceptions:
if isinstance(exception, DomainException):
condition.add(common_policy.IdentityExcept(domain=exception.domain))
elif isinstance(exception, UserException):
condition.add(common_policy.IdentityExcept(id=exception.uri))
elif isinstance(multi_identity_condition, DomainCondition):
condition = common_policy.IdentityMany(domain=multi_identity_condition.domain)
identity_condition.add(condition)
for exception in multi_identity_condition.exceptions:
if isinstance(exception, UserException):
condition.add(common_policy.IdentityExcept(id=exception.uri))
if len(identity_condition) == 0:
rule.conditions.add(common_policy.FalseCondition())
else:
rule.conditions.add(identity_condition)
dialog_rules.add(rule)
self.dialog_rules.dirty = True
def _OH_update_dialoginfo_policy(self, operation):
if not self.dialog_rules.supported:
return
dialog_rules = self.dialog_rules.content
if operation.policy.id is None or operation.policy.id not in dialog_rules:
if 'action' not in operation.attributes:
# We cannot assume what this policy's action was
return
policy = DialoginfoPolicy(operation.policy.id, operation.attributes.pop('action'))
if 'name' in operation.attributes:
policy.name = operation.attributes.pop('name')
if 'sphere' in operation.attributes:
policy.sphere = operation.attributes.pop('sphere')
if 'validity' in operation.attributes:
policy.validity = operation.attributes.pop('validity')
op = AddDialoginfoPolicyOperation(policy=policy)
handler = getattr(self, '_OH_%s' % op.name)
handler(op)
return
rule = dialog_rules[operation.policy.id]
if rule.conditions is None:
rule.conditions = []
if 'name' in operation.attributes:
#rule.display_name = operation.attributes.pop('name') # Rule display-name extension
operation.attributes.pop('name')
if 'action' in operation.attributes:
rule.actions = [dialogrules.SubHandling(operation.attributes.pop('action'))]
if 'sphere' in operation.attributes:
sphere = operation.attributes.pop('sphere')
try:
condition = (condition for condition in rule.conditions if isinstance(condition, common_policy.Sphere)).next()
except StopIteration:
if sphere is not None:
rule.conditions.add(common_policy.Sphere(sphere))
else:
if sphere is not None:
condition.value = sphere
else:
rule.conditions.remove(condition)
if 'validity' in operation.attributes:
validity = operation.attributes.pop('validity')
try:
condition = (condition for condition in rule.conditions if isinstance(condition, common_policy.Validity)).next()
except StopIteration:
if validity is not None:
rule.conditions.add(common_policy.Validity(validity))
else:
if validity is not None:
condition.clear()
condition.add(validity)
else:
rule.conditions.remove(condition)
try:
identity_condition = (condition for condition in rule.conditions if isinstance(condition, common_policy.Identity)).next()
except StopIteration:
identity_condition = common_policy.Identity()
if 'multi_identity_conditions' in operation.attributes:
for multi_identity_condition in [id_condition for id_condition in identity_condition if isinstance(id_condition, common_policy.IdentityMany)]:
identity_condition.remove(multi_identity_condition)
for multi_identity_condition in (operation.attributes.pop('multi_identity_conditions') or []):
if isinstance(multi_identity_condition, CatchAllCondition):
condition = common_policy.IdentityMany()
identity_condition.add(condition)
for exception in multi_identity_condition.exceptions:
if isinstance(exception, DomainException):
condition.add(common_policy.IdentityExcept(domain=exception.domain))
elif isinstance(exception, UserException):
condition.add(common_policy.IdentityExcept(id=exception.uri))
elif isinstance(multi_identity_condition, DomainCondition):
condition = common_policy.IdentityMany(domain=multi_identity_condition.domain)
identity_condition.add(condition)
for exception in multi_identity_condition.exceptions:
if isinstance(exception, UserException):
condition.add(common_policy.IdentityExcept(id=exception.uri))
# Identity condition can't be empty
if len(identity_condition) == 0:
rule.conditions.add(common_policy.FalseCondition())
else:
rule.conditions.add(identity_condition)
self.dialog_rules.dirty = True
def _OH_remove_dialoginfo_policy(self, operation):
if not self.dialog_rules.supported or operation.policy.id is None:
return
dialog_rules = self.dialog_rules.content
try:
del dialog_rules[operation.policy.id]
except KeyError:
return
self.dialog_rules.dirty = True
def _OH_set_status_icon(self, operation):
if not self.status_icon.supported:
return
if operation.icon is None or not operation.icon.data:
self.status_icon.content = None
else:
data = base64.encodestring(operation.icon.data)
mime_type = operation.icon.mime_type if operation.icon.mime_type in ('image/gif', 'image/jpeg', 'image/png') else 'image/jpeg'
self.status_icon.content = prescontent.PresenceContent(data=data, mime_type=mime_type, encoding='base64', description=operation.icon.description)
self.status_icon.dirty = True
def _OH_set_offline_status(self, operation):
if not self.pidf_manipulation.supported:
return
if operation.status is None:
self.pidf_manipulation.content = None
else:
self.pidf_manipulation.content = pidf.PIDF('sip:'+self.account.id)
person = pidf.Person('offline_status')
person.timestamp = pidf.PersonTimestamp()
if operation.status.note:
person.notes.add(pidf.Note(operation.status.note))
if operation.status.activity:
person.activities = rpid.Activities()
person.activities.add(operation.status.activity)
self.pidf_manipulation.content.add(person)
self.pidf_manipulation.dirty = True
# notification handling
#
@run_in_twisted_thread
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, Null)
handler(notification)
def _NH_CFGSettingsObjectDidChange(self, notification):
if set(['__id__', 'xcap.xcap_root', 'auth.username', 'auth.password', 'sip.subscribe_interval', 'sip.transport_list']).intersection(notification.data.modified):
self.command_channel.send(Command('reload', modified=notification.data.modified))
def _NH_CFGSettingsObjectWasDeleted(self, notification):
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=self.account, name='CFGSettingsObjectDidChange')
notification_center.remove_observer(self, sender=self.account, name='CFGSettingsObjectWasDeleted')
self.command_channel.send(Command('stop'))
self.command_channel.send(Command('cleanup'))
def _NH_XCAPSubscriptionDidStart(self, notification):
self.command_channel.send(Command('fetch', documents=self.document_names))
def _NH_XCAPSubscriptionDidFail(self, notification):
self.command_channel.send(Command('fetch', documents=self.document_names))
def _NH_XCAPSubscriptionGotNotify(self, notification):
- if notification.data.content_type == xcapdiff.XCAPDiff.content_type:
+ if notification.data.content_type == xcapdiff.XCAPDiffDocument.content_type:
try:
- xcap_diff = xcapdiff.XCAPDiff.parse(notification.data.body)
+ xcap_diff = xcapdiff.XCAPDiffDocument.parse(notification.data.body)
except ParserError:
self.command_channel.send(Command('fetch', documents=self.document_names))
else:
applications = set(child.selector.auid for child in xcap_diff if isinstance(child, xcapdiff.Document))
documents = [document.name for document in self.documents if document.application in applications]
self.command_channel.send(Command('fetch', documents=documents))
def _load_data(self):
if not self.resource_lists.supported:
return
resource_lists = self.resource_lists.content
try:
oma_buddylist = (child for child in resource_lists if isinstance(child, resourcelists.List) and child.name=='oma_buddylist').next()
except StopIteration:
# This should never happen as the document is normalized
return
list_presence_policies = {} # Maps a resourcelists.List to a list of PresencePolicy objects
presence_policies = {} # Maps a PresencePolicy to a common_policy.Identity condition if the policy contains one, otherwise to None
dialoginfo_policies = {} # Maps a DialoginfoPolicy to a common_policy.Identity condition if the policy contains one, otherwise to None
presence_lists = set() # resourcelists.List objects which are referenced from an rls service with package presence
dialoginfo_lists = set() # resourcelists.List objects which are referenced from an rls service with package dialog
list_services = {} # Maps a resourcelists.List to the set of services which reference the list
buddy_lists = set([oma_buddylist]) # resourcelists.List objects which are referenced from oma_buddylist
contacts = {} # Maps a URI to a Contact object
services = []
groups = set() # Group names
if self.rls_services.supported:
rls_services = self.rls_services.content
for service_element in rls_services:
packages = set(service_element.packages or [])
service = Service(service_element.uri, list(packages))
service.entries = set()
services.append(service)
if isinstance(service_element.list, rlsservices.RLSList):
expanded_list = [service_element.list]
elif isinstance(service_element.list, rlsservices.ResourceList):
try:
expanded_list = self._follow_rls_resource_list(resource_lists, service_element.list)
except ValueError:
expanded_list = []
else:
expanded_list = []
if 'presence' in packages:
presence_lists.update(expanded_list)
if 'dialog' in packages:
dialoginfo_lists.update(expanded_list)
for rlist in expanded_list:
list_services.setdefault(rlist, set()).add(service)
if self.pres_rules.supported:
pres_rules = self.pres_rules.content
for rule in pres_rules:
try:
action = (action.value for action in (rule.actions or []) if isinstance(action, presrules.SubHandling)).next()
except StopIteration:
continue # Ignore rules whose actions we don't understand
policy = PresencePolicy(rule.id, action)
#policy.name = rule.display_name.value if rule.display_name else None # Rule display-name extension
presence_policies[policy] = None
policy.multi_identity_conditions = []
for condition in (rule.conditions or []):
if isinstance(condition, omapolicy.ExternalList):
try:
ref_lists = self._follow_policy_external_list(resource_lists, condition)
except ValueError:
continue
else:
for ref_list in ref_lists:
list_presence_policies.setdefault(ref_list, set()).add(policy)
elif isinstance(condition, common_policy.Identity):
presence_policies[policy] = condition
for identity_many_condition in (identity_condition for identity_condition in condition if isinstance(identity_condition, common_policy.IdentityMany)):
if identity_many_condition.domain:
multi_condition = DomainCondition(identity_many_condition.domain)
else:
multi_condition = CatchAllCondition()
policy.multi_identity_conditions.append(multi_condition)
for exception_condition in (sub_condition for sub_condition in identity_many_condition if isinstance(sub_condition, common_policy.IdentityExcept)):
if exception_condition.domain:
multi_condition.exceptions.append(DomainException(exception_condition.domain))
elif exception_condition.id:
multi_condition.exceptions.append(UserException(exception_condition.id))
elif isinstance(condition, common_policy.Sphere):
policy.sphere = condition.value
elif isinstance(condition, common_policy.Validity):
policy.validity = list(condition)
transformation_attribute = {presrules.ProvideDeviceID: 'provide_device_id',
presrules.ProvidePlaceType: 'provide_place_type',
presrules.ProvidePrivacy: 'provide_privacy',
presrules.ProvideRelationship: 'provide_relationship',
presrules.ProvideUnknownAttribute: 'provide_unknown_attributes',
presrules.ProvidePlaceIs: 'provide_place_is',
presrules.ProvideClass: 'provide_class',
presrules.ProvideUserInput: 'provide_user_input',
presrules.ProvideTimeOffset: 'provide_time_offset',
presrules.ProvideStatusIcon: 'provide_status_icon',
presrules.ProvideMood: 'provide_mood',
presrules.ProvideActivities: 'provide_activities',
presrules.ProvideSphere: 'provide_sphere'}
policy.provide_all_attributes = rule.transformations is None
for transformation in (rule.transformations or []):
if transformation.__class__ in transformation_attribute:
setattr(policy, transformation_attribute[transformation.__class__], transformation.value)
elif isinstance(transformation, presrules.ProvideAllAttributes):
policy.provide_all_attributes = True
elif isinstance(transformation, presrules.ProvideDevices):
if All in transformation:
policy.provide_devices = All
else:
policy.provide_devices = []
for component in transformation:
if isinstance(component, presrules.Class):
policy.provide_devices.append(Class(component.value))
elif isinstance(component, presrules.OccurenceID):
policy.provide_devices.append(OccurenceID(component.value))
elif isinstance(component, presrules.DeviceID):
policy.provide_devices.append(DeviceID(component.value))
elif isinstance(transformation, presrules.ProvidePersons):
if All in transformation:
policy.provide_persons = All
else:
policy.provide_persons = []
for component in transformation:
if isinstance(component, presrules.Class):
policy.provide_persons.append(Class(component.value))
elif isinstance(component, presrules.OccurenceID):
policy.provide_persons.append(OccurenceID(component.value))
elif isinstance(transformation, presrules.ProvideServices):
if All in transformation:
policy.provide_services = All
else:
policy.provide_services = []
for component in transformation:
if isinstance(component, presrules.Class):
policy.provide_services.append(Class(component.value))
elif isinstance(component, presrules.OccurenceID):
policy.provide_services.append(OccurenceID(component.value))
elif isinstance(component, presrules.ServiceURI):
policy.provide_services.append(ServiceURI(component.value))
elif isinstance(component, presrules.ServiceURIScheme):
policy.provide_services.append(ServiceURIScheme(component.value))
if self.dialog_rules.supported:
dialog_rules = self.dialog_rules.content
for rule in dialog_rules:
try:
action = (action.value for action in (rule.actions or []) if isinstance(action, dialogrules.SubHandling)).next()
except StopIteration:
continue # Ignore rules whose actions we don't understand
policy = DialoginfoPolicy(rule.id, action)
#policy.name = rule.display_name.value if rule.display_name else None # Rule display-name extension
dialoginfo_policies[policy] = None
policy.multi_identity_conditions = []
for condition in (rule.conditions or []):
if isinstance(condition, common_policy.Identity):
dialoginfo_policies[policy] = condition
for identity_many_condition in (identity_condition for identity_condition in condition if isinstance(identity_condition, common_policy.IdentityMany)):
if identity_many_condition.domain:
multi_condition = DomainCondition(identity_many_condition.domain)
else:
multi_condition = CatchAllCondition()
policy.multi_identity_conditions.append(multi_condition)
for exception_condition in (sub_condition for sub_condition in identity_many_condition if isinstance(sub_condition, common_policy.IdentityExcept)):
if exception_condition.domain:
multi_condition.exceptions.append(DomainException(exception_condition.domain))
elif exception_condition.id:
multi_condition.exceptions.append(UserException(exception_condition.id))
elif isinstance(condition, common_policy.Sphere):
policy.sphere = condition.value
elif isinstance(condition, common_policy.Validity):
policy.validity = list(condition)
notexpanded = deque((l, l.display_name.value if l.display_name else None) for l in presence_lists|dialoginfo_lists|set(list_presence_policies)|buddy_lists)
visited = set(notexpanded)
while notexpanded:
rlist, list_name = notexpanded.popleft()
if rlist in buddy_lists and list_name is not None:
groups.add(list_name)
# Some children will need to be revisited so that their descendents are added to appropriate sets
for child in rlist:
if isinstance(child, resourcelists.List):
revisit = False
if rlist in presence_lists:
revisit = (child not in presence_lists) or revisit
presence_lists.add(child)
if rlist in dialoginfo_lists:
revisit = (child not in dialoginfo_lists) or revisit
dialoginfo_lists.add(child)
if rlist in list_presence_policies:
revisit = (child not in list_presence_policies) or not list_presence_policies[child].issuperset(list_presence_policies[rlist]) or revisit
list_presence_policies.setdefault(child, set()).update(list_presence_policies[rlist])
if rlist in list_services:
revisit = (child not in list_services) or not list_services[child].issuperset(list_services[rlist]) or revisit
list_services.setdefault(child, set()).update(list_services[rlist])
if rlist in buddy_lists:
revisit = (child not in buddy_lists) or revisit
buddy_lists.add(child)
if child not in visited or revisit:
visited.add(child)
notexpanded.append((child, child.display_name.value if child.display_name else list_name))
elif isinstance(child, resourcelists.External):
try:
ref_lists = self._follow_rl_external(resource_lists, child)
except ValueError:
ref_lists = []
revisit = set()
if rlist in presence_lists:
revisit.update(l for l in ref_lists if l not in presence_lists)
presence_lists.update(ref_lists)
if rlist in dialoginfo_lists:
revisit.update(l for l in ref_lists if l not in dialoginfo_lists)
dialoginfo_lists.update(ref_lists)
if rlist in list_presence_policies:
revisit.update(l for l in ref_lists if l not in list_presence_policies or not list_presence_policies[l].issuperset(list_presence_policies[rlist]))
for l in ref_lists:
list_presence_policies.setdefault(l, set()).update(list_presence_policies[rlist])
if rlist in list_services:
revisit.update(l for l in ref_lists if l not in list_services or not list_services[l].issuperset(list_services[rlist]))
for l in ref_lists:
list_services.setdefault(l, set()).update(list_services[rlist])
if rlist in buddy_lists:
revisit.update(l for l in ref_lists if l not in buddy_lists)
buddy_lists.update(ref_lists)
visited.update(l for l in ref_lists if l not in visited)
if child.display_name:
notexpanded.extend((l, child.display_name.value) for l in ref_lists if l not in visited or l in revisit)
else:
notexpanded.extend((l, l.display_name.value if l.display_name else list_name) for l in ref_lists if l not in visited or l in revisit)
elif isinstance(child, resourcelists.EntryRef):
try:
entries = self._follow_rl_entry_ref(resource_lists, child)
except ValueError:
continue
for service in list_services.get(rlist, ()):
service.entries.update(entry.uri for entry in entries)
for entry in entries:
try:
contact = contacts[entry.uri]
except KeyError:
contact = contacts[entry.uri] = Contact(None, entry.uri, None)
contact.presence_policies = set(policy for policy, identity in presence_policies.iteritems() if identity is not None and identity.matches(contact.uri))
contact.dialoginfo_policies = set(policy for policy, identity in dialoginfo_policies.iteritems() if identity is not None and identity.matches(contact.uri))
contact.subscribe_to_presence = False
contact.subscribe_to_dialoginfo = False
if contact.group is None and rlist in buddy_lists and list_name is not None:
contact.group = list_name
if contact.name is None and rlist in buddy_lists and child.display_name:
contact.name = child.display_name.value
elif contact.name is None and rlist in buddy_lists and entry.display_name:
contact.name = entry.display_name.value
if rlist in list_presence_policies:
contact.presence_policies.update(list_presence_policies[rlist])
if not contact.subscribe_to_presence and rlist in presence_lists:
contact.subscribe_to_presence = True
if not contact.subscribe_to_dialoginfo and rlist in dialoginfo_lists:
contact.subscribe_to_dialoginfo = True
if entry.attributes and rlist in buddy_lists:
for key in (key for key in entry.attributes if key not in contact.attributes):
contact.attributes[key] = entry.attributes[key]
elif isinstance(child, resourcelists.Entry):
for service in list_services.get(rlist, ()):
service.entries.add(child.uri)
try:
contact = contacts[child.uri]
except KeyError:
contact = contacts[child.uri] = Contact(None, child.uri, None)
contact.presence_policies = set(policy for policy, identity in presence_policies.iteritems() if identity is not None and identity.matches(contact.uri))
contact.dialoginfo_policies = set(policy for policy, identity in dialoginfo_policies.iteritems() if identity is not None and identity.matches(contact.uri))
contact.subscribe_to_presence = False
contact.subscribe_to_dialoginfo = False
if contact.group is None and rlist in buddy_lists and list_name is not None:
contact.group = list_name
if contact.name is None and rlist in buddy_lists and child.display_name:
contact.name = child.display_name.value
if rlist in list_presence_policies:
contact.presence_policies.update(list_presence_policies[rlist])
if not contact.subscribe_to_presence and rlist in presence_lists:
contact.subscribe_to_presence = True
if not contact.subscribe_to_dialoginfo and rlist in dialoginfo_lists:
contact.subscribe_to_dialoginfo = True
if child.attributes and rlist in buddy_lists:
for key in (key for key in child.attributes if key not in contact.attributes):
contact.attributes[key] = child.attributes[key]
if self.status_icon.supported and self.status_icon.content:
status_icon = self.status_icon.content
try:
data = base64.decodestring(status_icon.data)
except Exception:
icon = None
else:
mime_type = status_icon.mime_type.value if status_icon.mime_type else None
description = status_icon.description.value if status_icon.description else None
location = self.status_icon.alternative_location or self.status_icon.uri
icon = Icon(data, mime_type, description, location)
else:
icon = None
if self.pidf_manipulation.supported and self.pidf_manipulation.content:
pidf_manipulation = self.pidf_manipulation.content
persons = [child for child in pidf_manipulation if isinstance(child, pidf.Person)]
try:
note = unicode(chain(*(person.notes for person in persons if person.notes)).next())
except StopIteration:
note = None
try:
activity = chain(*(person.activities for person in persons if person.activities)).next()
except StopIteration:
activity = None
offline_status = OfflineStatus(activity, note)
else:
offline_status = None
contacts = contacts.values()
groups = list(groups)
presence_policies = list(presence_policies)
dialoginfo_policies = list(dialoginfo_policies)
for contact in contacts:
contact.presence_policies = list(contact.presence_policies)
contact.dialoginfo_policies = list(contact.dialoginfo_policies)
for service in services:
service.entries = list(service.entries)
notification_center = NotificationCenter()
notification_center.post_notification('XCAPManagerDidReloadData', sender=self,
data=TimestampedNotificationData(contacts=contacts, groups=groups, services=services, presence_policies=presence_policies,
dialoginfo_policies=dialoginfo_policies, status_icon=icon, offline_status=offline_status))
def _follow_policy_external_list(self, resource_lists, external_list):
result = []
for relative_uri in (uri[len(self.xcap_root):] for uri in external_list if uri.startswith(self.xcap_root)):
try:
uri = XCAPURI(self.xcap_root, relative_uri, self.namespaces)
# Only follow references to default resource-lists document which belongs to ourselves
if (uri.application_id == self.resource_lists.application and uri.document_selector.document_path == self.resource_lists.filename and
uri.user is not None and (uri.user.username, uri.user.domain) == (self.account.id.username, self.account.id.domain)):
result.extend(l for l in resource_lists.xpath(uri.node_selector.normalized, uri.node_selector.nsmap) if isinstance(l, resourcelists.List))
except ValueError:
pass
return result
def _follow_rl_external(self, resource_lists, external):
if external.anchor.startswith(self.xcap_root):
uri = XCAPURI(self.xcap_root, external.anchor[len(self.xcap_root):], self.namespaces)
# Only follow references to default resource-lists document which belongs to ourselves
if (uri.application_id == self.resource_lists.application and uri.document_selector.document_path == self.resource_lists.filename and
uri.user is not None and (uri.user.username, uri.user.domain) == (self.account.id.username, self.account.id.domain)):
return [l for l in resource_lists.xpath(uri.node_selector.normalized, uri.node_selector.nsmap) if isinstance(l, resourcelists.List)]
raise ValueError("XCAP URI does not point to default resource-lists document")
def _follow_rl_entry_ref(self, resource_lists, entry_ref):
uri = XCAPURI(self.xcap_root, entry_ref.ref, self.namespaces)
# Only follow references to default resource-lists document which belongs to ourselves
if (uri.application_id == self.resource_lists.application and uri.document_selector.document_path == self.resource_lists.filename and
uri.user is not None and (uri.user.username, uri.user.domain) == (self.account.id.username, self.account.id.domain)):
return [e for e in resource_lists.xpath(uri.node_selector.normalized, uri.node_selector.nsmap) if isinstance(e, resourcelists.Entry)]
def _follow_rls_resource_list(self, resource_lists, resource_list):
if resource_list.value.startswith(self.xcap_root):
uri = XCAPURI(self.xcap_root, resource_list.value[len(self.xcap_root):], self.namespaces)
# Only follow references to default resource-lists document which belongs to ourselves
if (uri.application_id == self.resource_lists.application and uri.document_selector.document_path == self.resource_lists.filename and
uri.user is not None and (uri.user.username, uri.user.domain) == (self.account.id.username, self.account.id.domain)):
return [l for l in resource_lists.xpath(uri.node_selector.normalized, uri.node_selector.nsmap) if isinstance(l, resourcelists.List)]
raise ValueError("XCAP URI does not point to default resource-lists document")
def _schedule_command(self, timeout, command):
from twisted.internet import reactor
timer = reactor.callLater(timeout, self.command_channel.send, command)
timer.command = command
return timer
def _save_journal(self):
try:
self.storage.save('journal', cPickle.dumps(self.journal))
except XCAPStorageError:
pass
@staticmethod
def unique_name(preferred_name, disallowed_names, skip_preferred_name=False):
disallowed_names = set(disallowed_names)
if not skip_preferred_name and preferred_name not in disallowed_names:
disallowed_names.add(preferred_name)
yield preferred_name
for i in xrange(100):
name = '%s_%03d' % (preferred_name, i)
if name not in disallowed_names:
disallowed_names.add(name)
yield name
while True:
characters = len(string.ascii_letters+string.digits)
for limit in count(4):
for i in xrange(characters**(limit-2)):
name = preferred_name + '_' + ''.join(random.choice(string.ascii_letters+string.digits) for x in xrange(limit))
if name not in disallowed_names:
disallowed_names.add(name)
yield name
class XCAPTransaction(object):
def __init__(self, xcap_manager):
self.xcap_manager = xcap_manager
def __enter__(self):
self.xcap_manager.start_transaction()
return self
def __exit__(self, type, value, traceback):
self.xcap_manager.commit_transaction()
diff --git a/sipsimple/payloads/__init__.py b/sipsimple/payloads/__init__.py
index 83f1d8a9..e03c2cb6 100644
--- a/sipsimple/payloads/__init__.py
+++ b/sipsimple/payloads/__init__.py
@@ -1,952 +1,965 @@
# Copyright (C) 2008-2011 AG Projects. See LICENSE for details.
#
__all__ = ['ParserError',
'BuilderError',
'ValidationError',
'parse_qname',
'XMLDocument',
'XMLAttribute',
'XMLElementID',
'XMLElementChild',
'XMLElementChoiceChild',
'XMLStringChoiceChild',
'XMLElement',
'XMLRootElement',
'XMLStringElement',
'XMLEmptyElement',
'XMLEmptyElementRegistryType',
'XMLListElement',
'XMLListRootElement',
'XMLStringListElement',
'uri_attribute_builder',
'uri_attribute_parser']
import os
import sys
import urllib
import weakref
from collections import defaultdict, deque
from application.python.descriptor import classproperty
from lxml import etree
## Exceptions
class ParserError(Exception): pass
class BuilderError(Exception): pass
class ValidationError(ParserError): pass
## Utilities
def parse_qname(qname):
if qname[0] == '{':
qname = qname[1:]
return qname.split('}')
else:
return None, qname
## XMLDocument
class XMLDocumentType(type):
def __init__(cls, name, bases, dct):
cls.nsmap = {}
cls.schema_map = {}
cls.element_map = {}
cls.root_element = None
cls.schema = None
cls.parser = None
for base in reversed(bases):
if hasattr(base, 'element_map'):
cls.element_map.update(base.element_map)
if hasattr(base, 'schema_map'):
cls.schema_map.update(base.schema_map)
if hasattr(base, 'nsmap'):
cls.nsmap.update(base.nsmap)
cls._update_schema()
def _update_schema(cls):
if cls.schema_map:
schema = """<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
%s
</xs:schema>
""" % '\r\n'.join('<xs:import namespace="%s" schemaLocation="%s"/>' % (ns, urllib.quote(schema)) for ns, schema in cls.schema_map.iteritems())
cls.schema = etree.XMLSchema(etree.XML(schema))
cls.parser = etree.XMLParser(schema=cls.schema, remove_blank_text=True)
else:
cls.schema = None
cls.parser = etree.XMLParser(remove_blank_text=True)
class XMLDocument(object):
__metaclass__ = XMLDocumentType
+ encoding = 'UTF-8'
+ content_type = None
+
+ @classmethod
+ def parse(cls, document):
+ try:
+ if isinstance(document, str):
+ xml = etree.XML(document, parser=cls.parser)
+ elif isinstance(document, unicode):
+ xml = etree.XML(document.encode('utf-8'), parser=cls.parser)
+ else:
+ xml = etree.parse(document, parser=cls.parser).getroot()
+ except etree.XMLSyntaxError, e:
+ raise ParserError(str(e))
+ else:
+ return cls.root_element.from_element(xml)
+
+ @classmethod
+ def build(cls, root_element, encoding=None, pretty_print=False, validate=True):
+ if type(root_element) is not cls.root_element:
+ raise TypeError("can only build XML documents from root elements of type %s" % cls.root_element.__name__)
+ element = root_element.to_element()
+ if validate and cls.schema is not None:
+ cls.schema.assertValid(element)
+ if encoding is None:
+ encoding = cls.encoding
+ return etree.tostring(element, encoding=encoding, method='xml', xml_declaration=True, pretty_print=pretty_print)
+
+ @classmethod
+ def create(cls, build_kw={}, **kw):
+ return cls.build(cls.root_element(**kw), **build_kw)
+
@classmethod
def register_element(cls, xml_class):
cls.element_map[xml_class.qname] = xml_class
for child in cls.__subclasses__():
child.register_element(xml_class)
@classmethod
def get_element(cls, qname, default=None):
return cls.element_map.get(qname, default)
@classmethod
def register_namespace(cls, namespace, prefix=None, schema=None):
if prefix in cls.nsmap:
raise ValueError("prefix %s is already registered in %s" % (prefix, cls.__name__))
if namespace in cls.nsmap.itervalues():
raise ValueError("namespace %s is already registered in %s" % (namespace, cls.__name__))
cls.nsmap[prefix] = namespace
if schema is not None:
cls.schema_map[namespace] = os.path.join(os.path.dirname(__file__), 'xml-schemas', schema)
cls._update_schema()
for child in cls.__subclasses__():
child.register_namespace(namespace, prefix, schema)
@classmethod
def unregister_namespace(cls, namespace):
try:
prefix = (prefix for prefix in cls.nsmap if cls.nsmap[prefix]==namespace).next()
except StopIteration:
raise KeyError("namespace %s is not registered in %s" % (namespace, cls.__name__))
del cls.nsmap[prefix]
schema = cls.schema_map.pop(namespace, None)
if schema is not None:
cls._update_schema()
for child in cls.__subclasses__():
try:
child.unregister_namespace(namespace)
except KeyError:
pass
## Children descriptors
class XMLAttribute(object):
def __init__(self, name, xmlname=None, type=unicode, default=None, parser=None, builder=None, required=False, test_equal=True, onset=None, ondel=None):
self.name = name
self.xmlname = xmlname or name
self.type = type
self.default = default
self.parser = parser or (lambda value: value)
self.builder = builder or (lambda value: unicode(value))
self.required = required
self.test_equal = test_equal
self.onset = onset
self.ondel = ondel
self.values = {}
def __get__(self, obj, objtype):
if obj is None:
return self
try:
return self.values[id(obj)][0]
except KeyError:
value = self.default
if value is not None:
obj.element.set(self.xmlname, self.builder(value))
obj_id = id(obj)
self.values[obj_id] = (value, weakref.ref(obj, lambda weak_ref: self.values.pop(obj_id)))
return value
def __set__(self, obj, value):
if value is not None and not isinstance(value, self.type):
value = self.type(value)
if value is not None:
obj.element.set(self.xmlname, self.builder(value))
else:
obj.element.attrib.pop(self.xmlname, None)
obj_id = id(obj)
self.values[obj_id] = (value, weakref.ref(obj, lambda weak_ref: self.values.pop(obj_id)))
if self.onset:
self.onset(obj, self, value)
def __delete__(self, obj):
obj.element.attrib.pop(self.xmlname, None)
try:
del self.values[id(obj)]
except KeyError:
pass
if self.ondel:
self.ondel(obj, self)
def parse(self, xmlvalue):
return self.parser(xmlvalue)
def build(self, value):
return self.builder(value)
class XMLElementID(XMLAttribute):
"""An XMLAttribute that represents the ID of an element (immutable)."""
def __set__(self, obj, value):
obj_id = id(obj)
if obj_id in self.values:
raise AttributeError("An XML element ID cannot be changed")
super(XMLElementID, self).__set__(obj, value)
def __delete__(self, obj):
raise AttributeError("An XML element ID cannot be deleted")
class XMLElementChild(object):
def __init__(self, name, type, required=False, test_equal=True, onset=None, ondel=None):
self.name = name
self.type = type
self.required = required
self.test_equal = test_equal
self.onset = onset
self.ondel = ondel
self.values = {}
def __get__(self, obj, objtype):
if obj is None:
return self
try:
return self.values[id(obj)][0]
except KeyError:
return None
def __set__(self, obj, value):
if value is not None and not isinstance(value, self.type):
value = self.type(value)
obj_id = id(obj)
try:
old_value = self.values[obj_id][0]
except KeyError:
pass
else:
if old_value is not None:
obj.element.remove(old_value.element)
self.values[obj_id] = (value, weakref.ref(obj, lambda weak_ref: self.values.pop(obj_id)))
if value is not None:
obj._insert_element(value.element)
if self.onset:
self.onset(obj, self, value)
def __delete__(self, obj):
try:
old_value = self.values.pop(id(obj))[0]
except KeyError:
pass
else:
if old_value is not None:
obj.element.remove(old_value.element)
if self.ondel:
self.ondel(obj, self)
class XMLElementChoiceChildWrapper(object):
__slots__ = ('descriptor', 'type')
def __init__(self, descriptor, type):
self.descriptor = descriptor
self.type = type
def __getattribute__(self, name):
if name in ('descriptor', 'type', 'register_extension', 'unregister_extension'):
return super(XMLElementChoiceChildWrapper, self).__getattribute__(name)
else:
return self.descriptor.__getattribute__(name)
def __setattr__(self, name, value):
if name in ('descriptor', 'type'):
super(XMLElementChoiceChildWrapper, self).__setattr__(name, value)
else:
setattr(self.descriptor, name, value)
def __dir__(self):
return dir(self.descriptor) + ['descriptor', 'type', 'register_extension', 'unregister_extension']
def register_extension(self, type):
if self.extension_type is None:
raise ValueError("The %s XML choice element of %s does not support extensions" % (self.name, self.type.__name__))
if not issubclass(type, XMLElement) or not issubclass(type, self.extension_type):
raise TypeError("type is not a subclass of XMLElement and/or %s: %s" % (self.extension_type.__name__, type.__name__))
if type in self.types:
raise ValueError("%s is already registered as a choice extension" % type.__name__)
self.types.add(type)
self.type._xml_children_qname_map[type.qname] = (self.descriptor, type)
for child_class in self.type.__subclasses__():
child_class._xml_children_qname_map[type.qname] = (self.descriptor, type)
def unregister_extension(self, type):
if self.extension_type is None:
raise ValueError("The %s XML choice element of %s does not support extensions" % (self.name, self.type.__name__))
try:
self.types.remove(type)
except ValueError:
raise ValueError("%s is not a registered choice extension on %s" % (type.__name__, self.type.__name__))
del self.type._xml_children_qname_map[type.qname]
for child_class in self.type.__subclasses__():
del child_class._xml_children_qname_map[type.qname]
class XMLElementChoiceChild(object):
def __init__(self, name, types, extension_type=None, required=False, test_equal=True, onset=None, ondel=None):
self.name = name
self.types = set(types)
self.extension_type = extension_type
self.required = required
self.test_equal = test_equal
self.onset = onset
self.ondel = ondel
self.values = {}
def __get__(self, obj, objtype):
if obj is None:
return XMLElementChoiceChildWrapper(self, objtype)
try:
return self.values[id(obj)][0]
except KeyError:
return None
def __set__(self, obj, value):
if value is not None and type(value) not in self.types:
raise TypeError("%s is not an acceptable type for %s" % (value.__class__.__name__, obj.__class__.__name__))
obj_id = id(obj)
try:
old_value = self.values[obj_id][0]
except KeyError:
pass
else:
if old_value is not None:
obj.element.remove(old_value.element)
self.values[obj_id] = (value, weakref.ref(obj, lambda weak_ref: self.values.pop(obj_id)))
if value is not None:
obj._insert_element(value.element)
if self.onset:
self.onset(obj, self, value)
def __delete__(self, obj):
try:
old_value = self.values.pop(id(obj))[0]
except KeyError:
pass
else:
if old_value is not None:
obj.element.remove(old_value.element)
if self.ondel:
self.ondel(obj, self)
class XMLStringChoiceChild(XMLElementChoiceChild):
"""
A choice between keyword strings from a registry, custom strings from the
other type and custom extensions. This descriptor will accept and return
strings instead of requiring XMLElement instances for the values in the
registry and the other type. Check XMLEmptyElementRegistryType for a
metaclass for building registries of XMLEmptyElement classes for keywords.
"""
def __init__(self, name, registry=None, other_type=None, extension_type=None):
self.registry = registry
self.other_type = other_type
self.extension_type = extension_type
types = registry.classes if registry is not None else ()
types += (other_type,) if other_type is not None else ()
super(XMLStringChoiceChild, self).__init__(name, types, extension_type=extension_type, required=True, test_equal=True)
def __get__(self, obj, objtype):
value = super(XMLStringChoiceChild, self).__get__(obj, objtype)
if obj is None or value is None or isinstance(value, self.extension_type or ()):
return value
else:
return unicode(value)
def __set__(self, obj, value):
if isinstance(value, basestring):
if self.registry is not None and value in self.registry.names:
value = self.registry.class_map[value]()
elif self.other_type is not None:
value = self.other_type.from_string(value)
super(XMLStringChoiceChild, self).__set__(obj, value)
## XMLElement base classes
class XMLElementType(type):
def __init__(cls, name, bases, dct):
super(XMLElementType, cls).__init__(name, bases, dct)
# set dictionary of xml attributes and xml child elements
cls._xml_attributes = {}
cls._xml_element_children = {}
cls._xml_children_qname_map = {}
for base in reversed(bases):
if hasattr(base, '_xml_attributes'):
cls._xml_attributes.update(base._xml_attributes)
if hasattr(base, '_xml_element_children') and hasattr(base, '_xml_children_qname_map'):
cls._xml_element_children.update(base._xml_element_children)
cls._xml_children_qname_map.update(base._xml_children_qname_map)
for name, value in dct.iteritems():
if isinstance(value, XMLElementID):
if cls._xml_id is not None:
raise AttributeError("Only one XMLElementID attribute can be defined in the %s class" % cls.__name__)
cls._xml_id = value
cls._xml_attributes[value.name] = value
elif isinstance(value, XMLAttribute):
cls._xml_attributes[value.name] = value
elif isinstance(value, XMLElementChild):
cls._xml_element_children[value.name] = value
cls._xml_children_qname_map[value.type.qname] = (value, value.type)
elif isinstance(value, XMLElementChoiceChild):
cls._xml_element_children[value.name] = value
for type in value.types:
cls._xml_children_qname_map[type.qname] = (value, type)
# register class in its XMLDocument
if cls._xml_document is not None:
cls._xml_document.register_element(cls)
class XMLElement(object):
__metaclass__ = XMLElementType
_xml_tag = None # To be defined in subclass
_xml_namespace = None # To be defined in subclass
_xml_document = None # To be defined in subclass
_xml_extension_type = None # Can be defined in subclass
_xml_id = None # Can be defined in subclass, or will be set by the metaclass to the XMLElementID attribute (if present)
_xml_children_order = {} # Can be defined in subclass
# dynamically generated
_xml_attributes = {}
_xml_element_children = {}
_xml_children_qname_map = {}
qname = classproperty(lambda cls: '{%s}%s' % (cls._xml_namespace, cls._xml_tag))
def __init__(self):
self.element = etree.Element(self.qname, nsmap=self._xml_document.nsmap)
def check_validity(self):
# check attributes
for name, attribute in self._xml_attributes.iteritems():
# if attribute has default but it was not set, will also be added with this occasion
value = getattr(self, name, None)
if attribute.required and value is None:
raise ValidationError("required attribute %s of %s is not set" % (name, self.__class__.__name__))
# check element children
for name, element_child in self._xml_element_children.iteritems():
# if child has default but it was not set, will also be added with this occasion
child = getattr(self, name, None)
if child is None and element_child.required:
raise ValidationError("element child %s of %s is not set" % (name, self.__class__.__name__))
def to_element(self):
try:
self.check_validity()
except ValidationError, e:
raise BuilderError(str(e))
# build element children
for name in self._xml_element_children:
child = getattr(self, name, None)
if child is not None:
child.to_element()
self._build_element()
return self.element
# To be defined in subclass
def _build_element(self):
try:
build_element = super(XMLElement, self)._build_element
except AttributeError:
pass
else:
build_element()
@classmethod
def from_element(cls, element):
obj = cls.__new__(cls)
obj.element = element
# set known attributes
for name, attribute in cls._xml_attributes.iteritems():
xmlvalue = element.get(attribute.xmlname, None)
if xmlvalue is not None:
try:
setattr(obj, name, attribute.parse(xmlvalue))
except (ValueError, TypeError):
raise ValidationError("got illegal value for attribute %s of %s: %s" % (name, cls.__name__, xmlvalue))
elif attribute.required:
raise ValidationError("attribute %s of %s is required but is not present" % (name, cls.__name__))
# set element children
required_children = set(child for child in cls._xml_element_children.itervalues() if child.required)
for child in element:
element_child, type = cls._xml_children_qname_map.get(child.tag, (None, None))
if element_child is not None:
try:
value = type.from_element(child)
except ValidationError:
pass # we should accept partially valid documents
else:
if element_child.required:
required_children.remove(element_child)
setattr(obj, element_child.name, value)
if required_children:
raise ValidationError("not all required sub elements exist in %s element" % cls.__name__)
obj._parse_element(element)
obj.check_validity()
return obj
# To be defined in subclass
def _parse_element(self, element):
try:
parse_element = super(XMLElement, self)._parse_element
except AttributeError:
pass
else:
parse_element(element)
@classmethod
def _register_xml_attribute(cls, attribute, element):
cls._xml_element_children[attribute] = element
cls._xml_children_qname_map[element.type.qname] = (element, element.type)
for subclass in cls.__subclasses__():
subclass._register_xml_attribute(attribute, element)
@classmethod
def _unregister_xml_attribute(cls, attribute):
element = cls._xml_element_children.pop(attribute)
del cls._xml_children_qname_map[element.type.qname]
for subclass in cls.__subclasses__():
subclass._unregister_xml_attribute(attribute)
@classmethod
def register_extension(cls, attribute, type, test_equal=True):
if cls._xml_extension_type is None:
raise ValueError("XMLElement type %s does not support extensions (requested extension type %s)" % (cls.__name__, type.__name__))
elif not issubclass(type, cls._xml_extension_type):
raise TypeError("XMLElement type %s only supports extensions of type %s (requested extension type %s)" % (cls.__name__, cls._xml_extension_type, type.__name__))
elif hasattr(cls, attribute):
raise ValueError("XMLElement type %s already has an attribute named %s (requested extension type %s)" % (cls.__name__, attribute, type.__name__))
extension = XMLElementChild(attribute, type=type, required=False, test_equal=test_equal)
setattr(cls, attribute, extension)
cls._register_xml_attribute(attribute, extension)
@classmethod
def unregister_extension(cls, attribute):
if cls._xml_extension_type is None:
raise ValueError("XMLElement type %s does not support extensions" % cls.__name__)
cls._unregister_xml_attribute(attribute)
delattr(cls, attribute)
def _insert_element(self, element):
if element in self.element:
return
order = self._xml_children_order.get(element.tag, self._xml_children_order.get(None, sys.maxint))
for i in xrange(len(self.element)):
child_order = self._xml_children_order.get(self.element[i].tag, self._xml_children_order.get(None, sys.maxint))
if child_order > order:
position = i
break
else:
position = len(self.element)
self.element.insert(position, element)
def __eq__(self, other):
if isinstance(other, XMLElement):
for name, attribute in self._xml_attributes.iteritems():
if attribute.test_equal:
if not hasattr(other, name) or getattr(self, name) != getattr(other, name):
return False
for name, element_child in self._xml_element_children.iteritems():
if element_child.test_equal:
if not hasattr(other, name) or getattr(self, name) != getattr(other, name):
return False
try:
__eq__ = super(XMLElement, self).__eq__
except AttributeError:
return True
else:
return __eq__(other)
elif self.__class__._xml_id is not None:
return self._xml_id == other
else:
return NotImplemented
def __ne__(self, other):
equal = self.__eq__(other)
return NotImplemented if equal is NotImplemented else not equal
def __hash__(self):
if self.__class__._xml_id is not None:
return hash(self._xml_id)
else:
return object.__hash__(self)
class XMLRootElementType(XMLElementType):
def __init__(cls, name, bases, dct):
super(XMLRootElementType, cls).__init__(name, bases, dct)
if cls._xml_document is not None:
if cls._xml_document.root_element is not None:
raise TypeError('there is already a root element registered for %s' % cls.__name__)
cls._xml_document.root_element = cls
class XMLRootElement(XMLElement):
__metaclass__ = XMLRootElementType
-
- encoding = 'UTF-8'
- content_type = None
-
+
def __init__(self):
XMLElement.__init__(self)
self.cache = weakref.WeakValueDictionary({self.element: self})
@classmethod
def from_element(cls, element):
obj = super(XMLRootElement, cls).from_element(element)
obj.cache = weakref.WeakValueDictionary({obj.element: obj})
return obj
-
+
@classmethod
def parse(cls, document):
- parser = cls._xml_document.parser
- try:
- if isinstance(document, str):
- xml = etree.XML(document, parser=parser)
- elif isinstance(document, unicode):
- xml = etree.XML(document.encode('utf-8'), parser=parser)
- else:
- xml = etree.parse(document, parser=parser).getroot()
- except etree.XMLSyntaxError, e:
- raise ParserError(str(e))
- else:
- return cls.from_element(xml)
+ return cls._xml_document.parse(document)
def toxml(self, encoding=None, pretty_print=False, validate=True):
- element = self.to_element()
- if validate and self._xml_document.schema is not None:
- self._xml_document.schema.assertValid(element)
- if encoding is None:
- encoding = self.encoding
- return etree.tostring(element, encoding=encoding, method='xml', xml_declaration=True, pretty_print=pretty_print)
+ return self._xml_document.build(self, encoding=encoding, pretty_print=pretty_print, validate=validate)
def xpath(self, xpath, namespaces=None):
result = []
try:
nodes = self.element.xpath(xpath, namespaces=namespaces)
except etree.XPathError:
raise ValueError("illegal XPath expression")
for element in (node for node in nodes if isinstance(node, etree._Element)):
if element in self.cache:
result.append(self.cache[element])
continue
if element is self.element:
self.cache[element] = self
result.append(self)
continue
for ancestor in element.iterancestors():
if ancestor in self.cache:
container = self.cache[ancestor]
break
else:
container = self
notvisited = deque([container])
visited = set()
while notvisited:
container = notvisited.popleft()
self.cache[container.element] = container
if isinstance(container, XMLListMixin):
children = set(child for child in container if isinstance(child, XMLElement) and child not in visited)
visited.update(children)
notvisited.extend(children)
for child in container._xml_element_children:
value = getattr(container, child)
if value is not None and value not in visited:
visited.add(value)
notvisited.append(value)
if element in self.cache:
result.append(self.cache[element])
return result
def get_xpath(self, element):
raise NotImplementedError
def find_parent(self, element):
raise NotImplementedError
## Mixin classes
class ThisClass(object):
"""
Special marker class that is used to indicate that an XMLListElement
subclass can be an item of itself. This is necessary because a class
cannot reference itself when defining _xml_item_type
"""
class XMLListMixinType(type):
def __init__(cls, name, bases, dct):
super(XMLListMixinType, cls).__init__(name, bases, dct)
if '_xml_item_type' in dct:
cls._xml_item_type = cls._xml_item_type # trigger __setattr__
def __setattr__(cls, name, value):
if name == '_xml_item_type':
if value is ThisClass:
value = cls
elif isinstance(value, tuple) and ThisClass in value:
value = tuple(cls if type is ThisClass else type for type in value)
if value is None:
cls._xml_item_element_types = ()
cls._xml_item_extension_types = ()
else:
item_types = value if isinstance(value, tuple) else (value,)
cls._xml_item_element_types = tuple(type for type in item_types if issubclass(type, XMLElement))
cls._xml_item_extension_types = tuple(type for type in item_types if not issubclass(type, XMLElement))
super(XMLListMixinType, cls).__setattr__(name, value)
class XMLListMixin(object):
"""A mixin representing a list of other XML elements"""
__metaclass__ = XMLListMixinType
_xml_item_type = None
def __new__(cls, *args, **kw):
if cls._xml_item_type is None:
raise TypeError("The %s class cannot be instantiated because it doesn't define the _xml_item_type attribute" % cls.__name__)
instance = super(XMLListMixin, cls).__new__(cls)
instance._element_map = {}
instance._xmlid_map = defaultdict(dict)
return instance
def __contains__(self, item):
return item in self._element_map.itervalues()
def __iter__(self):
return (self._element_map[element] for element in self.element if element in self._element_map)
def __len__(self):
return len(self._element_map)
def __nonzero__(self):
return bool(self._element_map)
def __repr__(self):
return '%s(%r)' % (self.__class__.__name__, list(self))
def _parse_element(self, element):
self._element_map.clear()
self._xmlid_map.clear()
for child in element[:]:
child_class = self._xml_document.get_element(child.tag, type(None))
if child_class in self._xml_item_element_types or issubclass(child_class, self._xml_item_extension_types):
try:
value = child_class.from_element(child)
except ValidationError:
pass
else:
if value._xml_id is not None and value._xml_id in self._xmlid_map[child_class]:
element.remove(child)
else:
if value._xml_id is not None:
self._xmlid_map[child_class][value._xml_id] = value
self._element_map[value.element] = value
def _build_element(self):
for child in self._element_map.itervalues():
child.to_element()
def add(self, item):
if not (item.__class__ in self._xml_item_element_types or isinstance(item, self._xml_item_extension_types)):
raise TypeError("%s cannot add items of type %s" % (self.__class__.__name__, item.__class__.__name__))
if item._xml_id is not None and item._xml_id in self._xmlid_map[item.__class__]:
self.remove(self._xmlid_map[item.__class__][item._xml_id])
self._insert_element(item.element)
if item._xml_id is not None:
self._xmlid_map[item.__class__][item._xml_id] = item
self._element_map[item.element] = item
def remove(self, item):
self.element.remove(item.element)
if item._xml_id is not None:
del self._xmlid_map[item.__class__][item._xml_id]
del self._element_map[item.element]
def update(self, sequence):
for item in sequence:
self.add(item)
def clear(self):
for item in self._element_map.values():
self.remove(item)
## Element types
class XMLStringElement(XMLElement):
_xml_lang = False # To be defined in subclass
_xml_value_type = unicode # To be defined in subclass
lang = XMLAttribute('lang', xmlname='{http://www.w3.org/XML/1998/namespace}lang', type=str, required=False, test_equal=True)
def __init__(self, value, lang=None):
XMLElement.__init__(self)
self.value = value
self.lang = lang
def __eq__(self, other):
if isinstance(other, XMLStringElement):
return self.lang == other.lang and self.value == other.value
elif isinstance(other, basestring) and (self._xml_lang is False or self.lang is None):
return self.value == other
else:
return NotImplemented
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.value, self.lang)
def __str__(self):
return str(self.value)
def __unicode__(self):
return unicode(self.value)
def _get_value(self):
return self.__dict__['value']
def _set_value(self, value):
if value is not None and not isinstance(value, self._xml_value_type):
value = self._xml_value_type(value)
self.__dict__['value'] = value
value = property(_get_value, _set_value)
del _get_value, _set_value
def _parse_element(self, element):
self.value = element.text
if self._xml_lang:
self.lang = element.get('{http://www.w3.org/XML/1998/namespace}lang', None)
else:
self.lang = None
def _build_element(self):
if self.value is not None:
self.element.text = unicode(self.value)
else:
self.element.text = None
if not self._xml_lang and self.lang is not None:
del self.element.attrib[self.__class__.lang.xmlname]
class XMLEmptyElement(XMLElement):
def __init__(self):
XMLElement.__init__(self)
def __repr__(self):
return '%s()' % self.__class__.__name__
def __eq__(self, other):
return type(self) is type(other) or NotImplemented
def __hash__(self):
return hash(self.__class__)
class XMLEmptyElementRegistryType(type):
"""A metaclass for building registries of XMLEmptyElement subclasses from names"""
def __init__(cls, name, bases, dct):
super(XMLEmptyElementRegistryType, cls).__init__(name, bases, dct)
typename = getattr(cls, '__typename__', name.partition('Registry')[0]).capitalize()
class BaseElementType(XMLEmptyElement):
def __str__(self): return self._xml_tag
def __unicode__(self): return unicode(self._xml_tag)
cls.__basetype__ = BaseElementType
cls.__basetype__.__name__ = 'Base%sType' % typename
cls.class_map = {}
for name in cls.names:
class ElementType(BaseElementType):
_xml_tag = name
_xml_namespace = cls._xml_namespace
_xml_document = cls._xml_document
_xml_id = name
ElementType.__name__ = typename + name.title().translate(None, '-_')
cls.class_map[name] = ElementType
cls.classes = tuple(cls.class_map[name] for name in cls.names)
## Created using mixins
class XMLListElementType(XMLElementType, XMLListMixinType): pass
class XMLListRootElementType(XMLRootElementType, XMLListMixinType): pass
class XMLListElement(XMLElement, XMLListMixin):
__metaclass__ = XMLListElementType
class XMLListRootElement(XMLRootElement, XMLListMixin):
__metaclass__ = XMLListRootElementType
class XMLStringListElementType(XMLListElementType):
def __init__(cls, name, bases, dct):
if cls._xml_item_type is not None:
raise TypeError("The %s class should not define _xml_item_type, but define _xml_item_registry, _xml_item_other_type and _xml_item_extension_type instead" % cls.__name__)
types = cls._xml_item_registry.classes if cls._xml_item_registry is not None else ()
types += tuple(type for type in (cls._xml_item_other_type, cls._xml_item_extension_type) if type is not None)
cls._xml_item_type = types or None
super(XMLStringListElementType, cls).__init__(name, bases, dct)
class XMLStringListElement(XMLListElement):
__metaclass__ = XMLStringListElementType
_xml_item_registry = None
_xml_item_other_type = None
_xml_item_extension_type = None
def __contains__(self, item):
if isinstance(item, basestring):
if self._xml_item_registry is not None and item in self._xml_item_registry.names:
item = self._xml_item_registry.class_map[item]()
elif self._xml_item_other_type is not None:
item = self._xml_item_other_type.from_string(item)
return item in self._element_map.itervalues()
def __iter__(self):
return (item if isinstance(item, self._xml_item_extension_types) else unicode(item) for item in super(XMLStringListElement, self).__iter__())
def add(self, item):
if isinstance(item, basestring):
if self._xml_item_registry is not None and item in self._xml_item_registry.names:
item = self._xml_item_registry.class_map[item]()
elif self._xml_item_other_type is not None:
item = self._xml_item_other_type.from_string(item)
super(XMLStringListElement, self).add(item)
def remove(self, item):
if isinstance(item, basestring):
if self._xml_item_registry is not None and item in self._xml_item_registry.names:
xmlitem = self._xml_item_registry.class_map[item]()
try:
item = (entry for entry in super(XMLStringListElement, self).__iter__() if entry == xmlitem).next()
except StopIteration:
raise KeyError(item)
elif self._xml_item_other_type is not None:
xmlitem = self._xml_item_other_type.from_string(item)
try:
item = (entry for entry in super(XMLStringListElement, self).__iter__() if entry == xmlitem).next()
except StopIteration:
raise KeyError(item)
super(XMLStringListElement, self).remove(item)
## Useful attribute builders/parser
def uri_attribute_builder(value):
return urllib.quote(value.encode('utf-8'))
def uri_attribute_parser(value):
return urllib.unquote(value).decode('utf-8')
diff --git a/sipsimple/payloads/conference.py b/sipsimple/payloads/conference.py
index f65a22ab..7331c6a2 100644
--- a/sipsimple/payloads/conference.py
+++ b/sipsimple/payloads/conference.py
@@ -1,836 +1,836 @@
# Copyright (C) 2010-2011 AG Projects. See LICENSE for details.
#
# This module is currently broken. It breaks the core assumption of the
# payloads infrastructure, that there is only one element with a given qname
# for every given application.
# Currently this module defines at least 2 different elements with the same
# qname (tag=entry) which are used inside list elements. As a result, when
# the list element tries to lookup the class to use for a given qname, only
# one list element will get the class right, the other will get a wrong class
# because the list element uses the application qname map to determine the
# class and that mapping can only contain 1 mapping from a given qname to a
# class. Since the class it obtains is not the right one, it will be ignored
# as it doesn't match the known item types for that list element and the
# corresponding xml data will also be ignored.
#
# To make matters even worse, this module subclasses XMLElement classes
# without changing their qname, which in turn generates even more overlapping
# classes for a given qname. At least according to the xml schema, all these
# subclasses (which seem to be defined in order to impose some restrictions
# in different cases), seem to be unnecessary. The schema only defines one
# type with a string element that has no resctrictions and is to be used
# in all the places. The code however tries to use a variation of the type
# with restrictions in different places and fails as the correct class cannot
# be identified anymore (see for example all the UrisType subclasses or the
# multiple classes to define purpose elements).
#
# -Dan
#
"""Parses and produces conference-info messages according to RFC4575."""
__all__ = ['namespace',
'ConferenceDocument',
'ConferenceDescription',
'ConfUris',
'ConfUrisEntry',
'ServiceUris',
'ServiceUrisEntry',
'UrisTypeModified',
'UrisTypeEntry',
'AvailableMedia',
'AvailableMediaEntry',
'Users',
'User',
'AssociatedAors',
'Roles',
'Role',
'Endpoint',
'CallInfo',
'Sip',
'Referred',
'JoiningInfo',
'DisconnectionInfo',
'HostInfo',
'HostInfoUris',
'ConferenceState',
'SidebarsByRef',
'SidebarsByVal',
'Conference',
'ConferenceDescriptionExtension',
# Extensions
'FileResource',
'FileResources',
'Resources']
from sipsimple.payloads import ValidationError, XMLDocument, XMLRootElement, XMLStringElement, XMLElementChild, XMLElement, XMLListElement, XMLAttribute
from sipsimple.util import Timestamp
namespace = 'urn:ietf:params:xml:ns:conference-info'
-class ConferenceDocument(XMLDocument): pass
+class ConferenceDocument(XMLDocument):
+ content_type = "application/conference-info+xml"
+
ConferenceDocument.register_namespace(namespace, prefix=None, schema='conference.xsd')
# Marker mixins
class ConferenceDescriptionExtension(object): pass
class State(str):
def __new__(cls, value):
if value not in ('full', 'partial', 'deleted'):
raise ValueError("illegal value for state")
return str.__new__(cls, value)
class Version(str):
def __new__(cls, value):
return str.__new__(cls, int(value))
class BooleanValue(object):
def __new__(cls, value):
if type(value) is str and value in ('true', 'false'):
return str.__new__(str, value)
if type(value) is not bool:
raise ValueError("illegal value for boolean type")
if value:
return str.__new__(str, 'true')
else:
return str.__new__(str, 'false')
class When(XMLStringElement):
_xml_tag = 'when'
_xml_namespace = namespace
_xml_document = ConferenceDocument
_xml_value_type = Timestamp
class Reason(XMLStringElement):
_xml_tag = 'reason'
_xml_namespace = namespace
_xml_document = ConferenceDocument
class By(XMLStringElement):
_xml_tag = 'by'
_xml_namespace = namespace
_xml_document = ConferenceDocument
class ExecutionType(XMLElement):
_xml_tag = None # To be set by the subclass
_xml_namespace = namespace
_xml_document = ConferenceDocument
when = XMLElementChild('when', type=When, required=False, test_equal=True)
reason = XMLElementChild('reason', type=Reason, required=False, test_equal=True)
by = XMLElementChild('by', type=By, required=False, test_equal=True)
def __init__(self, when=None, reason=None, by=None):
XMLElement.__init__(self)
self.when = when
self.reason = reason
self.by = by
class Uri(XMLStringElement):
_xml_tag = 'uri'
_xml_namespace = namespace
_xml_document = ConferenceDocument
class DisplayText(XMLStringElement):
_xml_tag = 'display-text'
_xml_namespace = namespace
_xml_document = ConferenceDocument
class UrisTypePurpose(XMLStringElement):
_xml_tag = 'purpose'
_xml_namespace = namespace
_xml_document = ConferenceDocument
class UrisTypeModified(ExecutionType):
_xml_tag = 'modified'
class UrisTypeEntry(XMLElement):
_xml_tag = 'entry'
_xml_namespace = namespace
_xml_document = ConferenceDocument
state = XMLAttribute('state', type=State, required=False, test_equal=False)
uri = XMLElementChild('uri', type=Uri, required=True, test_equal=True)
display_text = XMLElementChild('display_text', type=DisplayText, required=False, test_equal=True)
purpose = XMLElementChild('purpose', type=UrisTypePurpose, required=False, test_equal=True)
modified = XMLElementChild('modified', type=UrisTypeModified, required=False, test_equal=True)
def __init__(self, uri, state=None, display_text=None, purpose=None, modified=None):
XMLElement.__init__(self)
self.uri = uri
self.state = state
self.display_text = display_text
self.purpose = purpose
self.modified = modified
class Subject(XMLStringElement):
_xml_tag = 'subject'
_xml_namespace = namespace
_xml_document = ConferenceDocument
class FreeText(XMLStringElement):
_xml_tag = 'free-text'
_xml_namespace = namespace
_xml_document = ConferenceDocument
class Keywords(XMLStringElement):
_xml_tag = 'keywords'
_xml_namespace = namespace
_xml_document = ConferenceDocument
class ConfUrisPurposeValue(str):
def __new__(cls, value):
if value not in ('participation', 'streaming'):
raise ValueError("illegal value for purpose element")
return str.__new__(cls, value)
class ConfUrisPurpose(UrisTypePurpose):
_xml_value_type = ConfUrisPurposeValue
class ConfUrisEntry(UrisTypeEntry):
purpose = XMLElementChild('purpose', type=ConfUrisPurpose, required=False, test_equal=True)
class ConfUris(XMLListElement):
_xml_tag = 'conf-uris'
_xml_namespace = namespace
_xml_document = ConferenceDocument
_xml_item_type = ConfUrisEntry
def __init__(self, entries=[]):
XMLListElement.__init__(self)
self.update(entries)
class ServiceUrisPurposeValue(str):
def __new__(cls, value):
if value not in ('web-page', 'recording', 'event'):
raise ValueError("illegal value for purpose element")
return str.__new__(cls, value)
class ServiceUrisPurpose(UrisTypePurpose):
_xml_value_type = ServiceUrisPurposeValue
class ServiceUrisEntry(UrisTypeEntry):
purpose = XMLElementChild('purpose', type=ServiceUrisPurpose, required=False, test_equal=True)
class ServiceUris(XMLListElement):
_xml_tag = 'service-uris'
_xml_namespace = namespace
_xml_document = ConferenceDocument
_xml_item_type = ServiceUrisEntry
def __init__(self, entries=[]):
XMLListElement.__init__(self)
self.update(entries)
class MaximumUserCount(XMLStringElement):
_xml_tag = 'maximum-user-count'
_xml_namespace = namespace
_xml_document = ConferenceDocument
_xml_value_type = int
class MediaTypeValue(str):
def __new__(cls, value):
if value not in ('audio', 'video', 'text', 'message'):
raise ValueError("illegal value for type element")
return str.__new__(cls, value)
class MediaType(XMLStringElement):
_xml_tag = 'type'
_xml_namespace = namespace
_xml_document = ConferenceDocument
_xml_value_type = MediaTypeValue
class MediaTypeStatusValue(str):
def __new__(cls, value):
if value not in ('sendrecv', 'sendonly', 'recvonly', 'inactive'):
raise ValueError("illegal value for status element")
return str.__new__(cls, value)
class MediaTypeStatus(XMLStringElement):
_xml_tag = 'status'
_xml_namespace = namespace
_xml_document = ConferenceDocument
_xml_value_type = MediaTypeStatusValue
class AvailableMediaEntry(XMLElement):
_xml_tag = 'entry'
_xml_namespace = namespace
_xml_document = ConferenceDocument
_xml_children_order = {DisplayText.qname: 0,
MediaType.qname: 1,
MediaTypeStatus.qname: 2,
None: 3}
label = XMLAttribute('label', type=str, required=True, test_equal=False)
media_type = XMLElementChild('media_type', type=MediaType, required=True, test_equal=True)
display_text = XMLElementChild('display_text', type=DisplayText, required=False, test_equal=True)
status = XMLElementChild('status', type=MediaTypeStatus, required=False, test_equal=True)
def __init__(self, label, media_type, display_text=None, status=None):
XMLElement.__init__(self)
self.label = label
self.media_type = media_type
self.display_text = display_text
self.status = status
class AvailableMedia(XMLListElement):
_xml_tag = 'available-media'
_xml_namespace = namespace
_xml_document = ConferenceDocument
_xml_item_type = AvailableMediaEntry
def __init__(self, entries=[]):
XMLListElement.__init__(self)
self.update(entries)
class ConferenceDescription(XMLElement):
_xml_tag = 'conference-description'
_xml_namespace = namespace
_xml_document = ConferenceDocument
_xml_extension_type = ConferenceDescriptionExtension
display_text = XMLElementChild('display_text', type=DisplayText, required=False, test_equal=True)
subject = XMLElementChild('subject', type=Subject, required=False, test_equal=True)
free_text = XMLElementChild('free_text', type=FreeText, required=False, test_equal=True)
keywords = XMLElementChild('keywords', type=Keywords, required=False, test_equal=True)
conf_uris = XMLElementChild('conf_uris', type=ConfUris, required=False, test_equal=True)
service_uris = XMLElementChild('service_uris', type=ServiceUris, required=False, test_equal=True)
maximum_user_count = XMLElementChild('maximum_user_count', type=MaximumUserCount, required=False, test_equal=True)
available_media = XMLElementChild('available_media', type=AvailableMedia, required=False, test_equal=True)
def __init__(self, display_text=None, subject=None, free_text=None, keywords=None, conf_uris=None, service_uris=None, maximum_user_count=None, available_media=None):
XMLElement.__init__(self)
self.display_text = display_text
self.subject = subject
self.free_text = free_text
self.keywords = keywords
self.conf_uris = conf_uris
self.service_uris = service_uris
self.maximum_user_count = maximum_user_count
self.available_media = available_media
class WebPage(XMLStringElement):
_xml_tag = 'web-page'
_xml_namespace = namespace
_xml_document = ConferenceDocument
class HostInfoUris(XMLListElement):
_xml_tag = 'uris'
_xml_namespace = namespace
_xml_document = ConferenceDocument
_xml_item_type = UrisTypeEntry
def __init__(self, entries=[]):
XMLListElement.__init__(self)
self.update(entries)
class HostInfo(XMLElement):
_xml_tag = 'host-info'
_xml_namespace = namespace
_xml_document = ConferenceDocument
display_text = XMLElementChild('display_text', type=DisplayText, required=False, test_equal=True)
web_page = XMLElementChild('web_page', type=WebPage, required=False, test_equal=True)
uris = XMLElementChild('uris', type=HostInfoUris, required=False, test_equal=True)
def __init__(self, display_text=None, web_page=None, uris=None):
XMLElement.__init__(self)
self.display_text = display_text
self.web_page = web_page
self.uris = uris
class UserCount(XMLStringElement):
_xml_tag = 'user-count'
_xml_namespace = namespace
_xml_document = ConferenceDocument
_xml_value_type = int
class Active(XMLStringElement):
_xml_tag = 'active'
_xml_namespace = namespace
_xml_document = ConferenceDocument
_xml_value_type = BooleanValue
class Locked(XMLStringElement):
_xml_tag = 'locked'
_xml_namespace = namespace
_xml_document = ConferenceDocument
_xml_value_type = BooleanValue
class ConferenceState(XMLElement):
_xml_tag = 'conference-state'
_xml_namespace = namespace
_xml_document = ConferenceDocument
user_count = XMLElementChild('user_count', type=UserCount, required=False, test_equal=True)
active = XMLElementChild('active', type=Active, required=False, test_equal=True)
locked = XMLElementChild('locked', type=Locked, required=False, test_equal=True)
def __init__(self, user_count=None, active=None, locked=None):
XMLElement.__init__(self)
self.user_count = user_count
self.active = active
self.locked = locked
class AssociatedAors(XMLListElement):
_xml_tag = 'associated-aors'
_xml_namespace = namespace
_xml_document = ConferenceDocument
_xml_item_type = UrisTypeEntry
def __init__(self, entries=[]):
XMLListElement.__init__(self)
self.update(entries)
class Role(XMLStringElement):
_xml_tag = 'entry'
_xml_namespace = namespace
_xml_document = ConferenceDocument
class Roles(XMLListElement):
_xml_tag = 'roles'
_xml_namespace = namespace
_xml_document = ConferenceDocument
_xml_item_type = Role
def __init__(self, roles=[]):
XMLListElement.__init__(self)
self.update(roles)
class Languages(XMLStringElement):
_xml_tag = 'languages'
_xml_namespace = namespace
_xml_document = ConferenceDocument
class CascadedFocus(XMLStringElement):
_xml_tag = 'cascaded-focus'
_xml_namespace = namespace
_xml_document = ConferenceDocument
class Referred(ExecutionType):
_xml_tag = 'referred'
class EndpointStatusValue(str):
def __new__(cls, value):
if value not in ('connected', 'disconnected', 'on-hold', 'muted-via-focus', 'pending', 'alerting', 'dialing-in', 'dialing-out', 'disconnecting'):
raise ValueError("illegal value for status element")
return str.__new__(cls, value)
class EndpointStatus(XMLStringElement):
_xml_tag = 'status'
_xml_namespace = namespace
_xml_document = ConferenceDocument
_xml_value_type = EndpointStatusValue
class JoiningMethodValue(str):
def __new__(cls, value):
if value not in ('dialed-in', 'dialed-out', 'focus-owner'):
raise ValueError("illegal value for joining method element")
return str.__new__(cls, value)
class JoiningMethod(XMLStringElement):
_xml_tag = 'joining-method'
_xml_namespace = namespace
_xml_document = ConferenceDocument
_xml_value_type = JoiningMethodValue
class JoiningInfo(ExecutionType):
_xml_tag = 'joining-info'
class DisconnectionMethodValue(str):
def __new__(cls, value):
if value not in ('departed', 'booted', 'failed', 'busy'):
raise ValueError("illegal value for disconnection method element")
return str.__new__(cls, value)
class DisconnectionMethod(XMLStringElement):
_xml_tag = 'disconnection-method'
_xml_namespace = namespace
_xml_document = ConferenceDocument
_xml_value_type = DisconnectionMethodValue
class DisconnectionInfo(ExecutionType):
_xml_tag = 'disconnection-info'
class Label(XMLStringElement):
_xml_tag = 'label'
_xml_namespace = namespace
_xml_document = ConferenceDocument
class SrcId(XMLStringElement):
_xml_tag = 'src-id'
_xml_namespace = namespace
_xml_document = ConferenceDocument
class Media(XMLElement):
_xml_tag = 'media'
_xml_namespace = namespace
_xml_document = ConferenceDocument
id = XMLAttribute('id', type=str, required=True, test_equal=False)
display_text = XMLElementChild('display_text', type=DisplayText, required=False, test_equal=True)
media_type = XMLElementChild('media_type', type=MediaType, required=False, test_equal=True)
label = XMLElementChild('label', type=Label, required=False, test_equal=True)
src_id = XMLElementChild('src_id', type=SrcId, required=False, test_equal=True)
status = XMLElementChild('status', type=MediaTypeStatus, required=False, test_equal=True)
def __init__(self, id, display_text=None, media_type=None, label=None, src_id=None, status=None):
XMLElement.__init__(self)
self.id = id
self.display_text = display_text
self.media_type = media_type
self.label = label
self.src_id = src_id
self.status = status
class CallId(XMLStringElement):
_xml_tag = 'call-id'
_xml_namespace = namespace
_xml_document = ConferenceDocument
class FromTag(XMLStringElement):
_xml_tag = 'from-tag'
_xml_namespace = namespace
_xml_document = ConferenceDocument
class ToTag(XMLStringElement):
_xml_tag = 'to-tag'
_xml_namespace = namespace
_xml_document = ConferenceDocument
class Sip(XMLElement):
_xml_tag = 'sip'
_xml_namespace = namespace
_xml_document = ConferenceDocument
display_text = XMLElementChild('display_text', type=DisplayText, required=False, test_equal=True)
call_id = XMLElementChild('call_id', type=CallId, required=False, test_equal=True)
from_tag = XMLElementChild('from_tag', type=FromTag, required=False, test_equal=True)
to_tag = XMLElementChild('to_tag', type=ToTag, required=False, test_equal=True)
def __init__(self, display_text=None, call_id=None, from_tag=None, to_tag=None):
XMLElement.__init__(self)
self.display_text = display_text
self.call_id = call_id
self.from_tag = from_tag
self.to_tag = to_tag
class CallInfo(XMLElement):
_xml_tag = 'call-info'
_xml_namespace = namespace
_xml_document = ConferenceDocument
sip = XMLElementChild('sip', type=Sip, required=False, test_equal=True)
def __init__(self, sip=None):
XMLElement.__init__(self)
self.sip = sip
class Endpoint(XMLListElement):
_xml_tag = 'endpoint'
_xml_namespace = namespace
_xml_document = ConferenceDocument
_xml_item_type = Media
entity = XMLAttribute('entity', type=str, required=True, test_equal=False)
state = XMLAttribute('state', type=State, required=False, test_equal=False)
display_text = XMLElementChild('display_text', type=DisplayText, required=False, test_equal=True)
referred = XMLElementChild('referred', type=Referred, required=False, test_equal=True)
status = XMLElementChild('status', type=EndpointStatus, required=False, test_equal=True)
joining_method = XMLElementChild('joining_method', type=JoiningMethod, required=False, test_equal=True)
joining_info = XMLElementChild('joining_info', type=JoiningInfo, required=False, test_equal=True)
disconnection_method = XMLElementChild('disconnection_method', type=DisconnectionMethod, required=False, test_equal=True)
disconnection_info = XMLElementChild('disconnection_info', type=DisconnectionInfo, required=False, test_equal=True)
call_info = XMLElementChild('call_info', type=CallInfo, required=False, test_equal=True)
def __init__(self, entity, state='full', display_text=None, referred=None, status=None, joining_method=None, joining_info=None, disconnection_method=None, disconnection_info=None, call_info=None, media=[]):
XMLListElement.__init__(self)
self.entity = entity
self.state = state
self.display_text = display_text
self.referred = referred
self.status = status
self.joining_method = joining_method
self.joining_info = joining_info
self.disconnection_method = disconnection_method
self.disconnection_info = disconnection_info
self.call_info = call_info
self.update(media)
def __repr__(self):
args = ('entity', 'state', 'display_text', 'referred', 'status', 'joining_method', 'joining_info', 'disconnection_method', 'disconnection_info', 'call_info')
return "%s(%s, media=%r)" % (self.__class__.__name__, ', '.join("%s=%r" % (name, getattr(self, name)) for name in args), list(self))
class User(XMLListElement):
_xml_tag = 'user'
_xml_namespace = namespace
_xml_document = ConferenceDocument
_xml_item_type = Endpoint
entity = XMLAttribute('entity', type=str, required=True, test_equal=False)
state = XMLAttribute('state', type=State, required=False, test_equal=False)
display_text = XMLElementChild('display_text', type=DisplayText, required=False, test_equal=True)
associated_aors = XMLElementChild('associated_aors', type=AssociatedAors, required=False, test_equal=True)
roles = XMLElementChild('roles', type=Roles, required=False, test_equal=True)
languages = XMLElementChild('languages', type=Languages, required=False, test_equal=True)
cascaded_focus = XMLElementChild('cascaded_focus', type=CascadedFocus, required=False, test_equal=True)
def __init__(self, entity, state='full', display_text=None, associated_aors=None, roles=None, languages=None, cascaded_focus=None, endpoints=[]):
XMLListElement.__init__(self)
self.entity = entity
self.state = state
self.display_text = display_text
self.associated_aors = associated_aors
self.roles = roles
self.languages = languages
self.cascaded_focus = cascaded_focus
self.update(endpoints)
def __repr__(self):
args = ('entity', 'state', 'display_text', 'associated_aors', 'roles', 'languages', 'cascaded_focus')
return "%s(%s, endpoints=%r)" % (self.__class__.__name__, ', '.join("%s=%r" % (name, getattr(self, name)) for name in args), list(self))
class Users(XMLListElement):
_xml_tag = 'users'
_xml_namespace = namespace
_xml_document = ConferenceDocument
_xml_item_type = User
state = XMLAttribute('state', type=State, required=False, test_equal=False)
def __init__(self, state='full', users=[]):
XMLListElement.__init__(self)
self.state = state
self.update(users)
def __repr__(self):
return "%s(state=%r, users=%r)" % (self.__class__.__name__, self.state, list(self))
class SidebarsByRef(XMLListElement):
_xml_tag = 'sidebars-by-ref'
_xml_namespace = namespace
_xml_document = ConferenceDocument
_xml_item_type = UrisTypeEntry
def __init__(self, entries=[]):
XMLListElement.__init__(self)
self.update(entries)
class SidebarsByVal(XMLListElement):
_xml_tag = 'sidebars-by-val'
_xml_namespace = namespace
_xml_document = ConferenceDocument
_xml_item_type = None # will be set later, after the item type is defined below
state = XMLAttribute('state', type=State, required=False, test_equal=False)
def __init__(self, state='full', entries=[]):
XMLListElement.__init__(self)
self.state = state
self.update(entries)
def __repr__(self):
return "%s(state=%r, entries=%r)" % (self.__class__.__name__, self.state, list(self))
class SidebarsByValEntry(XMLElement):
_xml_tag = 'entry'
_xml_namespace = namespace
_xml_document = ConferenceDocument
entity = XMLAttribute('entity', type=str, required=True, test_equal=False)
state = XMLAttribute('state', type=State, required=False, test_equal=False)
conference_description = XMLElementChild('conference_description', type=ConferenceDescription, required=False, test_equal=True)
host_info = XMLElementChild('host_info', type=HostInfo, required=False, test_equal=True)
conference_state = XMLElementChild('conference_state', type=ConferenceState, required=False, test_equal=True)
users = XMLElementChild('users', type=Users, required=False, test_equal=True)
sidebars_by_ref = XMLElementChild('sidebars_by_ref', type=SidebarsByRef, required=False, test_equal=True)
sidebars_by_val = XMLElementChild('sidebars_by_val', type=SidebarsByVal, required=False, test_equal=True)
def __init__(self, entity, state='full', version=None, conference_description=None, host_info=None, conference_state=None, users=None, sidebars_by_ref=None, sidebars_by_val=None):
XMLElement.__init__(self)
self.entity = entity
self.state = state
self.version = version
self.conference_description = conference_description
self.host_info = host_info
self.conference_state = conference_state
self.users = users
self.sidebars_by_ref = sidebars_by_ref
self.sidebars_by_val = sidebars_by_val
if self.state == "full" and (self.conference_description is None or self.users is None):
raise ValidationError("A full conference document must at least include the <conference-description> and <users> child elements.")
SidebarsByVal._xml_item_type = SidebarsByValEntry
class Conference(XMLRootElement):
- content_type = "application/conference-info+xml"
-
_xml_tag = 'conference-info'
_xml_namespace = namespace
_xml_document = ConferenceDocument
_xml_children_order = {ConferenceDescription.qname: 0,
HostInfo.qname: 1,
ConferenceState.qname: 2,
Users.qname: 3,
SidebarsByRef.qname: 4,
SidebarsByVal.qname: 5,
None: 6}
entity = XMLAttribute('entity', type=str, required=True, test_equal=False)
state = XMLAttribute('state', type=State, required=False, test_equal=False)
version = XMLAttribute('version', type=Version, required=False, test_equal=False)
conference_description = XMLElementChild('conference_description', type=ConferenceDescription, required=False, test_equal=True)
host_info = XMLElementChild('host_info', type=HostInfo, required=False, test_equal=True)
conference_state = XMLElementChild('conference_state', type=ConferenceState, required=False, test_equal=True)
users = XMLElementChild('users', type=Users, required=False, test_equal=True)
sidebars_by_ref = XMLElementChild('sidebars_by_ref', type=SidebarsByRef, required=False, test_equal=True)
sidebars_by_val = XMLElementChild('sidebars_by_val', type=SidebarsByVal, required=False, test_equal=True)
def __init__(self, entity, state='full', version=None, conference_description=None, host_info=None, conference_state=None, users=None, sidebars_by_ref=None, sidebars_by_val=None):
XMLRootElement.__init__(self)
self.entity = entity
self.state = state
self.version = version
self.conference_description = conference_description
self.host_info = host_info
self.conference_state = conference_state
self.users = users
self.sidebars_by_ref = sidebars_by_ref
self.sidebars_by_val = sidebars_by_val
if self.state == "full" and (self.conference_description is None or self.users is None):
raise ValidationError("A full conference document must at least include the <conference-description> and <users> child elements.")
#
# Extensions
#
agp_conf_namespace = 'urn:ag-projects:xml:ns:conference-info'
ConferenceDocument.register_namespace(agp_conf_namespace, prefix='agp-conf')
class FileResource(XMLElement):
_xml_tag = 'file'
_xml_namespace = agp_conf_namespace
_xml_document = ConferenceDocument
name = XMLAttribute('name', type=unicode, required=True, test_equal=False)
hash = XMLAttribute('hash', type=str, required=True, test_equal=False)
size = XMLAttribute('size', type=int, required=True, test_equal=False)
sender = XMLAttribute('sender', type=unicode, required=True, test_equal=False)
status = XMLAttribute('status', type=str, required=True, test_equal=False)
def __init__(self, name, hash, size, sender, status):
XMLElement.__init__(self)
self.name = name
self.hash = hash
self.size = size
self.sender = sender
self.status = status
class FileResources(XMLListElement):
_xml_tag = 'files'
_xml_namespace = agp_conf_namespace
_xml_document = ConferenceDocument
_xml_item_type = FileResource
def __init__(self, files=[]):
XMLListElement.__init__(self)
self.update(files)
class Resources(XMLElement, ConferenceDescriptionExtension):
_xml_tag = 'resources'
_xml_namespace = agp_conf_namespace
_xml_document = ConferenceDocument
files = XMLElementChild('files', type=FileResources, required=False, test_equal=True)
def __init__(self, files=None):
XMLElement.__init__(self)
self.files = files
ConferenceDescription.register_extension('resources', Resources)
diff --git a/sipsimple/payloads/dialoginfo.py b/sipsimple/payloads/dialoginfo.py
index 42a17389..2ad961f3 100644
--- a/sipsimple/payloads/dialoginfo.py
+++ b/sipsimple/payloads/dialoginfo.py
@@ -1,255 +1,255 @@
# Copyright (C) 2009-2011 AG Projects. See LICENSE for details.
#
"""Parses and produces dialog-info messages according to RFC4235."""
__all__ = ['namespace',
'DialogInfoDocument',
'DialogState',
'Replaces',
'ReferredBy',
'Identity',
'Param',
'Target',
'Local',
'Remote',
'Dialog',
'DialogInfo']
from sipsimple.payloads import XMLDocument, XMLListRootElement, XMLListElement, XMLStringElement, XMLElementChild, XMLEmptyElement, XMLElement, XMLElementID, XMLAttribute
namespace = 'urn:ietf:params:xml:ns:dialog-info'
-class DialogInfoDocument(XMLDocument): pass
+class DialogInfoDocument(XMLDocument):
+ content_type = "application/dialog-info+xml"
+
DialogInfoDocument.register_namespace(namespace, prefix=None, schema='dialog-info.xsd')
# Attribute value types
class StateValue(str):
def __new__(cls, value):
if value not in ('full', 'partial'):
raise ValueError("illegal value for state")
return str.__new__(cls, value)
class VersionValue(int):
def __new__(cls, value):
value = int.__new__(cls, value)
if value < 0:
raise ValueError("illegal value for version")
return value
class DirectionValue(str):
def __new__(cls, value):
if value not in ('initiator', 'recipient'):
raise ValueError("illegal value for direction")
return str.__new__(cls, value)
class DialogEventValue(str):
def __new__(cls, value):
if value not in ('rejected', 'cancelled', 'replaced', 'local-bye', 'remote-bye', 'error', 'timeout'):
raise ValueError("illegal value for dialog state event")
return str.__new__(cls, value)
class DialogStateValue(str):
def __new__(cls, value):
if value not in ('trying', 'proceeding', 'early', 'confirmed', 'terminated'):
raise ValueError("illegal value for dialog state")
return str.__new__(cls, value)
class CodeValue(int):
def __new__(cls, value):
value = int.__new__(cls, value)
if value < 100 or value > 699:
raise ValueError("illegal value for code")
return value
# Elements
class CallId(XMLStringElement):
_xml_tag = 'call-id'
_xml_namespace = namespace
_xml_document = DialogInfoDocument
class LocalTag(XMLStringElement):
_xml_tag = 'local-tag'
_xml_namespace = namespace
_xml_document = DialogInfoDocument
class RemoteTag(XMLStringElement):
_xml_tag = 'remote-tag'
_xml_namespace = namespace
_xml_document = DialogInfoDocument
class DialogState(XMLStringElement):
_xml_tag = 'state'
_xml_namespace = namespace
_xml_document = DialogInfoDocument
_xml_value_type = DialogStateValue
code = XMLAttribute('code', type=int, required=False, test_equal=True)
event = XMLAttribute('event', type=DialogEventValue, required=False, test_equal=True)
class Duration(XMLStringElement):
_xml_tag = 'duration'
_xml_namespace = namespace
_xml_document = DialogInfoDocument
_xml_value_type = int
class Replaces(XMLEmptyElement):
_xml_tag = 'replaces'
_xml_namespace = namespace
_xml_document = DialogInfoDocument
call_id = XMLAttribute('call_id', xmlname='call-id', type=str, required=True, test_equal=True)
local_tag = XMLAttribute('local_tag', xmlname='local-tag', type=str, required=True, test_equal=True)
remote_tag = XMLAttribute('remote_tag', xmlname='remote-tag', type=str, required=True, test_equal=True)
def __init__(self, call_id, local_tag, remote_tag):
XMLEmptyElement.__init__(self)
self.call_id = call_id
self.local_tag = local_tag
self.remote_tag = remote_tag
class ReferredBy(XMLStringElement):
_xml_tag = 'referred-by'
_xml_namespace = namespace
_xml_document = DialogInfoDocument
display = XMLAttribute('display', type=str, required=False, test_equal=True)
class Identity(XMLStringElement):
_xml_tag = 'identity'
_xml_namespace = namespace
_xml_document = DialogInfoDocument
display = XMLAttribute('display', type=str, required=False, test_equal=True)
class Param(XMLEmptyElement):
_xml_tag = 'param'
_xml_namespace = namespace
_xml_document = DialogInfoDocument
pname = XMLAttribute('pname', type=str, required=True, test_equal=True)
pval = XMLAttribute('pval', type=str, required=True, test_equal=True)
def __init__(self, pname, pval):
XMLEmptyElement.__init__(self)
self.pname = pname
self.pval = pval
class Target(XMLListElement):
_xml_tag = 'target'
_xml_namespace = namespace
_xml_document = DialogInfoDocument
_xml_item_type = Param
uri = XMLAttribute('uri', type=str, required=True, test_equal=True)
def __init__(self, uri, params=[]):
self.uri = uri
self.update(params)
class Participant(XMLElement):
_xml_tag = '' # To be set by a subclass
_xml_namespace = namespace
_xml_document = DialogInfoDocument
identity = XMLElementChild('identity', type=Identity, required=False, test_equal=True)
target = XMLElementChild('target', type=Target, required=False, test_equal=True)
def __init__(self, identity=None, target=None):
XMLElement.__init__(self)
self.identity = identity
self.target = target
class Local(Participant):
_xml_tag = 'local'
class Remote(Participant):
_xml_tag = 'remote'
class Dialog(XMLElement):
_xml_tag = 'dialog'
_xml_namespace = namespace
_xml_document = DialogInfoDocument
id = XMLElementID('id', type=str, required=True, test_equal=True)
call_id = XMLAttribute('call_id', xmlname='call-id', type=str, required=False, test_equal=True)
local_tag = XMLAttribute('local_tag', xmlname='local-tag', type=str, required=False, test_equal=True)
remote_tag = XMLAttribute('remote_tag', xmlname='remote-tag', type=str, required=False, test_equal=True)
direction = XMLAttribute('direction', type=DirectionValue, required=False, test_equal=True)
state = XMLElementChild('state', type=DialogState, required=True, test_equal=True)
duration = XMLElementChild('duration', type=Duration, required=False, test_equal=True)
replaces = XMLElementChild('replaces', type=Replaces, required=False, test_equal=True)
referred_by = XMLElementChild('referred_by', type=ReferredBy, required=False, test_equal=True)
local = XMLElementChild('local', type=Local, required=False, test_equal=True)
remote = XMLElementChild('remote', type=Remote, required=False, test_equal=True)
def __init__(self, id, state, call_id=None, local_tag=None, remote_tag=None, direction=None, duration=None, replaces=None, referred_by=None, local=None, remote=None):
XMLElement.__init__(self)
self.id = id
self.state = state
self.call_id = call_id
self.local_tag = local_tag
self.remote_tag = remote_tag
self.direction = direction
self.duration = duration
self.replaces = replaces
self.referred_by = referred_by
self.local = local
self.remote = remote
class DialogInfo(XMLListRootElement):
- content_type = "application/dialog-info+xml"
-
_xml_tag = 'dialog-info'
_xml_namespace = namespace
_xml_document = DialogInfoDocument
_xml_children_order = {Dialog.qname: 0,
None: 1}
_xml_item_type = Dialog
version = XMLAttribute('version', type=VersionValue, required=True, test_equal=True)
state = XMLAttribute('state', type=StateValue, required=True, test_equal=True)
entity = XMLAttribute('entity', type=str, required=True, test_equal=True)
def __init__(self, version, state, entity, dialogs=[]):
XMLListRootElement.__init__(self)
self.version = version
self.state = state
self.entity = entity
self.update(dialogs)
def __getitem__(self, key):
return self._xmlid_map[Dialog][key]
def __delitem__(self, key):
self.remove(self._xmlid_map[Dialog][key])
diff --git a/sipsimple/payloads/directory.py b/sipsimple/payloads/directory.py
index 0387fdfd..979d6b82 100644
--- a/sipsimple/payloads/directory.py
+++ b/sipsimple/payloads/directory.py
@@ -1,81 +1,81 @@
# Copyright (C) 2009-2011 AG Projects. See LICENSE for details.
#
"""Parses xcap-directory messages according to OMA TS XDM Core 1.1"""
__all__ = ['namespace', 'XCAPDirectoryDocument', 'Folder', 'Entry', 'ErrorCode']
from sipsimple.payloads import XMLDocument, XMLListRootElement, XMLStringElement, XMLListElement, XMLAttribute, XMLElementChild
from sipsimple.util import Timestamp
namespace = 'urn:oma:xml:xdm:xcap-directory'
-class XCAPDirectoryDocument(XMLDocument): pass
+class XCAPDirectoryDocument(XMLDocument):
+ content_type = "application/vnd.oma.xcap-directory+xml"
+
XCAPDirectoryDocument.register_namespace(namespace, prefix=None, schema='xcap-directory.xsd')
# Attribute value types
class SizeValue(int):
def __new__(cls, value):
value = int.__new__(cls, value)
if value <= 0:
raise ValueError("illegal value for size")
return value
# Elements
class Entry(XMLStringElement):
_xml_tag = 'entry'
_xml_namespace = namespace
_xml_document = XCAPDirectoryDocument
uri = XMLAttribute('uri', type=str, required=True, test_equal=True)
etag = XMLAttribute('etag', type=str, required=True, test_equal=True)
last_modified = XMLAttribute('last_modified', xmlname='last-modified', type=Timestamp, required=False, test_equal=True)
size = XMLAttribute('size', type=SizeValue, required=False, test_equal=True)
class ErrorCode(XMLStringElement):
_xml_tag = 'error-code'
_xml_namespace = namespace
_xml_document = XCAPDirectoryDocument
class Folder(XMLListElement):
_xml_tag = 'folder'
_xml_namespace = namespace
_xml_document = XCAPDirectoryDocument
_xml_item_type = Entry
auid = XMLAttribute('auid', type=str, required=True, test_equal=True)
error_code = XMLElementChild('error_code', type=ErrorCode, required=False, test_equal=True, onset=lambda self, descriptor, value: self.clear() if value is not None else None)
def __init__(self, auid, entries=[], error_code=None):
if error_code is not None and entries:
raise ValueError("Cannot set both an error code and add entries at the same time")
XMLListElement.__init__(self)
self.auid = auid
self.error_code = error_code
self.update(entries)
def add(self, entry):
if self.error_code is not None:
raise ValueError("Cannot add an entry when error_code is set")
super(Folder, self).add(entry)
class XCAPDirectory(XMLListRootElement):
- content_type = "application/vnd.oma.xcap-directory+xml"
-
_xml_tag = 'xcap-directory'
_xml_namespace = namespace
_xml_document = XCAPDirectoryDocument
_xml_item_type = Folder
def __init__(self, folders=[]):
XMLListRootElement.__init__(self)
self.update(folders)
diff --git a/sipsimple/payloads/iscomposing.py b/sipsimple/payloads/iscomposing.py
index a97d19c7..fa1dafae 100644
--- a/sipsimple/payloads/iscomposing.py
+++ b/sipsimple/payloads/iscomposing.py
@@ -1,88 +1,88 @@
# Copyright (C) 2009-2011 AG Projects. See LICENSE for details.
#
"""Parses and produces isComposing messages according to RFC3994."""
__all__ = ['namespace', 'IsComposingDocument', 'State', 'LastActive', 'ContentType', 'Refresh', 'IsComposingMessage']
from sipsimple.payloads import XMLDocument, XMLRootElement, XMLStringElement, XMLElementChild
from sipsimple.util import Timestamp
namespace = 'urn:ietf:params:xml:ns:im-iscomposing'
-class IsComposingDocument(XMLDocument): pass
+class IsComposingDocument(XMLDocument):
+ content_type = "application/im-iscomposing+xml"
+
IsComposingDocument.register_namespace(namespace, prefix=None, schema='im-iscomposing.xsd')
# Attribute value types
class StateValue(str):
def __new__(cls, value):
if value not in ('active', 'idle'):
value = 'idle'
return str.__new__(cls, value)
class RefreshValue(int):
def __new__(cls, value):
value = int.__new__(cls, value)
if value <= 0:
raise ValueError("illegal value form refresh element")
return value
# Elements
class State(XMLStringElement):
_xml_tag = 'state'
_xml_namespace = namespace
_xml_document = IsComposingDocument
_xml_value_type = StateValue
class LastActive(XMLStringElement):
_xml_tag = 'lastactive'
_xml_namespace = namespace
_xml_document = IsComposingDocument
_xml_value_type = Timestamp
class ContentType(XMLStringElement):
_xml_tag = 'contenttype'
_xml_namespace = namespace
_xml_document = IsComposingDocument
class Refresh(XMLStringElement):
_xml_tag = 'refresh'
_xml_namespace = namespace
_xml_document = IsComposingDocument
_xml_value_type = RefreshValue
-
-class IsComposingMessage(XMLRootElement):
- content_type = "application/im-iscomposing+xml"
+class IsComposingMessage(XMLRootElement):
_xml_tag = 'isComposing'
_xml_namespace = namespace
_xml_document = IsComposingDocument
_xml_children_order = {State.qname: 0,
LastActive.qname: 1,
ContentType.qname: 2,
Refresh.qname: 3,
None: 4}
state = XMLElementChild('state', type=State, required=True, test_equal=True)
last_active = XMLElementChild('last_active', type=LastActive, required=False, test_equal=True)
- contenttype = XMLElementChild('contenttype', type=ContentType, required=False, test_equal=True)
+ content_type = XMLElementChild('content_type', type=ContentType, required=False, test_equal=True)
refresh = XMLElementChild('refresh', type=Refresh, required=False, test_equal=True)
-
+
def __init__(self, state=None, last_active=None, content_type=None, refresh=None):
XMLRootElement.__init__(self)
self.state = state
self.last_active = last_active
- self.contenttype = content_type
+ self.content_type = content_type
self.refresh = refresh
diff --git a/sipsimple/payloads/pidf.py b/sipsimple/payloads/pidf.py
index ee57098b..8a3d1f9a 100644
--- a/sipsimple/payloads/pidf.py
+++ b/sipsimple/payloads/pidf.py
@@ -1,458 +1,458 @@
# Copyright (C) 2008-2011 AG Projects. See LICENSE for details.
#
"""PIDF handling according to RFC3863 and RFC4479"""
__all__ = ['pidf_namespace',
'dm_namespace',
'PIDFDocument',
'ServiceExtension',
'DeviceExtension',
'PersonExtension',
'Note',
'DeviceID',
'Status',
'Basic',
'Contact',
'ServiceTimestamp',
'Service',
'DeviceTimestamp',
'Device',
'PersonTimestamp',
'Person',
'PIDF']
import datetime
import weakref
from sipsimple import util
from sipsimple.payloads import ValidationError, XMLDocument, XMLListRootElement, XMLElement, XMLStringElement, XMLAttribute, XMLElementID, XMLElementChild
pidf_namespace = 'urn:ietf:params:xml:ns:pidf'
dm_namespace = 'urn:ietf:params:xml:ns:pidf:data-model'
-class PIDFDocument(XMLDocument): pass
+class PIDFDocument(XMLDocument):
+ content_type = 'application/pidf+xml'
+
PIDFDocument.register_namespace(pidf_namespace, prefix=None, schema='pidf.xsd')
PIDFDocument.register_namespace(dm_namespace, prefix='dm', schema='data-model.xsd')
## Marker mixin
class ServiceExtension(object): pass
class DeviceExtension(object): pass
class PersonExtension(object): pass
## Attribute value types
class BasicStatusValue(str):
def __new__(cls, value):
if value not in ('closed', 'open'):
raise ValueError('illegal BasicStatusValue')
return str.__new__(cls, value)
## General elements
class Timestamp(XMLElement):
_xml_tag = 'timestamp'
_xml_namespace = pidf_namespace
_xml_document = PIDFDocument
def __init__(self, value=None):
XMLElement.__init__(self)
self.value = value
def __repr__(self):
return '%s(%r)' % (self.__class__.__name__, self.value)
def __str__(self):
return str(self.value)
def _get_value(self):
return self.__dict__['value']
def _set_value(self, value):
if value is None:
value = datetime.datetime.now()
elif isinstance(value, basestring):
value = util.Timestamp.parse(value)
elif isinstance(value, Timestamp):
value = value.value
self.__dict__['value'] = value
value = property(_get_value, _set_value)
del _get_value, _set_value
def _parse_element(self, element):
self.value = element.text
def _build_element(self):
self.element.text = util.Timestamp.format(self.value)
class Note(unicode):
def __new__(cls, value, lang=None):
instance = unicode.__new__(cls, value)
instance.lang = lang
return instance
def __repr__(self):
return "%s(%s, lang=%r)" % (self.__class__.__name__, unicode.__repr__(self), self.lang)
def __eq__(self, other):
if isinstance(other, Note):
return unicode.__eq__(self, other) and self.lang == other.lang
elif isinstance(other, basestring):
return self.lang is None and unicode.__eq__(self, other)
else:
return NotImplemented
def __ne__(self, other):
equal = self.__eq__(other)
return NotImplemented if equal is NotImplemented else not equal
class PIDFNote(XMLStringElement):
_xml_tag = 'note'
_xml_namespace = pidf_namespace
_xml_document = PIDFDocument
_xml_lang = True
def __unicode__(self):
return Note(self.value, self.lang)
class DMNote(XMLStringElement):
_xml_tag = 'note'
_xml_namespace = dm_namespace
_xml_document = PIDFDocument
_xml_lang = True
def __unicode__(self):
return Note(self.value, self.lang)
class NoteMap(object):
"""Descriptor to be used for _note_map attributes on XML elements with notes"""
def __init__(self):
self.object_map = weakref.WeakKeyDictionary()
def __get__(self, obj, type):
if obj is None:
return self
return self.object_map.setdefault(obj, {})
def __set__(self, obj, value):
raise AttributeError("cannot set attribute")
def __delete__(self, obj):
raise AttributeError("cannot delete attribute")
class NoteList(object):
def __init__(self, xml_element, note_type):
self.xml_element = xml_element
self.note_type = note_type
def __contains__(self, item):
if isinstance(item, Note):
item = self.note_type(item, item.lang)
elif isinstance(item, basestring):
item = self.note_type(item)
return item in self.xml_element._note_map.itervalues()
def __iter__(self):
return (unicode(self.xml_element._note_map[element]) for element in self.xml_element.element if element in self.xml_element._note_map)
def __len__(self):
return len(self.xml_element._note_map)
def __nonzero__(self):
return bool(self.xml_element._note_map)
def _parse_element(self, element):
self.xml_element._note_map.clear()
for child in element:
if child.tag == self.note_type.qname:
try:
note = self.note_type.from_element(child)
except ValidationError:
pass
else:
self.xml_element._note_map[note.element] = note
def _build_element(self):
for note in self.xml_element._note_map.itervalues():
note.to_element()
def add(self, item):
if isinstance(item, Note):
item = self.note_type(item, item.lang)
elif isinstance(item, basestring):
item = self.note_type(item)
if type(item) is not self.note_type:
raise TypeError("%s cannot add notes of type %s" % (self.xml_element.__class__.__name__, item.__class__.__name__))
self.xml_element._insert_element(item.element)
self.xml_element._note_map[item.element] = item
def remove(self, item):
if isinstance(item, Note):
try:
item = (entry for entry in self.xml_element._note_map.itervalues() if unicode(entry) == item).next()
except StopIteration:
raise KeyError(item)
elif isinstance(item, basestring):
try:
item = (entry for entry in self.xml_element._note_map.itervalues() if entry == item).next()
except StopIteration:
raise KeyError(item)
if type(item) is not self.note_type:
raise KeyError(item)
self.xml_element.element.remove(item.element)
del self.xml_element._note_map[item.element]
def update(self, sequence):
for item in sequence:
self.add(item)
def clear(self):
for item in self.xml_element._note_map.values():
self.remove(item)
class DeviceID(XMLStringElement):
_xml_tag = 'deviceID'
_xml_namespace = dm_namespace
_xml_document = PIDFDocument
_xml_lang = False
## Service elements
class Basic(XMLStringElement):
_xml_tag = 'basic'
_xml_namespace = pidf_namespace
_xml_document = PIDFDocument
_xml_lang = False
_xml_value_type = BasicStatusValue
class Status(XMLElement):
_xml_tag = 'status'
_xml_namespace = pidf_namespace
_xml_document = PIDFDocument
_xml_children_order = {Basic.qname: 0}
basic = XMLElementChild('basic', type=Basic, required=False, test_equal=True)
def __init__(self, basic=None):
XMLElement.__init__(self)
self.basic = basic
def check_validity(self):
if len(self.element) == 0:
raise ValidationError("Status objects must have at least one child")
super(Status, self).check_validity()
def __repr__(self):
return '%s(%r)' % (self.__class__.__name__, self.basic)
class Contact(XMLStringElement):
_xml_tag = 'contact'
_xml_namespace = pidf_namespace
_xml_document = PIDFDocument
_xml_lang = False
priority = XMLAttribute('priority', type=float, required=False, test_equal=False)
class ServiceTimestamp(Timestamp): pass
class Service(XMLElement):
_xml_tag = 'tuple'
_xml_namespace = pidf_namespace
_xml_document = PIDFDocument
_xml_extension_type = ServiceExtension
_xml_children_order = {Status.qname: 0,
None: 1,
Contact.qname: 2,
PIDFNote.qname: 3,
ServiceTimestamp.qname: 4}
id = XMLElementID('id', type=str, required=True, test_equal=True)
status = XMLElementChild('status', type=Status, required=True, test_equal=True)
contact = XMLElementChild('contact', type=Contact, required=False, test_equal=True)
timestamp = XMLElementChild('timestamp', type=ServiceTimestamp, required=False, test_equal=True)
device_id = XMLElementChild('device_id', type=DeviceID, required=False, test_equal=True)
_note_map = NoteMap()
def __init__(self, id, notes=[], status=None, contact=None, timestamp=None, device_id=None):
XMLElement.__init__(self)
self.id = id
self.status = status
self.contact = contact
self.timestamp = timestamp
self.device_id = device_id
self.notes.update(notes)
@property
def notes(self):
return NoteList(self, PIDFNote)
def __repr__(self):
return '%s(%r, %r, %r, %r, %r, %r)' % (self.__class__.__name__, self.id, list(self.notes), self.status, self.contact, self.timestamp, self.device_id)
def _parse_element(self, element):
super(Service, self)._parse_element(element)
self.notes._parse_element(element)
def _build_element(self):
super(Service, self)._build_element()
self.notes._build_element()
class DeviceTimestamp(Timestamp):
_xml_tag = 'timestamp'
_xml_namespace = dm_namespace
_xml_document = PIDFDocument
class Device(XMLElement):
_xml_tag = 'device'
_xml_namespace = dm_namespace
_xml_document = PIDFDocument
_xml_extension_type = DeviceExtension
_xml_children_order = {None: 0,
DeviceID.qname: 1,
DMNote.qname: 2,
DeviceTimestamp.qname: 3}
id = XMLElementID('id', type=str, required=True, test_equal=True)
device_id = XMLElementChild('device_id', type=DeviceID, required=False, test_equal=True)
timestamp = XMLElementChild('timestamp', type=DeviceTimestamp, required=False, test_equal=True)
_note_map = NoteMap()
def __init__(self, id, device_id=None, notes=[], timestamp=None):
XMLElement.__init__(self)
self.id = id
self.device_id = device_id
self.timestamp = timestamp
self.notes.update(notes)
@property
def notes(self):
return NoteList(self, DMNote)
def __repr__(self):
return '%s(%r, %r, %r, %r)' % (self.__class__.__name__, self.id, self.device_id, list(self.notes), self.timestamp)
def _parse_element(self, element):
super(Device, self)._parse_element(element)
self.notes._parse_element(element)
def _build_element(self):
super(Device, self)._build_element()
self.notes._build_element()
class PersonTimestamp(Timestamp):
_xml_tag = 'timestamp'
_xml_namespace = dm_namespace
_xml_document = PIDFDocument
class Person(XMLElement):
_xml_tag = 'person'
_xml_namespace = dm_namespace
_xml_document = PIDFDocument
_xml_extension_type = PersonExtension
_xml_children_order = {None: 0,
DMNote.qname: 1,
PersonTimestamp.qname: 2}
id = XMLElementID('id', type=str, required=True, test_equal=True)
timestamp = XMLElementChild('timestamp', type=PersonTimestamp, required=False, test_equal=True)
_note_map = NoteMap()
def __init__(self, id, notes=[], timestamp=None):
XMLElement.__init__(self)
self.id = id
self.timestamp = timestamp
self.notes.update(notes)
@property
def notes(self):
return NoteList(self, DMNote)
def __repr__(self):
return '%s(%r, %r, %r)' % (self.__class__.__name__, self.id, list(self.notes), self.timestamp)
def _parse_element(self, element):
super(Person, self)._parse_element(element)
self.notes._parse_element(element)
def _build_element(self):
super(Person, self)._build_element()
self.notes._build_element()
class PIDF(XMLListRootElement):
- content_type = 'application/pidf+xml'
-
_xml_tag = 'presence'
_xml_namespace = pidf_namespace
_xml_document = PIDFDocument
_xml_children_order = {Service.qname: 0,
PIDFNote.qname: 1,
Person.qname: 2,
Device.qname: 3}
_xml_item_type = (Service, PIDFNote, Person, Device)
entity = XMLAttribute('entity', type=str, required=True, test_equal=True)
services = property(lambda self: (item for item in self if type(item) is Service))
notes = property(lambda self: (item for item in self if type(item) is Note))
persons = property(lambda self: (item for item in self if type(item) is Person))
devices = property(lambda self: (item for item in self if type(item) is Device))
def __init__(self, entity, elements=[]):
XMLListRootElement.__init__(self)
self.entity = entity
self.update(elements)
def __contains__(self, item):
if isinstance(item, Note):
item = PIDFNote(item, item.lang)
return super(PIDF, self).__contains__(item)
def __iter__(self):
return (unicode(item) if type(item) is PIDFNote else item for item in super(PIDF, self).__iter__())
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.entity, list(self))
def add(self, item):
if isinstance(item, Note):
item = PIDFNote(item, item.lang)
super(PIDF, self).add(item)
def remove(self, item):
if isinstance(item, Note):
try:
item = (entry for entry in super(PIDF, self).__iter__() if type(entry) is PIDFNote and unicode(entry) == item).next()
except StopIteration:
raise KeyError(item)
super(PIDF, self).remove(item)
diff --git a/sipsimple/payloads/policy.py b/sipsimple/payloads/policy.py
index 789f9348..b520167f 100644
--- a/sipsimple/payloads/policy.py
+++ b/sipsimple/payloads/policy.py
@@ -1,371 +1,371 @@
# Copyright (C) 2008-2011 AG Projects. See LICENSE for details.
#
"""
Generic data types to be used in policy applications, according to
RFC4745.
"""
__all__ = ['namespace',
'CommonPolicyDocument',
'ConditionElement',
'ActionElement',
'TransformationElement',
'RuleExtension',
'IdentityOne',
'IdentityExcept',
'IdentityMany',
'Identity',
'Validity',
'Conditions',
'Actions',
'Transformations',
'Rule',
'RuleSet',
# Extensions
'FalseCondition',
'RuleDisplayName']
import datetime
from sipsimple.payloads import ValidationError, XMLDocument, XMLElement, XMLListElement, XMLListRootElement, XMLAttribute, XMLElementID, XMLElementChild, XMLStringElement
from sipsimple.util import Timestamp
namespace = 'urn:ietf:params:xml:ns:common-policy'
-class CommonPolicyDocument(XMLDocument): pass
+class CommonPolicyDocument(XMLDocument):
+ content_type = 'application/auth-policy+xml'
+
CommonPolicyDocument.register_namespace(namespace, prefix='cp', schema='common-policy.xsd')
## Mixin types for extensibility
class ConditionElement(object): pass
class ActionElement(object): pass
class TransformationElement(object): pass
class RuleExtension(object): pass
## Elements
class IdentityOne(XMLElement):
_xml_tag = 'one'
_xml_namespace = namespace
_xml_document = CommonPolicyDocument
id = XMLElementID('id', type=str, required=True, test_equal=True)
def __init__(self, id):
XMLElement.__init__(self)
self.id = id
def __repr__(self):
return '%s(%r)' % (self.__class__.__name__, self.id)
def __str__(self):
return self.id
def matches(self, uri):
return self.id == uri
class IdentityExcept(XMLElement):
_xml_tag = 'except'
_xml_namespace = namespace
_xml_document = CommonPolicyDocument
def _onset_id(self, attribute, value):
if value is not None:
self.domain = None
id = XMLAttribute('id', type=str, required=False, test_equal=True, onset=_onset_id)
del _onset_id
def _onset_domain(self, attribute, value):
if value is not None:
self.id = None
domain = XMLAttribute('domain', type=str, required=False, test_equal=True, onset=_onset_domain)
del _onset_domain
def __init__(self, id=None, domain=None):
XMLElement.__init__(self)
self.id = id
self.domain = domain
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.id, self.domain)
def __str__(self):
if self.id is not None:
return self.id
else:
return self.domain
def matches(self, uri):
if self.id is not None:
return self.id != uri
else:
return [self.domain] != uri.split('@', 1)[1:]
class IdentityMany(XMLListElement):
_xml_tag = 'many'
_xml_namespace = namespace
_xml_document = CommonPolicyDocument
_xml_children_order = {IdentityExcept.qname: 0}
_xml_item_type = IdentityExcept
domain = XMLAttribute('domain', type=str, required=False, test_equal=True)
def __init__(self, domain=None, exceptions=[]):
XMLListElement.__init__(self)
self.domain = domain
self.update(exceptions)
def __repr__(self):
return '%s(%r, %s)' % (self.__class__.__name__, self.domain, list(self))
def matches(self, uri):
if self.domain is not None:
if self.domain != uri.partition('@')[2]:
return False
for child in self:
if not child.matches(uri):
return False
return True
class Identity(XMLListElement):
_xml_tag = 'identity'
_xml_namespace = namespace
_xml_document = CommonPolicyDocument
_xml_item_type = (IdentityOne, IdentityMany)
def __init__(self, identities=[]):
XMLListElement.__init__(self)
self.update(identities)
def matches(self, uri):
for child in self:
if child.matches(uri):
return True
return False
class Sphere(XMLElement):
_xml_tag = 'sphere'
_xml_namespace = namespace
_xml_document = CommonPolicyDocument
value = XMLAttribute('value', type=str, required=True, test_equal=True)
def __init__(self, value):
XMLElement.__init__(self)
self.value = value
def __repr__(self):
return '%s(%r)' % (self.__class__.__name__, self.value)
class ValidFrom(XMLElement):
_xml_tag = 'from'
_xml_namespace = namespace
_xml_document = CommonPolicyDocument
def __init__(self, timestamp):
if isinstance(timestamp, (datetime.datetime, str)):
timestamp = Timestamp(timestamp)
if not isinstance(timestamp, Timestamp):
raise TypeError("Validity element can only be a Timestamp, datetime or string")
XMLElement.__init__(self)
self.element.text = str(timestamp)
class ValidUntil(XMLElement):
_xml_tag = 'until'
_xml_namespace = namespace
_xml_document = CommonPolicyDocument
def __init__(self, timestamp):
if isinstance(timestamp, (datetime.datetime, str)):
timestamp = Timestamp(timestamp)
if not isinstance(timestamp, Timestamp):
raise TypeError("Validity element can only be a Timestamp, datetime or string")
XMLElement.__init__(self)
self.element.text = str(timestamp)
class ValidityInterval(object):
def __init__(self, from_timestamp, until_timestamp):
self.valid_from = ValidFrom(from_timestamp)
self.valid_until = ValidUntil(until_timestamp)
def __eq__(self, other):
if isinstance(other, ValidityInterval):
return self.valid_from.text==other.valid_from.text and self.valid_until.text==other.valid_until.text
return NotImplemented
def __ne__(self, other):
if isinstance(other, ValidityInterval):
return self.valid_from.text!=other.valid_from.text or self.valid_until.text!=other.valid_until.text
return NotImplemented
@classmethod
def from_elements(cls, from_element, until_element):
instance = object.__new__(cls)
instance.valid_from = ValidFrom.from_element(from_element)
instance.valid_until = ValidUntil.from_element(until_element)
return instance
class Validity(XMLListElement):
_xml_tag = 'validity'
_xml_namespace = namespace
_xml_document = CommonPolicyDocument
_xml_item_type = ValidityInterval
def __init__(self, children=[]):
XMLListElement.__init__(self)
self.update(children)
def _parse_element(self, element):
iterator = iter(element)
for first_child in iterator:
second_child = iterator.next()
if first_child.tag == '{%s}from' % self._xml_namespace and second_child.tag == '{%s}until' % self._xml_namespace:
try:
item = ValidityInterval.from_elements(first_child, second_child)
except:
pass
else:
self._element_map[item.valid_from.element] = item
def _build_element(self):
for child in self:
child.valid_from.to_element()
child.valid_until.to_element()
def add(self, item):
if not isinstance(item, ValidityInterval):
raise TypeError("Validity element must be a ValidityInterval instance")
self._insert_element(item.valid_from.element)
self._insert_element(item.valid_until.element)
self._element_map[item.valid_from.element] = item
def remove(self, item):
self.element.remove(item.valid_from.element)
self.element.remove(item.valid_until.element)
del self._element_map[item.valid_from.element]
def check_validity(self):
if not self:
raise ValidationError("cannot have Validity element without any children")
XMLListElement.check_validity(self)
class Conditions(XMLListElement):
_xml_tag = 'conditions'
_xml_namespace = namespace
_xml_document = CommonPolicyDocument
_xml_children_order = {Identity.qname: 0,
Sphere.qname: 1,
Validity.qname: 2}
_xml_item_type = (Identity, Sphere, Validity, ConditionElement)
def __init__(self, conditions=[]):
XMLListElement.__init__(self)
self.update(conditions)
class Actions(XMLListElement):
_xml_tag = 'actions'
_xml_namespace = namespace
_xml_document = CommonPolicyDocument
_xml_item_type = ActionElement
def __init__(self, actions=[]):
XMLListElement.__init__(self)
self.update(actions)
class Transformations(XMLListElement):
_xml_tag = 'transformations'
_xml_namespace = namespace
_xml_document = CommonPolicyDocument
_xml_item_type = TransformationElement
def __init__(self, transformations=[]):
XMLListElement.__init__(self)
self.update(transformations)
class Rule(XMLElement):
_xml_tag = 'rule'
_xml_namespace = namespace
_xml_extension_type = RuleExtension
_xml_document = CommonPolicyDocument
_xml_children_order = {Conditions.qname: 0,
Actions.qname: 1,
Transformations.qname: 2}
id = XMLElementID('id', type=unicode, required=True, test_equal=True)
conditions = XMLElementChild('conditions', type=Conditions, required=False, test_equal=True)
actions = XMLElementChild('actions', type=Actions, required=False, test_equal=True)
transformations = XMLElementChild('transformations', type=Transformations, required=False, test_equal=True)
def __init__(self, id, conditions=None, actions=None, transformations=None):
XMLElement.__init__(self)
self.id = id
self.conditions = conditions
self.actions = actions
self.transformations = transformations
def __repr__(self):
return '%s(%r, %r, %r, %r)' % (self.__class__.__name__, self.id, self.conditions, self.actions, self.transformations)
class RuleSet(XMLListRootElement):
- content_type = 'application/auth-policy+xml'
-
_xml_tag = 'ruleset'
_xml_namespace = namespace
_xml_document = CommonPolicyDocument
_xml_item_type = Rule
def __init__(self, rules=[]):
XMLListRootElement.__init__(self)
self.update(rules)
def __getitem__(self, key):
return self._xmlid_map[Rule][key]
def __delitem__(self, key):
self.remove(self._xmlid_map[Rule][key])
#
# Extensions
#
agp_cp_namespace = 'urn:ag-projects:xml:ns:common-policy'
CommonPolicyDocument.register_namespace(agp_cp_namespace, prefix='agp-cp')
# A condition element in the AG Projects namespace, it will always be evaluated to false
# because it's not understood by servers
class FalseCondition(XMLElement, ConditionElement):
_xml_tag = 'false-condition'
_xml_namespace = agp_cp_namespace
_xml_document = CommonPolicyDocument
class RuleDisplayName(XMLStringElement, RuleExtension):
_xml_tag = 'display-name'
_xml_namespace = agp_cp_namespace
_xml_document = CommonPolicyDocument
_xml_lang = True
Rule.register_extension('display_name', RuleDisplayName)
diff --git a/sipsimple/payloads/prescontent.py b/sipsimple/payloads/prescontent.py
index e8a3bc38..8257d47b 100644
--- a/sipsimple/payloads/prescontent.py
+++ b/sipsimple/payloads/prescontent.py
@@ -1,68 +1,68 @@
# Copyright (C) 2009-2011 AG Projects. See LICENSE for details.
#
"""Generates presence content application documents according to OMA TS Presence SIMPLE Content"""
__all__ = ['namespace', 'PresenceContentDocument', 'MIMEType', 'Encoding', 'Description', 'Data', 'PresenceContent']
from sipsimple.payloads import XMLDocument, XMLRootElement, XMLStringElement, XMLElementChild
namespace = 'urn:oma:xml:prs:pres-content'
-class PresenceContentDocument(XMLDocument): pass
+class PresenceContentDocument(XMLDocument):
+ content_type = "application/vnd.oma.pres-content+xml"
+
PresenceContentDocument.register_namespace(namespace, prefix=None, schema='oma-pres-content.xsd')
# Elements
class MIMEType(XMLStringElement):
_xml_tag = 'mime-type'
_xml_namespace = namespace
_xml_document = PresenceContentDocument
class Encoding(XMLStringElement):
_xml_tag = 'encoding'
_xml_namespace = namespace
_xml_document = PresenceContentDocument
class Description(XMLStringElement):
_xml_tag = 'description'
_xml_namespace = namespace
_xml_document = PresenceContentDocument
_xml_lang = True
class Data(XMLStringElement):
_xml_tag = 'data'
_xml_namespace = namespace
_xml_document = PresenceContentDocument
class PresenceContent(XMLRootElement):
- content_type = "application/vnd.oma.pres-content+xml"
-
_xml_tag = 'content'
_xml_namespace = namespace
_xml_document = PresenceContentDocument
_xml_children_order = {MIMEType.qname: 0,
Encoding.qname: 1,
Description.qname: 2,
Data.qname: 3,
None: 4}
mime_type = XMLElementChild('mime_type', type=MIMEType, required=False, test_equal=True)
encoding = XMLElementChild('encoding', type=Encoding, required=False, test_equal=True)
description = XMLElementChild('description', type=Description, required=False, test_equal=True)
data = XMLElementChild('data', type=Data, required=True, test_equal=True)
def __init__(self, data, mime_type=None, encoding=None, description=None):
XMLRootElement.__init__(self)
self.data = data
self.mime_type = mime_type
self.encoding = encoding
self.description = description
diff --git a/sipsimple/payloads/resourcelists.py b/sipsimple/payloads/resourcelists.py
index 9caf4f35..334f510f 100644
--- a/sipsimple/payloads/resourcelists.py
+++ b/sipsimple/payloads/resourcelists.py
@@ -1,343 +1,343 @@
# Copyright (C) 2008-2011 AG Projects. See LICENSE for details.
#
"""Resource lists (rfc4826) handling"""
__all__ = ['namespace',
'ResourceListsDocument',
'DisplayName',
'Entry',
'EntryRef',
'External',
'List',
'ResourceLists',
# Extensions
'EntryAttributes']
from collections import deque
from lxml import etree
from xml.sax.saxutils import quoteattr
from sipsimple.payloads import XMLDocument, XMLListRootElement, XMLElement, XMLListElement, XMLStringElement, XMLElementID, XMLElementChild, ThisClass, uri_attribute_builder, uri_attribute_parser
namespace = 'urn:ietf:params:xml:ns:resource-lists'
# excerpt from the RFC:
# <list>
# attribute "name" - optional, unique among the same level
# body: optional <display-name>, the sequence of entry/list/entry-ref/external
# <display-name>
# attribute xml:lang - optional
# body: utf8 string
# <entry>
# attribute "uri" - mandatory, unique among all other <uri> within the same parent
# body: optional <display-name>
# <entry-ref>
# attribute "ref" - mandatory, unique among all other <entry-ref> within the same parent
# body: optional <display-name>
# ref is a relative URI that resolves into <entry>
# <external>
# attribute "anchor" - mandatory, unique among all other anchor in <external> within the same parent
# anchor must be an absolute http uri that resolves into <list>
-class ResourceListsDocument(XMLDocument): pass
+class ResourceListsDocument(XMLDocument):
+ content_type = 'application/resource-lists+xml'
+
ResourceListsDocument.register_namespace(namespace, prefix='rl', schema='resourcelists.xsd')
## Marker mixins
class ListElement(object): pass
class EntryExtension(object): pass
## Elements
class DisplayName(XMLStringElement):
_xml_tag = 'display-name'
_xml_namespace = namespace
_xml_document = ResourceListsDocument
_xml_lang = True
class Entry(XMLElement):
_xml_tag = 'entry'
_xml_namespace = namespace
_xml_extension_type = EntryExtension
_xml_document = ResourceListsDocument
_xml_children_order = {DisplayName.qname: 0}
uri = XMLElementID('uri', type=unicode, required=True, test_equal=True, builder=uri_attribute_builder, parser=uri_attribute_parser)
display_name = XMLElementChild('display_name', type=DisplayName, required=False, test_equal=False)
def __init__(self, uri, display_name=None):
XMLElement.__init__(self)
self.uri = uri
self.display_name = display_name
def __unicode__(self):
return self.display_name and u'"%s" <%s>' % (self.display_name, self.uri) or self.uri
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.uri, self.display_name)
class EntryRef(XMLElement):
_xml_tag = 'entry-ref'
_xml_namespace = namespace
_xml_document = ResourceListsDocument
_xml_children_order = {DisplayName.qname: 0}
ref = XMLElementID('ref', type=unicode, required=True, test_equal=True, builder=uri_attribute_builder, parser=uri_attribute_parser)
display_name = XMLElementChild('display_name', type=DisplayName, required=False, test_equal=False)
def __init__(self, ref, display_name=None):
XMLElement.__init__(self)
self.ref = ref
self.display_name = display_name
def __unicode__(self):
return self.display_name and '"%s" <%s>' % (self.display_name, self.ref) or self.ref
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.ref, self.display_name)
class External(XMLElement):
_xml_tag = 'external'
_xml_namespace = namespace
_xml_document = ResourceListsDocument
_xml_children_order = {DisplayName.qname: 0}
anchor = XMLElementID('anchor', type=unicode, required=True, test_equal=True, builder=uri_attribute_builder, parser=uri_attribute_parser)
display_name = XMLElementChild('display_name', type=DisplayName, required=False, test_equal=False)
def __init__(self, anchor, display_name=None):
XMLElement.__init__(self)
self.anchor = anchor
self.display_name = display_name
def __unicode__(self):
return self.display_name and '"%s" <%s>' % (self.display_name, self.anchor) or self.anchor
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.anchor, self.display_name)
List = ThisClass # a List can contain items of its own kind
class List(XMLListElement):
_xml_tag = 'list'
_xml_namespace = namespace
_xml_document = ResourceListsDocument
_xml_children_order = {DisplayName.qname: 0,
Entry.qname: 1,
EntryRef.qname: 1,
External.qname: 1}
_xml_item_type = (Entry, EntryRef, External, List, ListElement)
name = XMLElementID('name', type=unicode, required=False, test_equal=True)
display_name = XMLElementChild('display_name', type=DisplayName, required=False, test_equal=False)
def __init__(self, entries=[], name=None, display_name=None):
XMLListElement.__init__(self)
self.name = name
self.display_name = display_name
self.update(entries)
def __repr__(self):
return '%s(%s, %r, %r)' % (self.__class__.__name__, list(self), self.name, self.display_name)
def __unicode__(self):
name = u'List element'
if self.name is not None:
name += u' %s' % self.name
if self.display_name is not None:
name += u' (%s)' % self.display_name
return name
List._xml_children_order[List.qname] = 1 # cannot self reference in declaration
class ResourceLists(XMLListRootElement):
- content_type = 'application/resource-lists+xml'
-
_xml_tag = 'resource-lists'
_xml_namespace = namespace
_xml_document = ResourceListsDocument
_xml_children_order = {List.qname: 0}
_xml_item_type = List
def __init__(self, lists=[]):
XMLListRootElement.__init__(self)
self.update(lists)
def __getitem__(self, key):
return self._xmlid_map[List][key]
def __delitem__(self, key):
self.remove(self._xmlid_map[List][key])
def get_xpath(self, element):
if not isinstance(element, (List, Entry, EntryRef, External, ResourceLists)):
raise ValueError('can only find xpath for List, Entry, EntryRef or External elements')
namespaces = dict((namespace, prefix) for prefix, namespace in self._xml_document.nsmap.iteritems())
namespaces[self._xml_namespace] = ''
prefix = namespaces[self._xml_namespace]
root_xpath = '/%s:%s' % (prefix, self._xml_tag) if prefix else '/'+self._xml_tag
if element is self:
return root_xpath
notexpanded = deque([self])
visited = set(notexpanded)
parents = {self: None}
obj = None
while notexpanded:
list = notexpanded.popleft()
for child in list:
if child is element:
parents[child] = list
obj = child
notexpanded.clear()
break
elif isinstance(child, List) and child not in visited:
parents[child] = list
notexpanded.append(child)
visited.add(child)
if obj is None:
return None
components = []
while obj is not self:
prefix = namespaces[obj._xml_namespace]
name = '%s:%s' % (prefix, obj._xml_tag) if prefix else obj._xml_tag
if isinstance(obj, List):
if obj.name is not None:
components.append('/%s[@%s=%s]' % (name, List.name.xmlname, quoteattr(obj.name)))
else:
siblings = [l for l in parents[obj] if isinstance(l, List)]
components.append('/%s[%d]' % (name, siblings.index(obj)+1))
elif isinstance(obj, Entry):
components.append('/%s[@%s=%s]' % (name, Entry.uri.xmlname, quoteattr(obj.uri)))
elif isinstance(obj, EntryRef):
components.append('/%s[@%s=%s]' % (name, EntryRef.ref.xmlname, quoteattr(obj.ref)))
elif isinstance(obj, External):
components.append('/%s[@%s=%s]' % (name, External.anchor.xmlname, quoteattr(obj.anchor)))
obj = parents[obj]
components.reverse()
xpointer = ''.join('xmlns(%s=%s)' % (prefix, namespace) for namespace, prefix in namespaces.iteritems() if prefix)
return root_xpath + ''.join(components) + ('?'+xpointer if xpointer else '')
def find_parent(self, element):
if not isinstance(element, (List, Entry, EntryRef, External)):
raise ValueError('can only find parent for List, Entry, EntryRef or External elements')
if element is self:
return None
notexpanded = deque([self])
visited = set(notexpanded)
while notexpanded:
list = notexpanded.popleft()
for child in list:
if child is element:
return list
elif isinstance(child, List) and child not in visited:
notexpanded.append(child)
visited.add(child)
return None
#
# Extensions
#
class EntryAttributes(XMLElement, EntryExtension):
_xml_tag = 'attributes'
_xml_namespace = 'urn:ag-projects:xml:ns:resource-lists'
_xml_document = ResourceListsDocument
def __init__(self, attributes={}):
XMLElement.__init__(self)
self._attributes = dict()
self.update(attributes)
def _parse_element(self, element):
self._attributes = dict()
attribute_tag = '{%s}attribute' % self._xml_namespace
for child in (child for child in element if child.tag == attribute_tag):
if 'nil' in child.attrib:
self[child.attrib['name']] = None
else:
self[child.attrib['name']] = unicode(child.text or u'')
def _build_element(self):
self.element.clear()
attribute_tag = '{%s}attribute' % self._xml_namespace
for key, value in self.iteritems():
child = etree.SubElement(self.element, attribute_tag, nsmap=self._xml_document.nsmap)
child.attrib['name'] = key
if value is None:
child.attrib['nil'] = 'true'
else:
child.text = value
def __contains__(self, key):
return key in self._attributes
def __iter__(self):
return iter(self._attributes)
def __getitem__(self, key):
return self._attributes[key]
def __setitem__(self, key, value):
self._attributes[key] = value
def __delitem__(self, key):
del self._attributes[key]
def clear(self):
self._attributes.clear()
def get(self, key, default=None):
return self._attributes.get(key, default)
def has_key(self, key):
return key in self._attributes
def items(self):
return self._attributes.items()
def iteritems(self):
return self._attributes.iteritems()
def iterkeys(self):
return self._attributes.iterkeys()
def itervalues(self):
return self._attributes.itervalues()
def keys(self):
return self._attributes.keys()
def pop(self, key, *args):
return self._attributes.pop(key, *args)
def popitem(self):
return self._attributes.popitem()
def setdefault(self, key, default=None):
return self._attributes.setdefault(key, default)
def update(self, attributes):
self._attributes.update(attributes)
ResourceListsDocument.register_namespace(EntryAttributes._xml_namespace, prefix='agp-rl')
Entry.register_extension('attributes', EntryAttributes)
diff --git a/sipsimple/payloads/rlsservices.py b/sipsimple/payloads/rlsservices.py
index a3009f9b..c0b1ecbe 100644
--- a/sipsimple/payloads/rlsservices.py
+++ b/sipsimple/payloads/rlsservices.py
@@ -1,145 +1,145 @@
# Copyright (C) 2008-2011 AG Projects. See LICENSE for details.
#
"""RFC4826 compliant parser/builder for application/rls-services+xml documents."""
__all__ = ['rl_namespace',
'rls_namespace',
'RLSServicesDocument',
'Packages',
'ResourceList',
'RLSList',
'Service',
'RLSServices']
import urllib
from sipsimple.payloads import XMLListRootElement, XMLElement, XMLListElement, XMLStringElement, XMLElementID, XMLElementChild, XMLElementChoiceChild
from sipsimple.payloads.resourcelists import namespace as rl_namespace, List, ResourceListsDocument
rls_namespace = 'urn:ietf:params:xml:ns:rls-services'
-class RLSServicesDocument(ResourceListsDocument): pass
+class RLSServicesDocument(ResourceListsDocument):
+ content_type = 'application/rls-services+xml'
+
RLSServicesDocument.register_namespace(rls_namespace, prefix=None, schema='rlsservices.xsd')
## Marker mixins
class PackagesElement(object): pass
## Elements
class Package(XMLStringElement):
_xml_tag = 'package'
_xml_namespace = rls_namespace
_xml_document = RLSServicesDocument
class Packages(XMLListElement):
_xml_tag = 'packages'
_xml_namespace = rls_namespace
_xml_document = RLSServicesDocument
_xml_children_order = {Package.qname: 0}
_xml_item_type = (Package, PackagesElement)
def __init__(self, packages=[]):
XMLListElement.__init__(self)
self.update(packages)
def __iter__(self):
return (unicode(item) if type(item) is Package else item for item in super(Packages, self).__iter__())
def add(self, item):
if isinstance(item, basestring):
item = Package(item)
super(Packages, self).add(item)
def remove(self, item):
if isinstance(item, basestring):
package = Package(item)
try:
item = (entry for entry in super(Packages, self).__iter__() if entry == package).next()
except StopIteration:
raise KeyError(item)
super(Packages, self).remove(item)
class ResourceList(XMLElement):
_xml_tag = 'resource-list'
_xml_namespace = rls_namespace
_xml_document = RLSServicesDocument
def __init__(self, value):
XMLElement.__init__(self)
self.value = value
def _get_value(self):
return self.__dict__['value']
def _set_value(self, value):
self.__dict__['value'] = unicode(value)
value = property(_get_value, _set_value)
del _get_value, _set_value
def _parse_element(self, element):
self.value = urllib.unquote(element.text).decode('utf-8')
def _build_element(self):
self.element.text = urllib.quote(self.value.encode('utf-8'))
# This is identical to the list element in resourcelists, except for the
# namespace. We'll redefine the xml tag just for readability purposes.
class RLSList(List):
_xml_tag = 'list'
_xml_namespace = rls_namespace
_xml_document = RLSServicesDocument
class Service(XMLElement):
_xml_tag = 'service'
_xml_namespace = rls_namespace
_xml_document = RLSServicesDocument
_xml_children_order = {RLSList.qname: 0,
ResourceList.qname: 0,
Packages.qname: 1}
uri = XMLElementID('uri', type=str, required=True, test_equal=True)
list = XMLElementChoiceChild('list', types=(ResourceList, RLSList), required=True, test_equal=True)
packages = XMLElementChild('packages', type=Packages, required=False, test_equal=True)
def __init__(self, uri, list=RLSList(), packages=Packages()):
XMLElement.__init__(self)
self.uri = uri
self.list = list
self.packages = packages
def __repr__(self):
return '%s(%r, %r, %r)' % (self.__class__.__name__, self.uri, self.list, self.packages)
class RLSServices(XMLListRootElement):
- content_type = 'application/rls-services+xml'
-
_xml_tag = 'rls-services'
_xml_namespace = rls_namespace
_xml_document = RLSServicesDocument
_xml_children_order = {Service.qname: 0}
_xml_item_type = Service
def __init__(self, services=[]):
XMLListRootElement.__init__(self)
self.update(services)
def __getitem__(self, key):
return self._xmlid_map[Service][key]
def __delitem__(self, key):
self.remove(self._xmlid_map[Service][key])
diff --git a/sipsimple/payloads/watcherinfo.py b/sipsimple/payloads/watcherinfo.py
index edcba857..218b44b3 100644
--- a/sipsimple/payloads/watcherinfo.py
+++ b/sipsimple/payloads/watcherinfo.py
@@ -1,207 +1,207 @@
# Copyright (C) 2008-2011 AG Projects. See LICENSE for details.
#
"""Parses application/watcherinfo+xml documents according to RFC3857 and RFC3858."""
__all__ = ['namespace',
'NeedFullUpdateError',
'WatcherInfoDocument',
'Watcher',
'WatcherList',
'WatcherInfo']
from sipsimple.payloads import ValidationError, XMLDocument, XMLElement, XMLListElement, XMLListRootElement, XMLElementID, XMLAttribute
from sipsimple.payloads.util import UnsignedLong, SIPURI
namespace = 'urn:ietf:params:xml:ns:watcherinfo'
class NeedFullUpdateError(Exception): pass
-class WatcherInfoDocument(XMLDocument): pass
+class WatcherInfoDocument(XMLDocument):
+ content_type = 'application/watcherinfo+xml'
+
WatcherInfoDocument.register_namespace(namespace, prefix=None, schema='watcherinfo.xsd')
## Attribute value types
class WatcherStatus(str):
def __new__(cls, value):
if value not in ('pending', 'active', 'waiting', 'terminated'):
raise ValueError('illegal status value for watcher')
return str.__new__(cls, value)
class WatcherEvent(str):
def __new__(cls, value):
if value not in ('subscribe', 'approved', 'deactivated', 'probation', 'rejected', 'timeout', 'giveup', 'noresource'):
raise ValueError('illegal event value for watcher')
return str.__new__(cls, value)
class WatcherInfoState(str):
def __new__(cls, value):
if value not in ('full', 'partial'):
raise ValueError('illegal state value for watcherinfo')
return str.__new__(cls, value)
## XMLElements
class Watcher(XMLElement):
"""
Definition for a watcher in a watcherinfo document
-
+
Provides the attributes:
* id
* status
* event
* display_name
* expiration
* duration
* sipuri
Can be transformed to a string with the format DISPLAY_NAME <SIP_URI>.
"""
_xml_tag = 'watcher'
_xml_namespace = namespace
_xml_document = WatcherInfoDocument
id = XMLElementID('id', type=str, required=True, test_equal=True)
status = XMLAttribute('status', type=WatcherStatus, required=True, test_equal=True)
event = XMLAttribute('event', type=WatcherEvent, required=True, test_equal=True)
display_name = XMLAttribute('display_name', xmlname='display-name', type=str, required=False, test_equal=True)
expiration = XMLAttribute('expiration', type=UnsignedLong, required=False, test_equal=False)
duration = XMLAttribute('duration', xmlname='duration-subscribed', type=UnsignedLong, required=False, test_equal=False)
def __init__(self, sipuri, id, status, event, display_name=None, expiration=None, duration=None):
XMLElement.__init__(self)
self.sipuri = sipuri
self.id = id
self.status = status
self.event = event
self.display_name = display_name
self.expiration = expiration
self.duration = duration
def __repr__(self):
return '%s(%r, %r, %r, %r, %r, %r, %r)' % (self.__class__.__name__, self.sipuri, self.id, self.status, self.event, self.display_name, self.expiration, self.duration)
def __str__(self):
return self.display_name and '"%s" <%s>' % (self.display_name, self.sipuri) or self.sipuri
def _get_sipuri(self):
return self.__dict__['sipuri']
def _set_sipuri(self, value):
if not isinstance(value, SIPURI):
value = SIPURI(value)
self.__dict__['sipuri'] = value
sipuri = property(_get_sipuri, _set_sipuri)
del _get_sipuri, _set_sipuri
def _parse_element(self, element):
try:
self.sipuri = element.text
except ValueError, e:
raise ValidationError("invalid SIPURI in Watcher: %s" % str(e))
def _build_element(self):
self.element.text = self.sipuri
class WatcherList(XMLListElement):
"""
Definition for a list of watchers in a watcherinfo document
It behaves like a list in that it can be indexed by a number, can be
iterated and counted.
It also provides the properties pending, active and terminated which are
generators returning Watcher objects with the corresponding status.
"""
_xml_tag = 'watcher-list'
_xml_namespace = namespace
_xml_document = WatcherInfoDocument
_xml_children_order = {Watcher.qname: 0}
_xml_item_type = Watcher
resource = XMLElementID('resource', type=SIPURI, required=True, test_equal=True)
package = XMLAttribute('package', type=str, required=True, test_equal=True)
def __init__(self, resource, package, watchers=[]):
XMLListElement.__init__(self)
self.resource = resource
self.package = package
self.update(watchers)
def __getitem__(self, key):
return self._xmlid_map[Watcher][key]
def __delitem__(self, key):
self.remove(self._xmlid_map[Watcher][key])
def __repr__(self):
return '%s(%r, %r, %r)' % (self.__class__.__name__, self.resource, self.package, list(self))
pending = property(lambda self: (watcher for watcher in self if watcher.status == 'pending'))
waiting = property(lambda self: (watcher for watcher in self if watcher.status == 'waiting'))
active = property(lambda self: (watcher for watcher in self if watcher.status == 'active'))
terminated = property(lambda self: (watcher for watcher in self if watcher.status == 'terminated'))
class WatcherInfo(XMLListRootElement):
"""
Definition for watcher info: a list of WatcherList elements
The user agent instantiates this class once it subscribes to a *.winfo event
and calls its update() method with the applicatin/watcherinfo+xml documents
it receives via NOTIFY.
The watchers can be accessed in two ways:
1. via the wlists property, which returns a list of WatcherList elements;
2. via the pending, active and terminated properties, which return
dictionaries, mapping WatcherList objects to lists of Watcher objects.
Since WatcherList objects can be compared for equality to SIP URI strings,
representing the presentity to which the watchers have subscribed, the
dictionaries can also be indexed by such strings.
"""
-
- content_type = 'application/watcherinfo+xml'
_xml_tag = 'watcherinfo'
_xml_namespace = namespace
_xml_document = WatcherInfoDocument
_xml_children_order = {WatcherList.qname: 0}
_xml_item_type = WatcherList
version = XMLAttribute('version', type=int, required=True, test_equal=True)
state = XMLAttribute('state', type=WatcherInfoState, required=True, test_equal=True)
def __init__(self, version=-1, state='full', wlists=[]):
XMLListRootElement.__init__(self)
self.version = version
self.state = state
self.update(wlists)
def __getitem__(self, key):
return self._xmlid_map[WatcherList][key]
def __delitem__(self, key):
self.remove(self._xmlid_map[WatcherList][key])
def __repr__(self):
return '%s(%r, %r, %r)' % (self.__class__.__name__, self.version, self.state, list(self))
wlists = property(lambda self: self._element_map.values())
pending = property(lambda self: dict((wlist, list(wlist.pending)) for wlist in self._element_map.itervalues()))
waiting = property(lambda self: dict((wlist, list(wlist.waiting)) for wlist in self._element_map.itervalues()))
active = property(lambda self: dict((wlist, list(wlist.active)) for wlist in self._element_map.itervalues()))
terminated = property(lambda self: dict((wlist, list(wlist.terminated)) for wlist in self._element_map.itervalues()))
diff --git a/sipsimple/payloads/xcapcaps.py b/sipsimple/payloads/xcapcaps.py
index 1bb56a75..65e395b9 100644
--- a/sipsimple/payloads/xcapcaps.py
+++ b/sipsimple/payloads/xcapcaps.py
@@ -1,145 +1,146 @@
# Copyright (C) 2010-2011 AG Projects. See LICENSE for details
#
"""Support for parsing and building xcap-caps documents, as defined by RFC4825."""
__all__ = ['XCAPCapabilitiesDocument', 'AUIDS', 'Extensions', 'Namespaces', 'XCAPCapabilities']
from sipsimple.payloads import XMLDocument, XMLElementChild, XMLListElement, XMLRootElement, XMLStringElement
namespace = 'urn:ietf:params:xml:ns:xcap-caps'
-class XCAPCapabilitiesDocument(XMLDocument): pass
+class XCAPCapabilitiesDocument(XMLDocument):
+ content_type = 'application/xcap-caps+xml'
+
XCAPCapabilitiesDocument.register_namespace(namespace, prefix=None, schema='xcap-caps.xsd')
## Elements
class AUID(XMLStringElement):
_xml_tag = 'auid'
_xml_namespace = namespace
_xml_document = XCAPCapabilitiesDocument
class AUIDS(XMLListElement):
_xml_tag = 'auids'
_xml_namespace = namespace
_xml_document = XCAPCapabilitiesDocument
_xml_item_type = AUID
def __init__(self, children=[]):
XMLListElement.__init__(self)
self.update(children)
def __iter__(self):
return (unicode(item) for item in super(AUIDS, self).__iter__())
def add(self, item):
if isinstance(item, basestring):
item = AUID(item)
super(AUIDS, self).add(item)
def remove(self, item):
if isinstance(item, basestring):
try:
item = (entry for entry in super(AUIDS, self).__iter__() if entry == item).next()
except StopIteration:
raise KeyError(item)
super(AUIDS, self).remove(item)
class Extension(XMLStringElement):
_xml_tag = 'extension'
_xml_namespace = namespace
_xml_document = XCAPCapabilitiesDocument
class Extensions(XMLListElement):
_xml_tag = 'extensions'
_xml_namespace = namespace
_xml_document = XCAPCapabilitiesDocument
_xml_item_type = Extension
def __init__(self, children=[]):
XMLListElement.__init__(self)
self.update(children)
def __iter__(self):
return (unicode(item) for item in super(Extensions, self).__iter__())
def add(self, item):
if isinstance(item, basestring):
item = Extension(item)
super(Extensions, self).add(item)
def remove(self, item):
if isinstance(item, basestring):
try:
item = (entry for entry in super(Extensions, self).__iter__() if entry == item).next()
except StopIteration:
raise KeyError(item)
super(Extensions, self).remove(item)
class Namespace(XMLStringElement):
_xml_tag = 'extension'
_xml_namespace = namespace
_xml_document = XCAPCapabilitiesDocument
class Namespaces(XMLListElement):
_xml_tag = 'namespaces'
_xml_namespace = namespace
_xml_document = XCAPCapabilitiesDocument
_xml_item_type = Namespace
def __init__(self, children=[]):
XMLListElement.__init__(self)
self.update(children)
def __iter__(self):
return (unicode(item) for item in super(Namespaces, self).__iter__())
def add(self, item):
if isinstance(item, basestring):
item = Namespace(item)
super(Namespaces, self).add(item)
def remove(self, item):
if isinstance(item, basestring):
try:
item = (entry for entry in super(Namespaces, self).__iter__() if entry == item).next()
except StopIteration:
raise KeyError(item)
super(Namespaces, self).remove(item)
class XCAPCapabilities(XMLRootElement):
- content_type = 'application/xcap-caps+xml'
_xml_tag = 'xcap-caps'
_xml_namespace = namespace
_xml_document = XCAPCapabilitiesDocument
_xml_children_order = {AUIDS.qname: 0,
Extensions.qname: 1,
Namespaces.qname: 2}
auids = XMLElementChild('auids', type=AUIDS, required=True, test_equal=True)
extensions = XMLElementChild('extensions', type=Extensions, required=False, test_equal=True)
namespaces = XMLElementChild('namespaces', type=Namespaces, required=True, test_equal=True)
def __init__(self, auids=[], extensions=[], namespaces=[]):
XMLRootElement.__init__(self)
self.auids = AUIDS(auids)
self.extensions = Extensions(extensions)
self.namespaces = Namespaces(namespaces)
def __repr__(self):
return '%s(%r, %r, %r)' % (self.__class__.__name__, self.auids, self.extensions, self.namespaces)
diff --git a/sipsimple/payloads/xcapdiff.py b/sipsimple/payloads/xcapdiff.py
index 07448685..b19e66b6 100644
--- a/sipsimple/payloads/xcapdiff.py
+++ b/sipsimple/payloads/xcapdiff.py
@@ -1,113 +1,113 @@
# Copyright (C) 2008-2011 AG Projects. See LICENSE for details.
#
"""
This module allows parsing and building xcap-diff documents according to
RFC 5874.
"""
__all__ = ['namespace', 'XCAPDiffDocument', 'BodyNotChanged', 'Document', 'Element', 'Attribute', 'XCAPDiff']
from sipsimple.payloads import XMLDocument, XMLElement, XMLListRootElement, XMLStringElement, XMLEmptyElement, XMLAttribute, XMLElementID, XMLElementChild
from sipsimple.payloads.util import XCAPURI, Boolean
namespace = 'urn:ietf:params:xml:ns:xcap-diff'
-class XCAPDiffDocument(XMLDocument): pass
+class XCAPDiffDocument(XMLDocument):
+ content_type = 'application/xcap-diff+xml'
+
XCAPDiffDocument.register_namespace(namespace, prefix=None, schema='xcapdiff.xsd')
class BodyNotChanged(XMLEmptyElement):
_xml_tag = 'body-not-changed'
_xml_namespace = namespace
_xml_document = XCAPDiffDocument
class Document(XMLElement):
_xml_tag = 'document'
_xml_namespace = namespace
_xml_document = XCAPDiffDocument
selector = XMLElementID('selector', xmlname='sel', type=XCAPURI, required=True, test_equal=True)
new_etag = XMLAttribute('new_etag', xmlname='new-etag', type=str, required=False, test_equal=True)
previous_etag = XMLAttribute('previous_etag', xmlname='previous-etag', type=str, required=False, test_equal=True)
body_not_changed = XMLElementChild('body_not_changed', type=BodyNotChanged, required=False, test_equal=True)
def __init__(self, selector, new_etag=None, previous_etag=None):
XMLElement.__init__(self)
self.selector = selector
self.new_etag = new_etag
self.previous_etag = previous_etag
def __repr__(self):
return '%s(%r, %r, %r)' % (self.__class__.__name__, self.selector, self.new_etag, self.previous_etag)
def _get_empty_body(self):
return self.body_not_changed is not None
def _set_empty_body(self, body_not_changed):
if body_not_changed:
self.body_not_changed = BodyNotChanged()
else:
self.body_not_changed = None
empty_body = property(_get_empty_body, _set_empty_body)
del _get_empty_body, _set_empty_body
class Element(XMLElement):
_xml_tag = 'element'
_xml_namespace = namespace
_xml_document = XCAPDiffDocument
selector = XMLElementID('selector', xmlname='sel', type=XCAPURI, required=True, test_equal=True)
exists = XMLAttribute('exists', type=Boolean, required=False, test_equal=True)
def __init__(self, selector, exists=None):
XMLElement.__init__(self)
self.selector = selector
self.exists = exists
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.selector, self.exists)
class Attribute(XMLStringElement):
_xml_tag = 'attribute'
_xml_namespace = namespace
_xml_document = XCAPDiffDocument
selector = XMLElementID('selector', xmlname='sel', type=XCAPURI, required=True, test_equal=True)
exists = XMLAttribute('exists', type=Boolean, required=False, test_equal=True)
def __init__(self, selector, exists=None):
XMLStringElement.__init__(self)
self.selector = selector
self.exists = exists
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.selector, self.exists)
class XCAPDiff(XMLListRootElement):
- content_type = 'application/xcap-diff+xml'
-
_xml_tag = 'xcap-diff'
_xml_namespace = namespace
_xml_document = XCAPDiffDocument
_xml_item_type = (Document, Element, Attribute)
xcap_root = XMLElementID('xcap_root', xmlname='xcap-root', type=str, required=True, test_equal=True)
def __init__(self, xcap_root, children=[]):
XMLListRootElement.__init__(self)
self.xcap_root = xcap_root
self.update(children)
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.xcap_root, list(self))
diff --git a/sipsimple/session.py b/sipsimple/session.py
index 1ef4bfcc..ad952372 100644
--- a/sipsimple/session.py
+++ b/sipsimple/session.py
@@ -1,2434 +1,2434 @@
# Copyright (C) 2008-2011 AG Projects. See LICENSE for details.
#
"""
Implements an asynchronous notification based mechanism for
establishment, modification and termination of sessions using Session
Initiation Protocol (SIP) standardized in RFC3261.
"""
from __future__ import absolute_import, with_statement
__all__ = ['Session', 'SessionManager']
import random
from datetime import datetime
from threading import RLock
from time import time
from application.notification import IObserver, Notification, NotificationCenter
from application.python import Null, limit
from application.python.decorator import decorator, preserve_signature
from application.python.types import Singleton
from application.system import host
from eventlet import api, coros, proc
from eventlet.coros import queue
from twisted.internet import reactor
from zope.interface import implements
from sipsimple.core import DialogID, Engine, Invitation, Referral, Subscription, PJSIPError, SIPCoreError, SIPCoreInvalidStateError, SIPURI, sip_status_messages, sipfrag_re
from sipsimple.core import ContactHeader, FromHeader, Header, ReasonHeader, ReferToHeader, ReplacesHeader, RouteHeader, SubjectHeader, ToHeader, WarningHeader
from sipsimple.core import SDPConnection, SDPMediaStream, SDPSession
from sipsimple.account import AccountManager, BonjourAccount
from sipsimple.configuration.settings import SIPSimpleSettings
from sipsimple.lookup import DNSLookup, DNSLookupError
from sipsimple.payloads import ValidationError
-from sipsimple.payloads.conference import Conference
+from sipsimple.payloads.conference import ConferenceDocument
from sipsimple.streams import AudioStream, MediaStreamRegistry, InvalidStreamError, UnknownStreamError
from sipsimple.threading import run_in_twisted_thread
from sipsimple.threading.green import Command, run_in_green_thread
from sipsimple.util import TimestampedNotificationData
class InvitationDisconnectedError(Exception):
def __init__(self, invitation, data):
self.invitation = invitation
self.data = data
class MediaStreamDidFailError(Exception):
def __init__(self, stream, data):
self.stream = stream
self.data = data
class SubscriptionError(Exception):
def __init__(self, error, timeout, refresh_interval=None):
self.error = error
self.refresh_interval = refresh_interval
self.timeout = timeout
class SIPSubscriptionDidFail(Exception):
def __init__(self, data):
self.data = data
class InterruptSubscription(Exception):
pass
class TerminateSubscription(Exception):
pass
class ReferralError(Exception):
def __init__(self, error, code=0):
self.error = error
self.code = code
class TerminateReferral(Exception):
pass
class SIPReferralDidFail(Exception):
def __init__(self, data):
self.data = data
class IllegalStateError(RuntimeError):
pass
class IllegalDirectionError(Exception):
pass
class SIPInvitationTransferDidFail(Exception):
def __init__(self, data):
self.data = data
@decorator
def transition_state(required_state, new_state):
def state_transitioner(func):
@preserve_signature(func)
def wrapper(obj, *args, **kwargs):
with obj._lock:
if obj.state != required_state:
raise IllegalStateError('cannot call %s in %s state' % (func.__name__, obj.state))
obj.state = new_state
return func(obj, *args, **kwargs)
return wrapper
return state_transitioner
@decorator
def check_state(required_states):
def state_checker(func):
@preserve_signature(func)
def wrapper(obj, *args, **kwargs):
if obj.state not in required_states:
raise IllegalStateError('cannot call %s in %s state' % (func.__name__, obj.state))
return func(obj, *args, **kwargs)
return wrapper
return state_checker
@decorator
def check_transfer_state(direction, state):
def state_checker(func):
@preserve_signature(func)
def wrapper(obj, *args, **kwargs):
if obj.transfer_handler.direction != direction:
raise IllegalDirectionError('cannot transfer in %s direction' % obj.transfer_handler.direction)
if obj.transfer_handler.state != state:
raise IllegalStateError('cannot transfer in %s state' % obj.transfer_handler.state)
return func(obj, *args, **kwargs)
return wrapper
return state_checker
class AddParticipantOperation(object):
pass
class RemoveParticipantOperation(object):
pass
class ReferralHandler(object):
implements(IObserver)
def __init__(self, session, participant_uri, operation):
self.participant_uri = participant_uri
if not isinstance(self.participant_uri, SIPURI):
if not self.participant_uri.startswith(('sip:', 'sips:')):
self.participant_uri = 'sip:%s' % self.participant_uri
try:
self.participant_uri = SIPURI.parse(str(self.participant_uri)) # FIXME: SIPURI is not unicode friendly and expects a str.
except SIPCoreError:
notification_center = NotificationCenter()
if operation is AddParticipantOperation:
notification_center.post_notification('SIPConferenceDidNotAddParticipant', sender=session, data=TimestampedNotificationData(participant=self.participant_uri, code=0, reason='invalid participant URI'))
else:
notification_center.post_notification('SIPConferenceDidNotRemoveParticipant', sender=session, data=TimestampedNotificationData(participant=self.participant_uri, code=0, reason='invalid participant URI'))
return
self.session = session
self.operation = operation
self.active = False
self.route = None
self._channel = coros.queue()
self._referral = None
self._wakeup_timer = None
def start(self):
notification_center = NotificationCenter()
if not self.session.remote_focus:
if self.operation is AddParticipantOperation:
notification_center.post_notification('SIPConferenceDidNotAddParticipant', sender=self.session, data=TimestampedNotificationData(participant=self.participant_uri, code=0, reason='remote endpoint is not a focus'))
else:
notification_center.post_notification('SIPConferenceDidNotRemoveParticipant', sender=self.session, data=TimestampedNotificationData(participant=self.participant_uri, code=0, reason='remote endpoint is not a focus'))
self.session = None
return
notification_center.add_observer(self, sender=self.session)
notification_center.add_observer(self, name='DNSNameserversDidChange')
notification_center.add_observer(self, name='SystemIPAddressDidChange')
notification_center.add_observer(self, name='SystemDidWakeUpFromSleep')
proc.spawn(self._run)
def _run(self):
notification_center = NotificationCenter()
settings = SIPSimpleSettings()
try:
# Lookup routes
account = self.session.account
if account is BonjourAccount():
uri = SIPURI.new(self.session._invitation.remote_contact_header.uri)
elif account.sip.outbound_proxy is not None:
uri = SIPURI(host=account.sip.outbound_proxy.host,
port=account.sip.outbound_proxy.port,
parameters={'transport': account.sip.outbound_proxy.transport})
elif account.sip.always_use_my_proxy:
uri = SIPURI(host=account.id.domain)
else:
uri = SIPURI.new(self.session.remote_identity.uri)
lookup = DNSLookup()
try:
routes = lookup.lookup_sip_proxy(uri, settings.sip.transport_list).wait()
except DNSLookupError, e:
timeout = random.uniform(15, 30)
raise ReferralError(error='DNS lookup failed: %s' % e)
target_uri = SIPURI.new(self.session.remote_identity.uri)
timeout = time() + 30
for route in routes:
self.route = route
remaining_time = timeout - time()
if remaining_time > 0:
try:
contact_uri = account.contact[route]
except KeyError:
continue
refer_to_header = ReferToHeader(str(self.participant_uri))
refer_to_header.parameters['method'] = 'INVITE' if self.operation is AddParticipantOperation else 'BYE'
referral = Referral(target_uri, FromHeader(account.uri, account.display_name),
ToHeader(target_uri),
refer_to_header,
ContactHeader(contact_uri),
RouteHeader(route.get_uri()),
account.credentials)
notification_center.add_observer(self, sender=referral)
try:
referral.send_refer(timeout=limit(remaining_time, min=1, max=5))
except SIPCoreError:
notification_center.remove_observer(self, sender=referral)
timeout = 5
raise ReferralError(error='Internal error')
self._referral = referral
try:
while True:
notification = self._channel.wait()
if notification.name == 'SIPReferralDidStart':
break
except SIPReferralDidFail, e:
notification_center.remove_observer(self, sender=referral)
self._referral = None
if e.data.code in (403, 405):
raise ReferralError(error=sip_status_messages[e.data.code], code=e.data.code)
else:
# Otherwise just try the next route
continue
else:
break
else:
self.route = None
raise ReferralError(error='No more routes to try')
# At this point it is subscribed. Handle notifications and ending/failures.
try:
self.active = True
while True:
notification = self._channel.wait()
if notification.name == 'SIPReferralGotNotify':
if notification.data.event == 'refer' and notification.data.body:
match = sipfrag_re.match(notification.data.body)
if match:
code = int(match.group('code'))
reason = match.group('reason')
if self.operation is AddParticipantOperation:
notification_center.post_notification('SIPConferenceGotAddParticipantProgress', sender=self.session, data=TimestampedNotificationData(participant=self.participant_uri, code=code, reason=reason))
else:
notification_center.post_notification('SIPConferenceGotRemoveParticipantProgress', sender=self.session, data=TimestampedNotificationData(participant=self.participant_uri, code=code, reason=reason))
elif notification.name == 'SIPReferralDidEnd':
break
except SIPReferralDidFail, e:
notification_center.remove_observer(self, sender=self._referral)
raise ReferralError(error=e.data.reason, code=e.data.code)
else:
notification_center.remove_observer(self, sender=self._referral)
if self.operation is AddParticipantOperation:
notification_center.post_notification('SIPConferenceDidAddParticipant', sender=self.session, data=TimestampedNotificationData(participant=self.participant_uri))
else:
notification_center.post_notification('SIPConferenceDidRemoveParticipant', sender=self.session, data=TimestampedNotificationData(participant=self.participant_uri))
finally:
self.active = False
except TerminateReferral:
if self._referral is not None:
try:
self._referral.end(timeout=2)
except SIPCoreError:
pass
else:
try:
while True:
notification = self._channel.wait()
if notification.name == 'SIPReferralDidEnd':
break
except SIPReferralDidFail:
pass
finally:
notification_center.remove_observer(self, sender=self._referral)
if self.operation is AddParticipantOperation:
notification_center.post_notification('SIPConferenceDidNotAddParticipant', sender=self.session, data=TimestampedNotificationData(participant=self.participant_uri, code=0, reason='error'))
else:
notification_center.post_notification('SIPConferenceDidNotRemoveParticipant', sender=self.session, data=TimestampedNotificationData(participant=self.participant_uri, code=0, reason='error'))
except ReferralError, e:
if self.operation is AddParticipantOperation:
notification_center.post_notification('SIPConferenceDidNotAddParticipant', sender=self.session, data=TimestampedNotificationData(participant=self.participant_uri, code=e.code, reason=e.error))
else:
notification_center.post_notification('SIPConferenceDidNotRemoveParticipant', sender=self.session, data=TimestampedNotificationData(participant=self.participant_uri, code=e.code, reason=e.error))
finally:
if self._wakeup_timer is not None and self._wakeup_timer.active():
self._wakeup_timer.cancel()
self._wakeup_timer = None
notification_center.remove_observer(self, sender=self.session)
notification_center.remove_observer(self, name='DNSNameserversDidChange')
notification_center.remove_observer(self, name='SystemIPAddressDidChange')
notification_center.remove_observer(self, name='SystemDidWakeUpFromSleep')
self.session = None
self._referral = None
def _refresh(self):
try:
contact_header = ContactHeader(self.session.account.contact[self.route])
except KeyError:
pass
else:
try:
self._referral.refresh(contact_header=contact_header, timeout=2)
except (SIPCoreError, SIPCoreInvalidStateError):
pass
@run_in_twisted_thread
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, Null)
handler(notification)
def _NH_SIPReferralDidStart(self, notification):
self._channel.send(notification)
def _NH_SIPReferralDidEnd(self, notification):
self._channel.send(notification)
def _NH_SIPReferralDidFail(self, notification):
self._channel.send_exception(SIPReferralDidFail(notification.data))
def _NH_SIPReferralGotNotify(self, notification):
self._channel.send(notification)
def _NH_SIPSessionDidFail(self, notification):
if self._wakeup_timer is not None and self._wakeup_timer.active():
self._wakeup_timer.cancel()
self._wakeup_timer = None
self._channel.send_exception(TerminateReferral())
def _NH_SIPSessionWillEnd(self, notification):
if self._wakeup_timer is not None and self._wakeup_timer.active():
self._wakeup_timer.cancel()
self._wakeup_timer = None
self._channel.send_exception(TerminateReferral())
def _NH_DNSNameserversDidChange(self, notification):
if self.active:
self._refresh()
def _NH_SystemIPAddressDidChange(self, notification):
if self.active:
self._refresh()
def _NH_SystemDidWakeUpFromSleep(self, notification):
if self._wakeup_timer is None:
def wakeup_action():
if self.active:
self._refresh()
self._wakeup_timer = None
self._wakeup_timer = reactor.callLater(5, wakeup_action) # wait for system to stabilize
class ConferenceHandler(object):
implements(IObserver)
def __init__(self, session):
self.session = session
self.active = False
self.subscribed = False
self._command_proc = None
self._command_channel = coros.queue()
self._data_channel = coros.queue()
self._subscription = None
self._subscription_proc = None
self._subscription_timer = None
self._wakeup_timer = None
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=self.session)
notification_center.add_observer(self, name='DNSNameserversDidChange')
notification_center.add_observer(self, name='SystemIPAddressDidChange')
notification_center.add_observer(self, name='SystemDidWakeUpFromSleep')
self._command_proc = proc.spawn(self._run)
@run_in_green_thread
def add_participant(self, participant_uri):
referral_handler = ReferralHandler(self.session, participant_uri, AddParticipantOperation)
referral_handler.start()
@run_in_green_thread
def remove_participant(self, participant_uri):
referral_handler = ReferralHandler(self.session, participant_uri, RemoveParticipantOperation)
referral_handler.start()
def _run(self):
while True:
command = self._command_channel.wait()
handler = getattr(self, '_CH_%s' % command.name)
handler(command)
def _activate(self):
self.active = True
command = Command('subscribe')
self._command_channel.send(command)
return command
def _deactivate(self):
self.active = False
command = Command('unsubscribe')
self._command_channel.send(command)
return command
def _resubscribe(self):
command = Command('subscribe')
self._command_channel.send(command)
return command
def _terminate(self):
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=self.session)
notification_center.remove_observer(self, name='DNSNameserversDidChange')
notification_center.remove_observer(self, name='SystemIPAddressDidChange')
notification_center.remove_observer(self, name='SystemDidWakeUpFromSleep')
self._deactivate().wait()
self._command_proc.kill()
self.session = None
def _CH_subscribe(self, command):
if self._subscription_timer is not None and self._subscription_timer.active():
self._subscription_timer.cancel()
self._subscription_timer = None
if self._subscription_proc is not None:
subscription_proc = self._subscription_proc
subscription_proc.kill(InterruptSubscription)
subscription_proc.wait()
self._subscription_proc = proc.spawn(self._subscription_handler, command)
def _CH_unsubscribe(self, command):
# Cancel any timer which would restart the subscription process
if self._subscription_timer is not None and self._subscription_timer.active():
self._subscription_timer.cancel()
self._subscription_timer = None
if self._wakeup_timer is not None and self._wakeup_timer.active():
self._wakeup_timer.cancel()
self._wakeup_timer = None
if self._subscription_proc is not None:
subscription_proc = self._subscription_proc
subscription_proc.kill(TerminateSubscription)
subscription_proc.wait()
self._subscription_proc = None
command.signal()
def _subscription_handler(self, command):
notification_center = NotificationCenter()
settings = SIPSimpleSettings()
try:
# Lookup routes
account = self.session.account
if account is BonjourAccount():
uri = SIPURI.new(self.session._invitation.remote_contact_header.uri)
elif account.sip.outbound_proxy is not None:
uri = SIPURI(host=account.sip.outbound_proxy.host,
port=account.sip.outbound_proxy.port,
parameters={'transport': account.sip.outbound_proxy.transport})
elif account.sip.always_use_my_proxy:
uri = SIPURI(host=account.id.domain)
else:
uri = SIPURI.new(self.session.remote_identity.uri)
lookup = DNSLookup()
try:
routes = lookup.lookup_sip_proxy(uri, settings.sip.transport_list).wait()
except DNSLookupError, e:
timeout = random.uniform(15, 30)
raise SubscriptionError(error='DNS lookup failed: %s' % e, timeout=timeout)
target_uri = SIPURI.new(self.session.remote_identity.uri)
timeout = time() + 30
for route in routes:
remaining_time = timeout - time()
if remaining_time > 0:
try:
contact_uri = account.contact[route]
except KeyError:
continue
subscription = Subscription(target_uri, FromHeader(account.uri, account.display_name),
ToHeader(target_uri),
ContactHeader(contact_uri),
'conference',
RouteHeader(route.get_uri()),
credentials=account.credentials,
refresh=3600)
notification_center.add_observer(self, sender=subscription)
try:
subscription.subscribe(timeout=limit(remaining_time, min=1, max=5))
except (PJSIPError, SIPCoreError):
notification_center.remove_observer(self, sender=subscription)
timeout = 5
raise SubscriptionError(error='Internal error', timeout=timeout)
self._subscription = subscription
try:
while True:
notification = self._data_channel.wait()
if notification.sender is subscription and notification.name == 'SIPSubscriptionDidStart':
break
except SIPSubscriptionDidFail, e:
notification_center.remove_observer(self, sender=subscription)
self._subscription = None
if e.data.code == 407:
# Authentication failed, so retry the subscription in some time
timeout = random.uniform(60, 120)
raise SubscriptionError(error='Authentication failed', timeout=timeout)
elif e.data.code == 423:
# Get the value of the Min-Expires header
timeout = random.uniform(60, 120)
if e.data.min_expires is not None and e.data.min_expires > account.sip.subscribe_interval:
raise SubscriptionError(error='Interval too short', timeout=timeout, refresh_interval=e.data.min_expires)
else:
raise SubscriptionError(error='Interval too short', timeout=timeout)
elif e.data.code in (405, 406, 489, 1400):
command.signal(e)
return
else:
# Otherwise just try the next route
continue
else:
self.subscribed = True
command.signal()
break
else:
# There are no more routes to try, reschedule the subscription
timeout = random.uniform(60, 180)
raise SubscriptionError(error='No more routes to try', timeout=timeout)
# At this point it is subscribed. Handle notifications and ending/failures.
try:
while True:
notification = self._data_channel.wait()
if notification.sender is not self._subscription:
continue
if notification.name == 'SIPSubscriptionGotNotify':
if notification.data.event == 'conference' and notification.data.body:
try:
- conference_info = Conference.parse(notification.data.body)
+ conference_info = ConferenceDocument.parse(notification.data.body)
except ValidationError:
pass
else:
notification_center.post_notification('SIPSessionGotConferenceInfo', sender=self.session, data=TimestampedNotificationData(conference_info=conference_info))
elif notification.name == 'SIPSubscriptionDidEnd':
break
except SIPSubscriptionDidFail:
self._command_channel.send(Command('subscribe'))
notification_center.remove_observer(self, sender=self._subscription)
except InterruptSubscription, e:
if not self.subscribed:
command.signal(e)
if self._subscription is not None:
notification_center.remove_observer(self, sender=self._subscription)
try:
self._subscription.end(timeout=2)
except SIPCoreError:
pass
except TerminateSubscription, e:
if not self.subscribed:
command.signal(e)
if self._subscription is not None:
try:
self._subscription.end(timeout=2)
except SIPCoreError:
pass
else:
try:
while True:
notification = self._data_channel.wait()
if notification.sender is self._subscription and notification.name == 'SIPSubscriptionDidEnd':
break
except SIPSubscriptionDidFail:
pass
finally:
notification_center.remove_observer(self, sender=self._subscription)
except SubscriptionError, e:
self._subscription_timer = reactor.callLater(e.timeout, self._command_channel.send, Command('subscribe', command.event, refresh_interval=e.refresh_interval))
finally:
self.subscribed = False
self._subscription = None
self._subscription_proc = None
@run_in_twisted_thread
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, Null)
handler(notification)
def _NH_SIPSubscriptionDidStart(self, notification):
self._data_channel.send(notification)
def _NH_SIPSubscriptionDidEnd(self, notification):
self._data_channel.send(notification)
def _NH_SIPSubscriptionDidFail(self, notification):
self._data_channel.send_exception(SIPSubscriptionDidFail(notification.data))
def _NH_SIPSubscriptionGotNotify(self, notification):
self._data_channel.send(notification)
def _NH_SIPSessionDidStart(self, notification):
if self.session.remote_focus:
self._activate()
@run_in_green_thread
def _NH_SIPSessionDidFail(self, notification):
self._terminate()
@run_in_green_thread
def _NH_SIPSessionDidEnd(self, notification):
self._terminate()
def _NH_SIPSessionDidRenegotiateStreams(self, notification):
if self.session.remote_focus and not self.active:
self._activate()
elif not self.session.remote_focus and self.active:
self._deactivate()
def _NH_DNSNameserversDidChange(self, notification):
if self.active:
self._resubscribe()
def _NH_SystemIPAddressDidChange(self, notification):
if self.active:
self._resubscribe()
def _NH_SystemDidWakeUpFromSleep(self, notification):
if self._wakeup_timer is None:
def wakeup_action():
if self.active:
self._resubscribe()
self._wakeup_timer = None
self._wakeup_timer = reactor.callLater(5, wakeup_action) # wait for system to stabilize
class TransferInfo(object):
def __init__(self, referred_by=None, replaced_dialog_id=None):
self.referred_by = referred_by
self.replaced_dialog_id = replaced_dialog_id
class TransferHandler(object):
implements(IObserver)
def __init__(self, session):
self.direction = None
self.new_session = None
self.session = session
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=self.session)
notification_center.add_observer(self, sender=self.session._invitation)
self._command_channel = coros.queue()
self._data_channel = coros.queue()
self._proc = proc.spawn(self._run)
def _run(self):
while True:
command = self._command_channel.wait()
handler = getattr(self, '_CH_%s' % command.name)
handler(command)
self.direction = None
self.state = None
def _CH_incoming_transfer(self, command):
self.direction = 'incoming'
notification_center = NotificationCenter()
refer_to_hdr = command.data.headers.get('Refer-To')
target = SIPURI.parse(refer_to_hdr.uri)
referred_by_hdr = command.data.headers.get('Referred-By', None)
if referred_by_hdr is not None:
origin = referred_by_hdr.body
else:
origin = None
try:
while True:
try:
notification = self._data_channel.wait()
except SIPInvitationTransferDidFail:
self.state = 'failed'
return
else:
if notification.name == 'SIPInvitationTransferDidStart':
self.state = 'starting'
refer_to_uri = SIPURI.new(target)
refer_to_uri.headers = {}
refer_to_uri.parameters = {}
notification_center.post_notification('SIPSessionTransferNewIncoming', self.session, TimestampedNotificationData(transfer_destination=refer_to_uri, transfer_source=origin))
elif notification.name == 'SIPSessionTransferDidStart':
break
elif notification.name == 'SIPSessionTransferDidFail':
self.state = 'failed'
try:
self.session._invitation.notify_transfer_progress(notification.data.code)
except SIPCoreError:
return
while True:
try:
notification = self._data_channel.wait()
except SIPInvitationTransferDidFail:
return
self.state = 'started'
transfer_info = TransferInfo(referred_by=origin)
try:
replaces_hdr = target.headers.pop('Replaces')
call_id, rest = replaces_hdr.split(';', 1)
params = dict((item.split('=') for item in rest.split(';')))
to_tag = params.get('to-tag')
from_tag = params.get('from-tag')
except (KeyError, ValueError):
pass
else:
transfer_info.replaced_dialog_id = DialogID(call_id, local_tag=from_tag, remote_tag=to_tag)
settings = SIPSimpleSettings()
account = self.session.account
if account is BonjourAccount():
uri = target
elif account.sip.outbound_proxy is not None:
uri = SIPURI(host=account.sip.outbound_proxy.host,
port=account.sip.outbound_proxy.port,
parameters={'transport': account.sip.outbound_proxy.transport})
elif account.sip.always_use_my_proxy:
uri = SIPURI(host=account.id.domain)
else:
uri = target
lookup = DNSLookup()
try:
routes = lookup.lookup_sip_proxy(uri, settings.sip.transport_list).wait()
except DNSLookupError, e:
self.state = 'failed'
notification_center.post_notification('SIPSessionTransferDidFail', sender=self.session, data=TimestampedNotificationData(code=e.data.code, reason=e.data.reason))
try:
self.session._invitation.notify_transfer_progress(480)
except SIPCoreError:
pass
while True:
try:
notification = self._data_channel.wait()
except SIPInvitationTransferDidFail:
return
account = self.session.account
self.new_session = Session(account)
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=self.new_session)
self.new_session.connect(ToHeader(target), routes=routes, streams=[AudioStream(account)], transfer_info=transfer_info)
while True:
try:
notification = self._data_channel.wait()
except SIPInvitationTransferDidFail:
return
if notification.name == 'SIPInvitationTransferDidEnd':
return
except proc.ProcExit:
if self.new_session is not None:
notification_center.remove_observer(self, sender=self.new_session)
self.new_session = None
raise
def _CH_outgoing_transfer(self, command):
self.direction = 'outgoing'
notification_center = NotificationCenter()
self.state = 'starting'
while True:
try:
notification = self._data_channel.wait()
except SIPInvitationTransferDidFail, e:
self.state = 'failed'
notification_center.post_notification('SIPSessionTransferDidFail', sender=self.session, data=TimestampedNotificationData(code=e.data.code, reason=e.data.reason))
return
if notification.name == 'SIPInvitationTransferDidStart':
self.state = 'started'
notification_center.post_notification('SIPSessionTransferDidStart', self.session, TimestampedNotificationData())
elif notification.name == 'SIPInvitationTransferDidEnd':
self.state = 'ended'
self.session.end()
notification_center.post_notification('SIPSessionTransferDidEnd', sender=self.session, data=TimestampedNotificationData())
return
def _terminate(self):
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=self.session._invitation)
notification_center.remove_observer(self, sender=self.session)
self._proc.kill()
self._proc = None
self._command_channel = None
self._data_channel = None
self.session = None
@run_in_twisted_thread
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, Null)
handler(notification)
def _NH_SIPInvitationTransferNewIncoming(self, notification):
self._command_channel.send(Command('incoming_transfer', data=notification.data))
def _NH_SIPInvitationTransferNewOutgoing(self, notification):
self._command_channel.send(Command('outgoing_transfer', data=notification.data))
def _NH_SIPInvitationTransferDidStart(self, notification):
self._data_channel.send(notification)
def _NH_SIPInvitationTransferDidFail(self, notification):
self._data_channel.send_exception(SIPInvitationTransferDidFail(notification.data))
def _NH_SIPInvitationTransferDidEnd(self, notification):
self._data_channel.send(notification)
def _NH_SIPInvitationTransferGotNotify(self, notification):
if notification.data.event == 'refer' and notification.data.body:
match = sipfrag_re.match(notification.data.body)
if match:
code = int(match.group('code'))
reason = match.group('reason')
notification_center = NotificationCenter()
notification_center.post_notification('SIPSessionTransferGotProgress', sender=self.session, data=TimestampedNotificationData(code=code, reason=reason))
def _NH_SIPSessionTransferDidStart(self, notification):
if notification.sender is self.session and self.state == 'starting':
self._data_channel.send(notification)
def _NH_SIPSessionTransferDidFail(self, notification):
if notification.sender is self.session and self.state == 'starting':
self._data_channel.send(notification)
def _NH_SIPSessionGotRingIndication(self, notification):
if notification.sender is self.new_session and self.session is not None:
try:
self.session._invitation.notify_transfer_progress(180)
except SIPCoreError:
pass
def _NH_SIPSessionGotProvisionalResponse(self, notification):
if notification.sender is self.new_session and self.session is not None:
try:
self.session._invitation.notify_transfer_progress(notification.data.code, notification.data.reason)
except SIPCoreError:
pass
def _NH_SIPSessionDidStart(self, notification):
if notification.sender is self.new_session:
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=notification.sender)
self.new_session = None
if self.session is not None:
notification_center.post_notification('SIPSessionTransferDidEnd', sender=self.session, data=TimestampedNotificationData())
if self.state == 'started':
try:
self.session._invitation.notify_transfer_progress(200)
except SIPCoreError:
pass
self.state = 'ended'
self.session.end()
def _NH_SIPSessionDidEnd(self, notification):
if notification.sender is self.new_session:
# If any stream fails to start we won't get SIPSessionDidFail, we'll get here instead
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=notification.sender)
self.new_session = None
if self.session is not None:
notification_center.post_notification('SIPSessionTransferDidFail', sender=self.session, data=TimestampedNotificationData(code=500, reason='internal error'))
if self.state == 'started':
try:
self.session._invitation.notify_transfer_progress(500)
except SIPCoreError:
pass
self.state = 'failed'
else:
self._terminate()
def _NH_SIPSessionDidFail(self, notification):
if notification.sender is self.new_session:
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=notification.sender)
self.new_session = None
if self.session is not None:
notification_center.post_notification('SIPSessionTransferDidFail', sender=self.session, data=TimestampedNotificationData(code=notification.data.code or 500, reason=notification.data.reason))
if self.state == 'started':
try:
self.session._invitation.notify_transfer_progress(notification.data.code or 500, notification.data.reason)
except SIPCoreError:
pass
self.state = 'failed'
else:
self._terminate()
class SessionReplaceHandler(object):
implements(IObserver)
def __init__(self, session):
self.session = session
def start(self):
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=self.session)
notification_center.add_observer(self, sender=self.session.replaced_session)
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, Null)
handler(notification)
def _NH_SIPSessionDidStart(self, notification):
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=self.session)
notification_center.remove_observer(self, sender=self.session.replaced_session)
self.session.replaced_session.end()
self.session.replaced_session = None
self.session = None
def _NH_SIPSessionDidFail(self, notification):
if notification.sender is self.session:
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=self.session)
notification_center.remove_observer(self, sender=self.session.replaced_session)
self.session.replaced_session = None
self.session = None
_NH_SIPSessionDidEnd = _NH_SIPSessionDidFail
class Session(object):
implements(IObserver)
media_stream_timeout = 15
def __init__(self, account):
self.account = account
self.direction = None
self.end_time = None
self.on_hold = False
self.proposed_streams = None
self.route = None
self.state = None
self.start_time = None
self.streams = None
self.transport = None
self.local_focus = False
self.remote_focus = False
self.greenlet = None
self.conference = None
self.replaced_session = None
self.transfer_handler = None
self.transfer_info = None
self._channel = queue()
self._hold_in_progress = False
self._invitation = None
self._local_identity = None
self._remote_identity = None
self._lock = RLock()
self.__dict__['subject'] = None
def init_incoming(self, invitation, data):
notification_center = NotificationCenter()
remote_sdp = invitation.sdp.proposed_remote
self.proposed_streams = []
if remote_sdp:
for index, media_stream in enumerate(remote_sdp.media):
if media_stream.port != 0:
for stream_type in MediaStreamRegistry():
try:
stream = stream_type.new_from_sdp(self.account, remote_sdp, index)
except InvalidStreamError:
break
except UnknownStreamError:
continue
else:
stream.index = index
self.proposed_streams.append(stream)
break
if self.proposed_streams:
self.direction = 'incoming'
self.state = 'incoming'
self.transport = invitation.transport
self._invitation = invitation
self.conference = ConferenceHandler(self)
self.transfer_handler = TransferHandler(self)
if 'isfocus' in invitation.remote_contact_header.parameters:
self.remote_focus = True
try:
self.__dict__['subject'] = data.headers['Subject'].subject
except KeyError:
pass
if 'Referred-By' in data.headers or 'Replaces' in data.headers:
self.transfer_info = TransferInfo()
if 'Referred-By' in data.headers:
self.transfer_info.referred_by = data.headers['Referred-By'].body
if 'Replaces' in data.headers:
replaces_header = data.headers.get('Replaces')
replaced_dialog_id = DialogID(replaces_header.call_id, local_tag=replaces_header.to_tag, remote_tag=replaces_header.from_tag)
session_manager = SessionManager()
try:
self.replaced_session = (session for session in session_manager.sessions if session._invitation is not None and session._invitation.dialog_id == replaced_dialog_id).next()
except StopIteration:
invitation.send_response(481)
return
else:
self.transfer_info.replaced_dialog_id = replaced_dialog_id
replace_handler = SessionReplaceHandler(self)
replace_handler.start()
notification_center.add_observer(self, sender=invitation)
notification_center.post_notification('SIPSessionNewIncoming', self, TimestampedNotificationData(streams=self.proposed_streams))
else:
invitation.send_response(488)
@transition_state(None, 'connecting')
@run_in_green_thread
def connect(self, to_header, routes, streams, is_focus=False, subject=None, transfer_info=None):
self.greenlet = api.getcurrent()
notification_center = NotificationCenter()
settings = SIPSimpleSettings()
connected = False
received_code = 0
received_reason = None
unhandled_notifications = []
self.direction = 'outgoing'
self.proposed_streams = streams
self.route = routes[0]
self.transport = self.route.transport
self.local_focus = is_focus
self._invitation = Invitation()
self._local_identity = FromHeader(self.account.uri, self.account.display_name)
self._remote_identity = to_header
self.conference = ConferenceHandler(self)
self.transfer_handler = TransferHandler(self)
self.__dict__['subject'] = subject
self.transfer_info = transfer_info
notification_center.add_observer(self, sender=self._invitation)
notification_center.post_notification('SIPSessionNewOutgoing', self, TimestampedNotificationData(streams=streams))
for stream in self.proposed_streams:
notification_center.add_observer(self, sender=stream)
stream.initialize(self, direction='outgoing')
try:
wait_count = len(self.proposed_streams)
while wait_count > 0:
notification = self._channel.wait()
if notification.name == 'MediaStreamDidInitialize':
wait_count -= 1
try:
contact_uri = self.account.contact[self.route]
except KeyError, e:
for stream in self.proposed_streams:
notification_center.remove_observer(self, sender=stream)
stream.deactivate()
stream.end()
self._fail(originator='local', code=480, reason=sip_status_messages[480], error=str(e))
return
local_ip = contact_uri.host
local_sdp = SDPSession(local_ip, connection=SDPConnection(local_ip), name=settings.user_agent)
stun_addresses = []
for index, stream in enumerate(self.proposed_streams):
stream.index = index
media = stream.get_local_media(for_offer=True)
local_sdp.media.append(media)
stun_addresses.extend((value.split(' ', 5)[4] for value in media.attributes.getall('candidate') if value.startswith('S ')))
if stun_addresses:
local_sdp.connection.address = stun_addresses[0]
from_header = FromHeader(self.account.uri, self.account.display_name)
route_header = RouteHeader(self.route.get_uri())
contact_header = ContactHeader(contact_uri)
if is_focus:
contact_header.parameters['isfocus'] = None
extra_headers = []
if self.subject is not None:
extra_headers.append(SubjectHeader(self.subject))
if self.transfer_info is not None:
extra_headers.append(Header('Referred-By', self.transfer_info.referred_by))
if self.transfer_info.replaced_dialog_id is not None:
dialog_id = self.transfer_info.replaced_dialog_id
extra_headers.append(ReplacesHeader(dialog_id.call_id, dialog_id.local_tag, dialog_id.remote_tag))
self._invitation.send_invite(to_header.uri, from_header, to_header, route_header, contact_header, local_sdp, self.account.credentials, extra_headers)
try:
with api.timeout(settings.sip.invite_timeout):
while True:
notification = self._channel.wait()
if notification.name == 'SIPInvitationGotSDPUpdate':
if notification.data.succeeded:
local_sdp = notification.data.local_sdp
remote_sdp = notification.data.remote_sdp
break
else:
for stream in self.proposed_streams:
notification_center.remove_observer(self, sender=stream)
stream.deactivate()
stream.end()
self._fail(originator='remote', code=0, reason=None, error='SDP negotiation failed: %s' % notification.data.error)
return
elif notification.name == 'SIPInvitationChangedState':
if notification.data.state == 'early':
if notification.data.code == 180:
notification_center.post_notification('SIPSessionGotRingIndication', self, TimestampedNotificationData())
notification_center.post_notification('SIPSessionGotProvisionalResponse', self, TimestampedNotificationData(code=notification.data.code, reason=notification.data.reason))
elif notification.data.state == 'connecting':
received_code = notification.data.code
received_reason = notification.data.reason
elif notification.data.state == 'connected':
if not connected:
connected = True
notification_center.post_notification('SIPSessionDidProcessTransaction', self,
TimestampedNotificationData(originator='local', method='INVITE', code=received_code, reason=received_reason))
else:
unhandled_notifications.append(notification)
elif notification.data.state == 'disconnected':
raise InvitationDisconnectedError(notification.sender, notification.data)
except api.TimeoutError:
self.greenlet = None
self.end()
return
notification_center.post_notification('SIPSessionWillStart', self, TimestampedNotificationData())
stream_map = dict((stream.index, stream) for stream in self.proposed_streams)
for index, local_media in enumerate(local_sdp.media):
remote_media = remote_sdp.media[index]
stream = stream_map[index]
if remote_media.port:
stream.start(local_sdp, remote_sdp, index)
else:
notification_center.remove_observer(self, sender=stream)
self.proposed_streams.remove(stream)
del stream_map[stream.index]
stream.deactivate()
stream.end()
removed_streams = [stream for stream in self.proposed_streams if stream.index >= len(local_sdp.media)]
for stream in removed_streams:
notification_center.remove_observer(self, sender=stream)
self.proposed_streams.remove(stream)
del stream_map[stream.index]
stream.deactivate()
stream.end()
invitation_notifications = []
with api.timeout(self.media_stream_timeout):
wait_count = len(self.proposed_streams)
while wait_count > 0:
notification = self._channel.wait()
if notification.name == 'MediaStreamDidStart':
wait_count -= 1
elif notification.name == 'SIPInvitationChangedState':
invitation_notifications.append(notification)
[self._channel.send(notification) for notification in invitation_notifications]
while not connected or self._channel:
notification = self._channel.wait()
if notification.name == 'SIPInvitationChangedState':
if notification.data.state == 'early':
if notification.data.code == 180:
notification_center.post_notification('SIPSessionGotRingIndication', self, TimestampedNotificationData())
notification_center.post_notification('SIPSessionGotProvisionalResponse', self, TimestampedNotificationData(code=notification.data.code, reason=notification.data.reason))
elif notification.data.state == 'connecting':
received_code = notification.data.code
received_reason = notification.data.reason
elif notification.data.state == 'connected':
if not connected:
connected = True
notification_center.post_notification('SIPSessionDidProcessTransaction', self,
TimestampedNotificationData(originator='local', method='INVITE', code=received_code, reason=received_reason))
else:
unhandled_notifications.append(notification)
elif notification.data.state == 'disconnected':
raise InvitationDisconnectedError(notification.sender, notification.data)
except (MediaStreamDidFailError, api.TimeoutError), e:
for stream in self.proposed_streams:
notification_center.remove_observer(self, sender=stream)
stream.deactivate()
stream.end()
if isinstance(e, api.TimeoutError):
error = 'media stream timed out while starting'
else:
error = 'media stream failed: %s' % e.data.reason
self._fail(originator='local', code=0, reason=None, error=error)
except InvitationDisconnectedError, e:
notification_center.remove_observer(self, sender=self._invitation)
for stream in self.proposed_streams:
notification_center.remove_observer(self, sender=stream)
stream.deactivate()
stream.end()
self.state = 'terminated'
# As it weird as it may sound, PJSIP accepts a BYE even without receiving a final response to the INVITE
if e.data.prev_state in ('connecting', 'connected') or getattr(e.data, 'method', None) == 'BYE':
notification_center.post_notification('SIPSessionWillEnd', self, TimestampedNotificationData(originator=e.data.originator))
if e.data.originator == 'remote':
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='remote', method=e.data.method, code=200, reason=sip_status_messages[200]))
self.end_time = datetime.now()
notification_center.post_notification('SIPSessionDidEnd', self, TimestampedNotificationData(originator=e.data.originator, end_reason=e.data.disconnect_reason))
else:
if e.data.originator == 'remote':
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='local', method='INVITE', code=e.data.code, reason=e.data.reason))
if e.data.originator == 'remote':
code = e.data.code
reason = e.data.reason
elif e.data.disconnect_reason == 'timeout':
code = 408
reason = 'timeout'
else:
code = 0
reason = None
if e.data.originator == 'remote' and code // 100 == 3:
redirect_identities = e.data.headers.get('Contact', [])
else:
redirect_identities = None
notification_center.post_notification('SIPSessionDidFail', self, TimestampedNotificationData(originator=e.data.originator, code=code, reason=reason, failure_reason=e.data.disconnect_reason, redirect_identities=redirect_identities))
self.greenlet = None
except SIPCoreError, e:
for stream in self.proposed_streams:
notification_center.remove_observer(self, sender=stream)
stream.deactivate()
stream.end()
self._fail(originator='local', code=0, reason=None, error='SIP core error: %s' % str(e))
else:
self.greenlet = None
self.state = 'connected'
self.streams = self.proposed_streams
self.proposed_streams = None
self.start_time = datetime.now()
notification_center.post_notification('SIPSessionDidStart', self, TimestampedNotificationData(streams=self.streams))
for notification in unhandled_notifications:
self.handle_notification(notification)
if self._hold_in_progress:
self._send_hold()
@check_state(['incoming', 'received_proposal'])
@run_in_green_thread
def send_ring_indication(self):
try:
self._invitation.send_response(180)
except SIPCoreInvalidStateError:
pass # The INVITE session might have already been cancelled; ignore the error
@transition_state('incoming', 'accepting')
@run_in_green_thread
def accept(self, streams, is_focus=False):
self.greenlet = api.getcurrent()
notification_center = NotificationCenter()
settings = SIPSimpleSettings()
self.local_focus = is_focus
connected = False
unhandled_notifications = []
if self.proposed_streams:
for stream in self.proposed_streams:
if stream in streams:
notification_center.add_observer(self, sender=stream)
stream.initialize(self, direction='incoming')
else:
for index, stream in enumerate(streams):
notification_center.add_observer(self, sender=stream)
stream.index = index
stream.initialize(self, direction='outgoing')
self.proposed_streams = streams
try:
wait_count = len(self.proposed_streams)
while wait_count > 0:
notification = self._channel.wait()
if notification.name == 'MediaStreamDidInitialize':
wait_count -= 1
sdp_connection = self._invitation.sdp.proposed_remote.connection or (media.connection for media in self._invitation.sdp.proposed_remote.media if media.connection is not None).next()
local_ip = host.outgoing_ip_for(sdp_connection.address)
if local_ip is None:
for stream in self.proposed_streams:
notification_center.remove_observer(self, sender=stream)
stream.deactivate()
stream.end()
self._fail(originator='local', code=500, reason=sip_status_messages[500], error='could not get local IP address')
return
local_sdp = SDPSession(local_ip, connection=SDPConnection(local_ip), name=settings.user_agent)
stun_addresses = []
if self._invitation.sdp.proposed_remote:
stream_map = dict((stream.index, stream) for stream in self.proposed_streams)
for index, media in enumerate(self._invitation.sdp.proposed_remote.media):
stream = stream_map.get(index, None)
if stream is not None:
media = stream.get_local_media(for_offer=False)
local_sdp.media.append(media)
stun_addresses.extend((value.split(' ', 5)[4] for value in media.attributes.getall('candidate') if value.startswith('S ')))
else:
media = SDPMediaStream.new(media)
media.port = 0
media.attributes = []
local_sdp.media.append(media)
else:
for stream in self.proposed_streams:
media = stream.get_local_media(for_offer=True)
local_sdp.media.append(media)
stun_addresses.extend((value.split(' ', 5)[4] for value in media.attributes.getall('candidate') if value.startswith('S ')))
if stun_addresses:
local_sdp.connection.address = stun_addresses[0]
if is_focus:
contact_header = ContactHeader.new(self._invitation.local_contact_header)
contact_header.parameters['isfocus'] = None
self._invitation.send_response(200, contact_header=contact_header, sdp=local_sdp)
else:
self._invitation.send_response(200, sdp=local_sdp)
notification_center.post_notification('SIPSessionWillStart', self, TimestampedNotificationData())
# Local and remote SDPs will be set after the 200 OK is sent
while True:
notification = self._channel.wait()
if notification.name == 'SIPInvitationGotSDPUpdate':
if notification.data.succeeded:
local_sdp = notification.data.local_sdp
remote_sdp = notification.data.remote_sdp
break
else:
if not connected:
# we could not have got a SIPInvitationGotSDPUpdate if we did not get an ACK
connected = True
notification_center.post_notification('SIPSessionDidProcessTransaction', self,
TimestampedNotificationData(originator='remote', method='INVITE', code=200, reason=sip_status_messages[200], ack_received=True))
for stream in self.proposed_streams:
notification_center.remove_observer(self, sender=stream)
stream.deactivate()
stream.end()
self._fail(originator='remote', code=0, reason=None, error='SDP negotiation failed: %s' % notification.data.error)
return
elif notification.name == 'SIPInvitationChangedState':
if notification.data.state == 'connected':
if not connected:
connected = True
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='remote', method='INVITE', code=200, reason=sip_status_messages[200], ack_received=True))
elif notification.data.prev_state == 'connected':
unhandled_notifications.append(notification)
elif notification.data.state == 'disconnected':
raise InvitationDisconnectedError(notification.sender, notification.data)
wait_count = 0
stream_map = dict((stream.index, stream) for stream in self.proposed_streams)
for index, local_media in enumerate(local_sdp.media):
remote_media = remote_sdp.media[index]
stream = stream_map.get(index, None)
if stream is not None:
if remote_media.port:
wait_count += 1
stream.start(local_sdp, remote_sdp, index)
else:
notification_center.remove_observer(self, sender=stream)
self.proposed_streams.remove(stream)
del stream_map[stream.index]
stream.deactivate()
stream.end()
removed_streams = [stream for stream in self.proposed_streams if stream.index >= len(local_sdp.media)]
for stream in removed_streams:
notification_center.remove_observer(self, sender=stream)
self.proposed_streams.remove(stream)
del stream_map[stream.index]
stream.deactivate()
stream.end()
with api.timeout(self.media_stream_timeout):
while wait_count > 0 or not connected or self._channel:
notification = self._channel.wait()
if notification.name == 'MediaStreamDidStart':
wait_count -= 1
elif notification.name == 'SIPInvitationChangedState':
if notification.data.state == 'connected':
if not connected:
connected = True
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='remote', method='INVITE', code=200, reason='OK', ack_received=True))
elif notification.data.prev_state == 'connected':
unhandled_notifications.append(notification)
elif notification.data.state == 'disconnected':
raise InvitationDisconnectedError(notification.sender, notification.data)
else:
unhandled_notifications.append(notification)
except (MediaStreamDidFailError, api.TimeoutError), e:
if self._invitation.state == 'connecting':
ack_received = False if isinstance(e, api.TimeoutError) and wait_count == 0 else 'unknown'
# pjsip's invite session object does not inform us whether the ACK was received or not
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='remote', method='INVITE', code=200, reason='OK', ack_received=ack_received))
elif self._invitation.state == 'connected' and not connected:
# we didn't yet get to process the SIPInvitationChangedState (state -> connected) notification
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='remote', method='INVITE', code=200, reason='OK', ack_received=True))
for stream in self.proposed_streams:
notification_center.remove_observer(self, sender=stream)
stream.deactivate()
stream.end()
reason_header = None
if isinstance(e, api.TimeoutError) and wait_count > 0:
error = 'media stream timed out while starting'
elif isinstance(e, api.TimeoutError) and wait_count == 0:
error = 'No ACK received'
reason_header = ReasonHeader('SIP')
reason_header.cause = 500
reason_header.text = 'Missing ACK'
else:
error = 'media stream failed: %s' % e.data.reason
reason_header = ReasonHeader('SIP')
reason_header.cause = 500
reason_header.text = 'media stream failed to start'
self.start_time = datetime.now()
if self._invitation.state in ('incoming', 'early'):
self._fail(originator='local', code=500, reason=sip_status_messages[500], error=error, reason_header=reason_header)
else:
self._fail(originator='local', code=0, reason=None, error=error, reason_header=reason_header)
except InvitationDisconnectedError, e:
notification_center.remove_observer(self, sender=self._invitation)
for stream in self.proposed_streams:
notification_center.remove_observer(self, sender=stream)
stream.deactivate()
stream.end()
self.state = 'terminated'
if e.data.prev_state in ('incoming', 'early'):
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='remote', method='INVITE', code=487, reason='Session Cancelled', ack_received='unknown'))
notification_center.post_notification('SIPSessionDidFail', self, TimestampedNotificationData(originator='remote', code=487, reason='Session Cancelled', failure_reason=e.data.disconnect_reason, redirect_identities=None))
elif e.data.prev_state == 'connecting' and e.data.disconnect_reason == 'missing ACK':
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='remote', method='INVITE', code=200, reason='OK', ack_received=False))
notification_center.post_notification('SIPSessionDidFail', self, TimestampedNotificationData(originator='local', code=200, reason=sip_status_messages[200], failure_reason=e.data.disconnect_reason, redirect_identities=None))
else:
notification_center.post_notification('SIPSessionWillEnd', self, TimestampedNotificationData(originator='remote'))
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='remote', method=e.data.method, code=200, reason='OK'))
self.end_time = datetime.now()
notification_center.post_notification('SIPSessionDidEnd', self, TimestampedNotificationData(originator='remote', end_reason=e.data.disconnect_reason))
self.greenlet = None
except SIPCoreInvalidStateError:
# the only reason for which this error can be thrown is if invitation.send_response was called after the INVITE session was cancelled by the remote party
notification_center.remove_observer(self, sender=self._invitation)
for stream in self.proposed_streams:
notification_center.remove_observer(self, sender=stream)
stream.deactivate()
stream.end()
self.greenlet = None
self.state = 'terminated'
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='remote', method='INVITE', code=487, reason='Session Cancelled', ack_received='unknown'))
notification_center.post_notification('SIPSessionDidFail', self, TimestampedNotificationData(originator='remote', code=487, reason='Session Cancelled', failure_reason='user request', redirect_identities=None))
except SIPCoreError, e:
for stream in self.proposed_streams:
notification_center.remove_observer(self, sender=stream)
stream.deactivate()
stream.end()
self._fail(originator='local', code=500, reason=sip_status_messages[500], error='SIP core error: %s' % str(e))
else:
self.greenlet = None
self.state = 'connected'
self.streams = self.proposed_streams
self.proposed_streams = None
self.start_time = datetime.now()
notification_center.post_notification('SIPSessionDidStart', self, TimestampedNotificationData(streams=self.streams))
for notification in unhandled_notifications:
self.handle_notification(notification)
if self._hold_in_progress:
self._send_hold()
@transition_state('incoming', 'terminating')
@run_in_green_thread
def reject(self, code=603, reason=None):
self.greenlet = api.getcurrent()
notification_center = NotificationCenter()
try:
self._invitation.send_response(code, reason)
with api.timeout(1):
while True:
notification = self._channel.wait()
if notification.name == 'SIPInvitationChangedState':
if notification.data.state == 'disconnected':
ack_received = notification.data.disconnect_reason != 'missing ACK'
notification_center.post_notification('SIPSessionDidProcessTransaction', self,
TimestampedNotificationData(originator='remote', method='INVITE', code=code, reason=sip_status_messages[code], ack_received=ack_received))
break
except SIPCoreInvalidStateError:
# the only reason for which this error can be thrown is if invitation.send_response was called after the INVITE session was cancelled by the remote party
notification_center.remove_observer(self, sender=self._invitation)
self.greenlet = None
self.state = 'terminated'
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='remote', method='INVITE', code=487, reason='Session Cancelled', ack_received='unknown'))
notification_center.post_notification('SIPSessionDidFail', self, TimestampedNotificationData(originator='remote', code=487, reason='Session Cancelled', failure_reason='user request', redirect_identities=None))
except SIPCoreError, e:
self._fail(originator='local', code=500, reason=sip_status_messages[500], error='SIP core error: %s' % str(e))
except api.TimeoutError:
notification_center.remove_observer(self, sender=self._invitation)
self.greenlet = None
self.state = 'terminated'
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='remote', method='INVITE', code=code, reason=sip_status_messages[code], ack_received=False))
notification_center.post_notification('SIPSessionDidFail', self, TimestampedNotificationData(originator='local', code=code, reason=sip_status_messages[code], failure_reason='timeout', redirect_identities=None))
else:
notification_center.remove_observer(self, sender=self._invitation)
self.greenlet = None
self.state = 'terminated'
self.proposed_streams = None
notification_center.post_notification('SIPSessionDidFail', self, TimestampedNotificationData(originator='local', code=code, reason=sip_status_messages[code], failure_reason='user request', redirect_identities=None))
@transition_state('received_proposal', 'accepting_proposal')
@run_in_green_thread
def accept_proposal(self, streams):
self.greenlet = api.getcurrent()
notification_center = NotificationCenter()
unhandled_notifications = []
streams = [stream for stream in streams if stream in self.proposed_streams]
for stream in self.proposed_streams:
if stream in streams:
notification_center.add_observer(self, sender=stream)
stream.initialize(self, direction='incoming')
try:
wait_count = len(streams)
while wait_count > 0:
notification = self._channel.wait()
if notification.name == 'MediaStreamDidInitialize':
wait_count -= 1
local_sdp = SDPSession.new(self._invitation.sdp.active_local)
local_sdp.version += 1
stream_map = dict((stream.index, stream) for stream in streams)
for index, media in enumerate(self._invitation.sdp.proposed_remote.media):
stream = stream_map.get(index, None)
if stream is not None:
if index < len(local_sdp.media):
local_sdp.media[index] = stream.get_local_media(for_offer=False)
else:
local_sdp.media.append(stream.get_local_media(for_offer=False))
elif index >= len(local_sdp.media): # actually == is sufficient
media = SDPMediaStream.new(media)
media.port = 0
media.attributes = []
local_sdp.media.append(media)
self._invitation.send_response(200, sdp=local_sdp)
prev_on_hold_streams = set(stream for stream in self.streams if stream.hold_supported and stream.on_hold_by_remote)
received_invitation_state = False
received_sdp_update = False
while not received_invitation_state or not received_sdp_update:
notification = self._channel.wait()
if notification.name == 'SIPInvitationGotSDPUpdate':
received_sdp_update = True
if notification.data.succeeded:
local_sdp = notification.data.local_sdp
remote_sdp = notification.data.remote_sdp
for stream in self.streams:
stream.update(local_sdp, remote_sdp, stream.index)
else:
self._fail_proposal(originator='remote', error='SDP negotiation failed: %s' % notification.data.error)
return
elif notification.name == 'SIPInvitationChangedState':
if notification.data.state == 'connected' and notification.data.sub_state == 'normal':
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='remote', method='INVITE', code=200, reason=sip_status_messages[200], ack_received='unknown'))
received_invitation_state = True
elif notification.data.state == 'disconnected':
raise InvitationDisconnectedError(notification.sender, notification.data)
on_hold_streams = set(stream for stream in self.streams if stream.hold_supported and stream.on_hold_by_remote)
if on_hold_streams != prev_on_hold_streams:
hold_supported_streams = (stream for stream in self.streams if stream.hold_supported)
notification_center.post_notification('SIPSessionDidChangeHoldState', self, TimestampedNotificationData(originator='remote', on_hold=bool(on_hold_streams),
partial=bool(on_hold_streams) and any(not stream.on_hold_by_remote for stream in hold_supported_streams)))
for stream in streams:
stream.start(local_sdp, remote_sdp, stream.index)
with api.timeout(self.media_stream_timeout):
wait_count = len(streams)
while wait_count > 0 or self._channel:
notification = self._channel.wait()
if notification.name == 'MediaStreamDidStart':
wait_count -= 1
else:
unhandled_notifications.append(notification)
except (MediaStreamDidFailError, api.TimeoutError), e:
if isinstance(e, api.TimeoutError):
error = 'media stream timed out while starting'
else:
error = 'media stream failed: %s' % e.data.reason
self._fail_proposal(originator='remote', error=error)
except InvitationDisconnectedError, e:
self._fail_proposal(originator='remote', error='session ended')
self.handle_notification(Notification('SIPInvitationChangedState', e.invitation, e.data))
except SIPCoreError, e:
self._fail_proposal(originator='remote', error='SIP core error: %s' % str(e))
else:
self.greenlet = None
self.state = 'connected'
notification_center.post_notification('SIPSessionGotAcceptProposal', self, TimestampedNotificationData(originator='remote', streams=streams, proposed_streams=self.proposed_streams))
self.streams = self.streams + streams
self.proposed_streams = None
notification_center.post_notification('SIPSessionDidRenegotiateStreams', self, TimestampedNotificationData(originator='remote', action='add', streams=streams))
for notification in unhandled_notifications:
self.handle_notification(notification)
if self._hold_in_progress:
self._send_hold()
@transition_state('received_proposal', 'rejecting_proposal')
@run_in_green_thread
def reject_proposal(self, code=488, reason=None):
self.greenlet = api.getcurrent()
notification_center = NotificationCenter()
try:
self._invitation.send_response(code, reason)
with api.timeout(1, None):
while True:
notification = self._channel.wait()
if notification.name == 'SIPInvitationChangedState':
if notification.data.state == 'connected' and notification.data.sub_state == 'normal':
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='remote', method='INVITE', code=code, reason=sip_status_messages[code], ack_received='unknown'))
except SIPCoreError, e:
self._fail_proposal(originator='remote', error='SIP core error: %s' % str(e))
else:
self.greenlet = None
self.state = 'connected'
notification_center.post_notification('SIPSessionGotRejectProposal', self, TimestampedNotificationData(originator='remote', code=code, reason=sip_status_messages[code], streams=self.proposed_streams))
self.proposed_streams = None
if self._hold_in_progress:
self._send_hold()
@transition_state('connected', 'sending_proposal')
@run_in_green_thread
def add_stream(self, stream):
self.greenlet = api.getcurrent()
notification_center = NotificationCenter()
settings = SIPSimpleSettings()
received_code = None
received_reason = None
unhandled_notifications = []
self.proposed_streams = [stream]
notification_center.add_observer(self, sender=stream)
stream.initialize(self, direction='outgoing')
try:
while True:
notification = self._channel.wait()
if notification.name == 'MediaStreamDidInitialize':
break
elif notification.name == 'SIPInvitationChangedState':
# This is actually the only reason for which this notification could be received
if notification.data.state == 'connected' and notification.data.sub_state == 'received_proposal':
self._fail_proposal(originator='local', error='received stream proposal')
self.handle_notification(notification)
return
local_sdp = SDPSession.new(self._invitation.sdp.active_local)
local_sdp.version += 1
stream.index = len(local_sdp.media)
local_sdp.media.append(stream.get_local_media(for_offer=True))
self._invitation.send_reinvite(sdp=local_sdp)
notification_center.post_notification('SIPSessionGotProposal', self, TimestampedNotificationData(originator='local', streams=self.proposed_streams))
received_invitation_state = False
received_sdp_update = False
try:
with api.timeout(settings.sip.invite_timeout):
while not received_invitation_state or not received_sdp_update:
notification = self._channel.wait()
if notification.name == 'SIPInvitationGotSDPUpdate':
received_sdp_update = True
if notification.data.succeeded:
local_sdp = notification.data.local_sdp
remote_sdp = notification.data.remote_sdp
for s in self.streams:
s.update(local_sdp, remote_sdp, s.index)
else:
self._fail_proposal(originator='local', error='SDP negotiation failed: %s' % notification.data.error)
return
elif notification.name == 'SIPInvitationChangedState':
if notification.data.state == 'connected' and notification.data.sub_state == 'normal':
received_invitation_state = True
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='local', method='INVITE', code=notification.data.code, reason=notification.data.reason))
if 200 <= notification.data.code < 300:
received_code = notification.data.code
received_reason = notification.data.reason
else:
notification_center.remove_observer(self, sender=stream)
stream.deactivate()
stream.end()
notification_center.post_notification('SIPSessionGotRejectProposal', self, TimestampedNotificationData(originator='local', code=notification.data.code, reason=notification.data.reason, streams=self.proposed_streams))
self.state = 'connected'
self.proposed_streams = None
self.greenlet = None
return
elif notification.data.state == 'disconnected':
raise InvitationDisconnectedError(notification.sender, notification.data)
except api.TimeoutError:
self.greenlet = None
self.cancel_proposal()
return
try:
remote_media = remote_sdp.media[stream.index]
except IndexError:
self._fail_proposal(originator='local', error='SDP media missing in answer')
return
else:
if remote_media.port:
stream.start(local_sdp, remote_sdp, stream.index)
else:
notification_center.remove_observer(self, sender=stream)
stream.deactivate()
stream.end()
notification_center.post_notification('SIPSessionGotRejectProposal', self, TimestampedNotificationData(originator='local', code=received_code, reason=received_reason, streams=self.proposed_streams))
self.state = 'connected'
self.proposed_streams = None
self.greenlet = None
return
with api.timeout(self.media_stream_timeout):
wait_count = 1
while wait_count > 0 or self._channel:
notification = self._channel.wait()
if notification.name == 'MediaStreamDidStart':
wait_count -= 1
except (MediaStreamDidFailError, api.TimeoutError), e:
if isinstance(e, api.TimeoutError):
error = 'media stream timed out while starting'
else:
error = 'media stream failed: %s' % e.data.reason
self._fail_proposal(originator='local', error=error)
except InvitationDisconnectedError, e:
self._fail_proposal(originator='local', error='session ended')
self.handle_notification(Notification('SIPInvitationChangedState', e.invitation, e.data))
except SIPCoreError, e:
self._fail_proposal(originator='local', error='SIP core error: %s' % str(e))
else:
self.greenlet = None
self.state = 'connected'
notification_center.post_notification('SIPSessionGotAcceptProposal', self, TimestampedNotificationData(originator='local', streams=self.proposed_streams, proposed_streams=self.proposed_streams))
self.streams = self.streams + self.proposed_streams
proposed_streams = self.proposed_streams
self.proposed_streams = None
notification_center.post_notification('SIPSessionDidRenegotiateStreams', self, TimestampedNotificationData(originator='local', action='add', streams=proposed_streams))
for notification in unhandled_notifications:
self.handle_notification(notification)
if self._hold_in_progress:
self._send_hold()
@transition_state('connected', 'sending_proposal')
@run_in_green_thread
def remove_stream(self, stream):
if stream not in self.streams:
self.state = 'connected'
return
self.greenlet = api.getcurrent()
notification_center = NotificationCenter()
unhandled_notifications = []
try:
notification_center.remove_observer(self, sender=stream)
stream.deactivate()
self.streams.remove(stream)
local_sdp = SDPSession.new(self._invitation.sdp.active_local)
local_sdp.version += 1
local_sdp.media[stream.index].port = 0
local_sdp.media[stream.index].attributes = []
self._invitation.send_reinvite(sdp=local_sdp)
received_invitation_state = False
received_sdp_update = False
while not received_invitation_state or not received_sdp_update:
notification = self._channel.wait()
if notification.name == 'SIPInvitationGotSDPUpdate':
received_sdp_update = True
elif notification.name == 'SIPInvitationChangedState':
if notification.data.state == 'connected' and notification.data.sub_state == 'normal':
received_invitation_state = True
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='local', method='INVITE', code=notification.data.code, reason=notification.data.reason))
elif notification.data.state == 'disconnected':
raise InvitationDisconnectedError(notification.sender, notification.data)
except InvitationDisconnectedError, e:
self.greenlet = None
self.handle_notification(Notification('SIPInvitationChangedState', e.invitation, e.data))
except SIPCoreError:
raise #FIXME
else:
stream.end()
self.greenlet = None
self.state = 'connected'
notification_center.post_notification('SIPSessionDidRenegotiateStreams', self, TimestampedNotificationData(originator='local', action='remove', streams=[stream]))
for notification in unhandled_notifications:
self.handle_notification(notification)
if self._hold_in_progress:
self._send_hold()
@transition_state('sending_proposal', 'cancelling_proposal')
@run_in_green_thread
def cancel_proposal(self):
if self.greenlet is not None:
api.kill(self.greenlet, api.GreenletExit())
self.greenlet = api.getcurrent()
notification_center = NotificationCenter()
try:
self._invitation.cancel_reinvite()
while True:
try:
notification = self._channel.wait()
except MediaStreamDidFailError:
continue
if notification.name == 'SIPInvitationChangedState':
if notification.data.state == 'connected' and notification.data.sub_state == 'normal':
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='remote', method='INVITE', code=notification.data.code, reason=notification.data.reason))
if notification.data.code == 487:
for stream in self.proposed_streams:
stream.deactivate()
stream.end()
notification_center.post_notification('SIPSessionGotRejectProposal', self, TimestampedNotificationData(originator='remote', code=notification.data.code, reason=notification.data.reason, streams=self.proposed_streams))
elif notification.data.code == 200:
self.end()
elif notification.data.state == 'disconnected':
raise InvitationDisconnectedError(notification.sender, notification.data)
break
except SIPCoreError, e:
for stream in self.proposed_streams:
stream.deactivate()
stream.end()
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='local', code=0, reason=None, failure_reason='SIP core error: %s' % str(e), redirect_identities=None))
notification_center.post_notification('SIPSessionGotRejectProposal', self, TimestampedNotificationData(originator='local', code=0, reason='SIP core error: %s' % str(e), streams=self.proposed_streams))
self.proposed_streams = None
self.greenlet = None
self.state = 'connected'
except InvitationDisconnectedError, e:
self.proposed_streams = None
self.greenlet = None
self.handle_notification(Notification('SIPInvitationChangedState', e.invitation, e.data))
else:
self.proposed_streams = None
self.greenlet = None
self.state = 'connected'
finally:
if self._hold_in_progress:
self._send_hold()
@run_in_green_thread
def hold(self):
if self.on_hold or self._hold_in_progress:
return
self._hold_in_progress = True
streams = self.streams if self.streams is not None else self.proposed_streams
if not streams:
return
for stream in streams:
stream.hold()
if self.state == 'connected':
self._send_hold()
@run_in_green_thread
def unhold(self):
if not self.on_hold and not self._hold_in_progress:
return
self._hold_in_progress = False
streams = self.streams if self.streams is not None else self.proposed_streams
if not streams:
return
for stream in streams:
stream.unhold()
if self.state == 'connected':
self._send_unhold()
@run_in_green_thread
def end(self):
if self.state in (None, 'terminating', 'terminated'):
return
if self.greenlet is not None:
api.kill(self.greenlet, api.GreenletExit())
self.greenlet = None
notification_center = NotificationCenter()
if self._invitation is None or self._invitation.state is None:
# The invitation was not yet constructed
notification_center.post_notification('SIPSessionDidFail', self, TimestampedNotificationData(originator='local', code=487, reason='Session Cancelled', failure_reason='user request', redirect_identities=None))
return
invitation_state = self._invitation.state
if invitation_state in ('disconnecting', 'disconnected'):
return
self.greenlet = api.getcurrent()
self.state = 'terminating'
if invitation_state == 'connected':
notification_center.post_notification('SIPSessionWillEnd', self, TimestampedNotificationData(originator='local'))
streams = (self.streams or []) + (self.proposed_streams or [])
for stream in streams[:]:
try:
notification_center.remove_observer(self, sender=stream)
except KeyError:
streams.remove(stream)
else:
stream.deactivate()
cancelling = invitation_state != 'connected' and self.direction == 'outgoing'
try:
self._invitation.end(timeout=1)
while True:
try:
notification = self._channel.wait()
except MediaStreamDidFailError:
continue
if notification.name == 'SIPInvitationChangedState' and notification.data.state == 'disconnected':
if notification.data.disconnect_reason in ('internal error', 'missing ACK'):
pass
elif notification.data.disconnect_reason == 'timeout':
notification_center.post_notification('SIPSessionDidProcessTransaction', self,
TimestampedNotificationData(originator='local' if self.direction=='outgoing' else 'remote', method='INVITE', code=408, reason='Timeout'))
elif cancelling:
notification_center.post_notification('SIPSessionDidProcessTransaction', self,
TimestampedNotificationData(originator='local', method='INVITE', code=notification.data.code, reason=notification.data.reason))
elif hasattr(notification.data, 'method'):
notification_center.post_notification('SIPSessionDidProcessTransaction', self,
TimestampedNotificationData(originator='remote', method=notification.data.method, code=200, reason=sip_status_messages[200]))
elif notification.data.disconnect_reason == 'user request':
notification_center.post_notification('SIPSessionDidProcessTransaction', self,
TimestampedNotificationData(originator='local', method='BYE', code=notification.data.code, reason=notification.data.reason))
break
except SIPCoreError, e:
if cancelling:
notification_center.post_notification('SIPSessionDidFail', self, TimestampedNotificationData(originator='local', code=0, reason=None, failure_reason='SIP core error: %s' % str(e), redirect_identities=None))
else:
self.end_time = datetime.now()
notification_center.post_notification('SIPSessionDidEnd', self, TimestampedNotificationData(originator='local', end_reason='SIP core error: %s' % str(e)))
except InvitationDisconnectedError, e:
# As it weird as it may sound, PJSIP accepts a BYE even without receiving a final response to the INVITE
if e.data.prev_state == 'connected':
if e.data.originator == 'remote':
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator=e.data.originator, method=e.data.method, code=200, reason=sip_status_messages[200]))
self.end_time = datetime.now()
notification_center.post_notification('SIPSessionDidEnd', self, TimestampedNotificationData(originator=e.data.originator, end_reason=e.data.disconnect_reason))
elif getattr(e.data, 'method', None) == 'BYE' and e.data.originator == 'remote':
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator=e.data.originator, method=e.data.method, code=200, reason=sip_status_messages[200]))
notification_center.post_notification('SIPSessionDidFail', self, TimestampedNotificationData(originator=e.data.originator, code=0, reason=None, failure_reason=e.data.disconnect_reason, redirect_identities=None))
else:
if e.data.originator == 'remote':
code = e.data.code
reason = e.data.reason
elif e.data.disconnect_reason == 'timeout':
code = 408
reason = 'timeout'
else:
code = 0
reason = None
if e.data.originator == 'remote' and code // 100 == 3:
redirect_identities = e.data.headers.get('Contact', [])
else:
redirect_identities = None
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='local', method='INVITE', code=code, reason=reason))
notification_center.post_notification('SIPSessionDidFail', self, TimestampedNotificationData(originator=e.data.originator, code=code, reason=reason, failure_reason=e.data.disconnect_reason, redirect_identities=redirect_identities))
else:
if cancelling:
notification_center.post_notification('SIPSessionDidFail', self, TimestampedNotificationData(originator='local', code=487, reason='Session Cancelled', failure_reason='user request', redirect_identities=None))
else:
self.end_time = datetime.now()
notification_center.post_notification('SIPSessionDidEnd', self, TimestampedNotificationData(originator='local', end_reason='user request'))
finally:
for stream in streams:
stream.end()
notification_center.remove_observer(self, sender=self._invitation)
self.greenlet = None
self.state = 'terminated'
@check_state(['connected'])
@run_in_twisted_thread
def transfer(self, target_uri, replaced_session=None):
notification_center = NotificationCenter()
notification_center.post_notification('SIPSessionTransferNewOutgoing', self, TimestampedNotificationData(transfer_destination=target_uri, transfer_source=self.local_identity.uri))
try:
self._invitation.transfer(target_uri, replaced_session._invitation.dialog_id if replaced_session is not None else None)
except SIPCoreError, e:
notification_center.post_notification('SIPSessionTransferDidFail', sender=self, data=TimestampedNotificationData(code=500, reason=str(e)))
@check_state(['connected', 'received_proposal', 'sending_proposal', 'accepting_proposal', 'rejecting_proposal', 'cancelling_proposal'])
@check_transfer_state('incoming', 'starting')
def accept_transfer(self):
notification_center = NotificationCenter()
notification_center.post_notification('SIPSessionTransferDidStart', self, TimestampedNotificationData())
@check_state(['connected', 'received_proposal', 'sending_proposal', 'accepting_proposal', 'rejecting_proposal', 'cancelling_proposal'])
@check_transfer_state('incoming', 'starting')
def reject_transfer(self, code=486, reason=None):
notification_center = NotificationCenter()
notification_center.post_notification('SIPSessionTransferDidFail', self, TimestampedNotificationData(code=code, reason=reason or sip_status_messages[code]))
@property
def local_identity(self):
if self._invitation is not None and self._invitation.local_identity is not None:
return self._invitation.local_identity
else:
return self._local_identity
@property
def peer_address(self):
return self._invitation.peer_address if self._invitation is not None else None
@property
def remote_identity(self):
if self._invitation is not None and self._invitation.remote_identity is not None:
return self._invitation.remote_identity
else:
return self._remote_identity
@property
def remote_user_agent(self):
return self._invitation.remote_user_agent if self._invitation is not None else None
@property
def subject(self):
return self.__dict__['subject']
def _send_hold(self):
self.state = 'sending_proposal'
self.greenlet = api.getcurrent()
notification_center = NotificationCenter()
unhandled_notifications = []
try:
local_sdp = SDPSession.new(self._invitation.sdp.active_local)
local_sdp.version += 1
for stream in self.streams:
local_sdp.media[stream.index] = stream.get_local_media(for_offer=True)
self._invitation.send_reinvite(sdp=local_sdp)
received_invitation_state = False
received_sdp_update = False
while not received_invitation_state or not received_sdp_update:
notification = self._channel.wait()
if notification.name == 'SIPInvitationGotSDPUpdate':
received_sdp_update = True
if notification.data.succeeded:
local_sdp = notification.data.local_sdp
remote_sdp = notification.data.remote_sdp
for stream in self.streams:
stream.update(local_sdp, remote_sdp, stream.index)
elif notification.name == 'SIPInvitationChangedState':
if notification.data.state == 'connected' and notification.data.sub_state == 'normal':
received_invitation_state = True
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='local', method='INVITE', code=notification.data.code, reason=notification.data.reason))
elif notification.data.state == 'disconnected':
raise InvitationDisconnectedError(notification.sender, notification.data)
except InvitationDisconnectedError, e:
self.greenlet = None
self.handle_notification(Notification('SIPInvitationChangedState', e.invitation, e.data))
except SIPCoreError, e:
raise #FIXME
else:
self.greenlet = None
self.on_hold = True
self.state = 'connected'
hold_supported_streams = (stream for stream in self.streams if stream.hold_supported)
notification_center.post_notification('SIPSessionDidChangeHoldState', self, TimestampedNotificationData(originator='local', on_hold=True, partial=any(not stream.on_hold_by_local for stream in hold_supported_streams)))
for notification in unhandled_notifications:
self.handle_notification(notification)
if not self._hold_in_progress:
for stream in self.streams:
stream.unhold()
self._send_unhold()
else:
self._hold_in_progress = False
def _send_unhold(self):
self.state = 'sending_proposal'
self.greenlet = api.getcurrent()
notification_center = NotificationCenter()
unhandled_notifications = []
try:
local_sdp = SDPSession.new(self._invitation.sdp.active_local)
local_sdp.version += 1
for stream in self.streams:
local_sdp.media[stream.index] = stream.get_local_media(for_offer=True)
self._invitation.send_reinvite(sdp=local_sdp)
received_invitation_state = False
received_sdp_update = False
while not received_invitation_state or not received_sdp_update:
notification = self._channel.wait()
if notification.name == 'SIPInvitationGotSDPUpdate':
received_sdp_update = True
if notification.data.succeeded:
local_sdp = notification.data.local_sdp
remote_sdp = notification.data.remote_sdp
for stream in self.streams:
stream.update(local_sdp, remote_sdp, stream.index)
elif notification.name == 'SIPInvitationChangedState':
if notification.data.state == 'connected' and notification.data.sub_state == 'normal':
received_invitation_state = True
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='local', method='INVITE', code=notification.data.code, reason=notification.data.reason))
elif notification.data.state == 'disconnected':
raise InvitationDisconnectedError(notification.sender, notification.data)
except InvitationDisconnectedError, e:
self.greenlet = None
self.handle_notification(Notification('SIPInvitationChangedState', e.invitation, e.data))
except SIPCoreError, e:
raise #FIXME
else:
self.greenlet = None
self.on_hold = False
self.state = 'connected'
notification_center.post_notification('SIPSessionDidChangeHoldState', self, TimestampedNotificationData(originator='local', on_hold=False, partial=False))
for notification in unhandled_notifications:
self.handle_notification(notification)
if self._hold_in_progress:
for stream in self.streams:
stream.hold()
self._send_hold()
def _fail(self, originator, code, reason, error, reason_header=None):
notification_center = NotificationCenter()
prev_inv_state = self._invitation.state
self.state = 'terminating'
if prev_inv_state not in (None, 'incoming', 'outgoing', 'early', 'connecting'):
notification_center.post_notification('SIPSessionWillEnd', self, TimestampedNotificationData(originator=originator))
if self._invitation.state not in (None, 'disconnecting', 'disconnected'):
try:
if self._invitation.direction == 'incoming' and self._invitation.state in ('incoming', 'early'):
if 400<=code<=699 and reason is not None:
self._invitation.send_response(code, extra_headers=[reason_header] if reason_header is not None else [])
else:
self._invitation.end(extra_headers=[reason_header] if reason_header is not None else [])
with api.timeout(1):
while True:
notification = self._channel.wait()
if notification.name == 'SIPInvitationChangedState' and notification.data.state == 'disconnected':
if prev_inv_state in ('connecting', 'connected'):
if notification.data.disconnect_reason in ('timeout', 'missing ACK'):
sip_code = 200
sip_reason = 'OK'
originator = 'local'
elif hasattr(notification.data, 'method'):
sip_code = 200
sip_reason = 'OK'
originator = 'remote'
else:
sip_code = notification.data.code
sip_reason = notification.data.reason
originator = 'local'
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator=originator, method='BYE', code=sip_code, reason=sip_reason))
elif self._invitation.direction == 'incoming' and prev_inv_state in ('incoming', 'early'):
ack_received = notification.data.disconnect_reason != 'missing ACK'
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='remote', method='INVITE', code=code, reason=reason, ack_received=ack_received))
elif self._invitation.direction == 'outgoing' and prev_inv_state in ('outgoing', 'early'):
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='local', method='INVITE', code=487, reason='Session Cancelled'))
break
except SIPCoreError:
pass
except api.TimeoutError:
if prev_inv_state in ('connecting', 'connected'):
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='local', method='BYE', code=408, reason=sip_status_messages[408]))
notification_center.remove_observer(self, sender=self._invitation)
self.state = 'terminated'
notification_center.post_notification('SIPSessionDidFail', self, TimestampedNotificationData(originator=originator, code=code, reason=reason, failure_reason=error, redirect_identities=None))
self.greenlet = None
def _fail_proposal(self, originator, error):
notification_center = NotificationCenter()
for stream in self.proposed_streams:
try:
notification_center.remove_observer(self, sender=stream)
except KeyError:
# _fail_proposal can be called from reject_proposal, which means the stream will
# not have been initialized or the session registered as an observer for it.
pass
else:
stream.deactivate()
stream.end()
if originator == 'remote' and self._invitation.sub_state == 'received_proposal':
try:
self._invitation.send_response(500)
except SIPCoreError:
pass
else:
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='remote', method='INVITE', code=500, reason=sip_status_messages[500], ack_received='unknown'))
notification_center.post_notification('SIPSessionHadProposalFailure', self, TimestampedNotificationData(originator=originator, failure_reason=error, streams=self.proposed_streams))
self.state = 'connected'
self.proposed_streams = None
self.greenlet = None
@run_in_green_thread
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, None)
if handler is not None:
handler(notification)
def _NH_SIPInvitationChangedState(self, notification):
if self.state == 'terminated':
return
if notification.data.originator == 'remote' and notification.data.state not in ('disconnecting', 'disconnected'):
contact_header = notification.data.headers.get('Contact', None)
if contact_header and 'isfocus' in contact_header[0].parameters:
self.remote_focus = True
if self.greenlet is not None:
if notification.data.state == 'disconnected' and notification.data.prev_state != 'disconnecting':
self._channel.send_exception(InvitationDisconnectedError(notification.sender, notification.data))
else:
self._channel.send(notification)
else:
notification_center = NotificationCenter()
self.greenlet = api.getcurrent()
try:
if notification.data.state == 'connected' and notification.data.sub_state == 'received_proposal':
self.state = 'received_proposal'
try:
proposed_remote_sdp = self._invitation.sdp.proposed_remote
active_remote_sdp = self._invitation.sdp.active_remote
for stream in self.streams:
if not stream.validate_update(proposed_remote_sdp, stream.index):
engine = Engine()
self._invitation.send_response(488, extra_headers=[WarningHeader(399, engine.user_agent, 'Failed to update media stream index %d' % stream.index)])
self.state = 'connected'
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='remote', method='INVITE', code=488, reason=sip_status_messages[488], ack_received='unknown'))
return
# These tests are here because some ALGs mess up the SDP and the behaviour
# of pjsip in these situations is unexpected (eg. loss of audio). -Luci
for attr in ('user', 'net_type', 'address_type'):
if getattr(proposed_remote_sdp, attr) != getattr(active_remote_sdp, attr):
engine = Engine()
self._invitation.send_response(488, extra_headers=[WarningHeader(399, engine.user_agent, 'Difference in contents of o= line')])
self.state = 'connected'
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='remote', method='INVITE', code=488, reason=sip_status_messages[488], ack_received='unknown'))
return
added_media_indexes = set()
removed_media_indexes = set()
for index, media_stream in enumerate(proposed_remote_sdp.media):
if index >= len(active_remote_sdp.media):
added_media_indexes.add(index)
elif media_stream.media != active_remote_sdp.media[index].media:
added_media_indexes.add(index)
removed_media_indexes.add(index)
elif not media_stream.port and active_remote_sdp.media[index].port:
removed_media_indexes.add(index)
removed_media_indexes.update(xrange(len(proposed_remote_sdp.media), len(active_remote_sdp.media)))
if added_media_indexes and removed_media_indexes:
engine = Engine()
self._invitation.send_response(488, extra_headers=[WarningHeader(399, engine.user_agent, 'Both removing AND adding a media stream is currently not supported')])
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='remote', method='INVITE', code=488, reason=sip_status_messages[488], ack_received='unknown'))
elif added_media_indexes:
self.proposed_streams = []
for index in added_media_indexes:
media_stream = proposed_remote_sdp.media[index]
if media_stream.port != 0:
for stream_type in MediaStreamRegistry():
try:
stream = stream_type.new_from_sdp(self.account, proposed_remote_sdp, index)
except InvalidStreamError:
break
except UnknownStreamError:
continue
else:
stream.index = index
self.proposed_streams.append(stream)
break
if self.proposed_streams:
self._invitation.send_response(100)
notification_center.post_notification('SIPSessionGotProposal', sender=self, data=TimestampedNotificationData(originator='remote', streams=self.proposed_streams))
return
else:
self._invitation.send_response(488)
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='remote', method='INVITE', code=488, reason=sip_status_messages[488], ack_received='unknown'))
else:
local_sdp = SDPSession.new(self._invitation.sdp.active_local)
local_sdp.version += 1
removed_streams = [stream for stream in self.streams if stream.index in removed_media_indexes]
prev_on_hold_streams = set(stream for stream in self.streams if stream.hold_supported and stream.on_hold_by_remote)
for stream in removed_streams:
notification_center.remove_observer(self, sender=stream)
stream.deactivate()
local_sdp.media[stream.index].port = 0
local_sdp.media[stream.index].attributes = []
for stream in self.streams:
local_sdp.media[stream.index] = stream.get_local_media(for_offer=False)
try:
self._invitation.send_response(200, sdp=local_sdp)
except PJSIPError, e:
if 'PJMEDIA_SDPNEG' in str(e):
engine = Engine()
self._invitation.send_response(488, extra_headers=[WarningHeader(399, engine.user_agent, 'Changing the codec of an audio stream is currently not supported')])
self.state = 'connected'
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='remote', method='INVITE', code=488, reason=sip_status_messages[488], ack_received='unknown'))
return
else:
raise
else:
for stream in removed_streams:
self.streams.remove(stream)
stream.end()
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='remote', method='INVITE', code=200, reason=sip_status_messages[200], ack_received='unknown'))
received_invitation_state = False
received_sdp_update = False
while not received_sdp_update or not received_invitation_state:
notification = self._channel.wait()
if notification.name == 'SIPInvitationGotSDPUpdate':
received_sdp_update = True
if notification.data.succeeded:
local_sdp = notification.data.local_sdp
remote_sdp = notification.data.remote_sdp
for stream in self.streams:
stream.update(local_sdp, remote_sdp, stream.index)
elif notification.name == 'SIPInvitationChangedState':
if notification.data.state == 'connected' and notification.data.sub_state == 'normal':
received_invitation_state = True
on_hold_streams = set(stream for stream in self.streams if stream.hold_supported and stream.on_hold_by_remote)
if on_hold_streams != prev_on_hold_streams:
hold_supported_streams = (stream for stream in self.streams if stream.hold_supported)
notification_center.post_notification('SIPSessionDidChangeHoldState', self, TimestampedNotificationData(originator='remote', on_hold=bool(on_hold_streams),
partial=bool(on_hold_streams) and any(not stream.on_hold_by_remote for stream in hold_supported_streams)))
if removed_media_indexes:
notification_center.post_notification('SIPSessionDidRenegotiateStreams', self, TimestampedNotificationData(originator='remote', action='remove', streams=removed_streams))
except InvitationDisconnectedError, e:
self.greenlet = None
self.state == 'connected'
self.handle_notification(Notification('SIPInvitationChangedState', e.invitation, e.data))
except SIPCoreError:
raise #FIXME
else:
self.state = 'connected'
elif notification.data.state == 'connected' and notification.data.sub_state == 'normal' and notification.data.prev_sub_state == 'received_proposal':
if notification.data.originator == 'local' and notification.data.code == 487:
self.state = 'connected'
notification_center.post_notification('SIPSessionGotRejectProposal', self, TimestampedNotificationData(originator='remote', code=notification.data.code, reason=notification.data.reason, streams=self.proposed_streams))
self.proposed_streams = None
if self._hold_in_progress:
self._send_hold()
elif notification.data.state == 'disconnected':
if self.state == 'incoming':
self.state = 'terminated'
if notification.data.originator == 'remote':
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator='remote', method='INVITE', code=487, reason='Session Cancelled', ack_received='unknown'))
notification_center.post_notification('SIPSessionDidFail', self, TimestampedNotificationData(originator='remote', code=487, reason='Session Cancelled', failure_reason=notification.data.disconnect_reason, redirect_identities=None))
else:
# There must have been an error involved
notification_center.post_notification('SIPSessionDidFail', self, TimestampedNotificationData(originator='local', code=0, reason=None, failure_reason=notification.data.disconnect_reason, redirect_identities=None))
else:
notification_center.post_notification('SIPSessionWillEnd', self, TimestampedNotificationData(originator=notification.data.originator))
for stream in self.streams:
notification_center.remove_observer(self, sender=stream)
stream.deactivate()
stream.end()
self.state = 'terminated'
if notification.data.originator == 'remote':
if hasattr(notification.data, 'method'):
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator=notification.data.originator, method=notification.data.method, code=200, reason=sip_status_messages[200]))
else:
notification_center.post_notification('SIPSessionDidProcessTransaction', self, TimestampedNotificationData(originator=notification.data.originator, method='INVITE', code=notification.data.code, reason=notification.data.reason))
self.end_time = datetime.now()
notification_center.post_notification('SIPSessionDidEnd', self, TimestampedNotificationData(originator=notification.data.originator, end_reason=notification.data.disconnect_reason))
notification_center.remove_observer(self, sender=self._invitation)
finally:
self.greenlet = None
def _NH_SIPInvitationGotSDPUpdate(self, notification):
if self.greenlet is not None:
self._channel.send(notification)
def _NH_MediaStreamDidInitialize(self, notification):
if self.greenlet is not None:
self._channel.send(notification)
def _NH_MediaStreamDidStart(self, notification):
if self.greenlet is not None:
self._channel.send(notification)
def _NH_MediaStreamDidFail(self, notification):
if self.greenlet is not None:
if self.state not in ('terminating', 'terminated'):
self._channel.send_exception(MediaStreamDidFailError(notification.sender, notification.data))
else:
stream = notification.sender
if self.streams == [stream]:
self.greenlet = None
self.end()
else:
try:
self.remove_stream(stream)
except IllegalStateError:
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=stream)
self.streams.remove(stream)
notification_center.post_notification('SIPSessionDidRenegotiateStreams', self, TimestampedNotificationData(originator='remote', action='remove', streams=[stream]))
class SessionManager(object):
__metaclass__ = Singleton
implements(IObserver)
def __init__(self):
self.sessions = []
self.state = None
self._channel = coros.queue()
def start(self):
self.state = 'starting'
notification_center = NotificationCenter()
notification_center.post_notification('SIPSessionManagerWillStart', self, TimestampedNotificationData())
notification_center.add_observer(self, 'SIPInvitationChangedState')
notification_center.add_observer(self, 'SIPSessionNewIncoming')
notification_center.add_observer(self, 'SIPSessionNewOutgoing')
notification_center.add_observer(self, 'SIPSessionDidFail')
notification_center.add_observer(self, 'SIPSessionDidEnd')
self.state = 'started'
notification_center.post_notification('SIPSessionManagerDidStart', self, TimestampedNotificationData())
def stop(self):
self.state = 'stopping'
notification_center = NotificationCenter()
notification_center.post_notification('SIPSessionManagerWillEnd', self, TimestampedNotificationData())
for session in self.sessions:
session.end()
while self.sessions:
self._channel.wait()
notification_center.remove_observer(self, 'SIPInvitationChangedState')
notification_center.remove_observer(self, 'SIPSessionNewIncoming')
notification_center.remove_observer(self, 'SIPSessionNewOutgoing')
notification_center.remove_observer(self, 'SIPSessionDidFail')
notification_center.remove_observer(self, 'SIPSessionDidEnd')
self.state = 'stopped'
notification_center.post_notification('SIPSessionManagerDidEnd', self, TimestampedNotificationData())
@run_in_twisted_thread
def handle_notification(self, notification):
if notification.name == 'SIPInvitationChangedState' and notification.data.state == 'incoming':
account_manager = AccountManager()
account = account_manager.find_account(notification.data.request_uri)
if account is None:
notification.sender.send_response(404)
return
notification.sender.send_response(100)
session = Session(account)
session.init_incoming(notification.sender, notification.data)
elif notification.name in ('SIPSessionNewIncoming', 'SIPSessionNewOutgoing'):
self.sessions.append(notification.sender)
elif notification.name in ('SIPSessionDidFail', 'SIPSessionDidEnd'):
self.sessions.remove(notification.sender)
if self.state == 'stopping':
self._channel.send(notification)
diff --git a/sipsimple/streams/msrp.py b/sipsimple/streams/msrp.py
index dbacfa01..6e55617f 100644
--- a/sipsimple/streams/msrp.py
+++ b/sipsimple/streams/msrp.py
@@ -1,1115 +1,1115 @@
# Copyright (C) 2009-2011 AG Projects. See LICENSE for details.
#
"""
Handling of MSRP media streams according to RFC4975, RFC4976, RFC5547
and RFC3994.
This module provides classes to parse and generate SDP related to SIP
sessions that negotiate Instant Messsaging, File Transfer and Desktop
Sharing and handling of the actual media streams.
"""
__all__ = ['MSRPStreamError', 'ChatStreamError', 'ChatStream', 'FileSelector', 'FileTransferStream', 'IDesktopSharingHandler', 'DesktopSharingHandlerBase',
'InternalVNCViewerHandler', 'InternalVNCServerHandler', 'ExternalVNCViewerHandler', 'ExternalVNCServerHandler', 'DesktopSharingStream']
import os
import re
import random
import hashlib
import mimetypes
from datetime import datetime
from application.notification import NotificationCenter, NotificationData, IObserver
from application.system import host
from dateutil.tz import tzlocal
from functools import partial
from itertools import chain
from twisted.internet.error import ConnectionDone
from twisted.python.failure import Failure
from zope.interface import implements, Interface, Attribute
from eventlet import api
from eventlet.coros import queue
from eventlet.greenio import GreenSocket
from eventlet.proc import spawn, ProcExit
from eventlet.util import tcp_socket, set_reuse_addr
from msrplib.connect import DirectConnector, DirectAcceptor, RelayConnection, MSRPRelaySettings
from msrplib.protocol import URI, FailureReportHeader, SuccessReportHeader, ContentTypeHeader, parse_uri
from msrplib.session import MSRPSession, contains_mime_type, OutgoingFile
from msrplib.transport import make_response, make_report
from sipsimple.account import Account, BonjourAccount
from sipsimple.core import SDPAttribute, SDPMediaStream
-from sipsimple.payloads.iscomposing import IsComposingMessage, State, LastActive, Refresh, ContentType
+from sipsimple.payloads.iscomposing import IsComposingDocument, State, LastActive, Refresh, ContentType
from sipsimple.streams import IMediaStream, MediaStreamRegistrar, StreamError, InvalidStreamError, UnknownStreamError
from sipsimple.streams.applications.chat import ChatIdentity, ChatMessage, CPIMMessage, CPIMParserError
from sipsimple.threading import run_in_twisted_thread
from sipsimple.threading.green import run_in_green_thread
from sipsimple.util import TimestampedNotificationData
class MSRPStreamError(StreamError): pass
class ChatStreamError(MSRPStreamError): pass
class MSRPStreamBase(object):
__metaclass__ = MediaStreamRegistrar
implements(IMediaStream, IObserver)
# Attributes that need to be defined by each MSRP stream type
type = None
priority = None
use_msrp_session = False
media_type = None
accept_types = None
accept_wrapped_types = None
# These attributes are always False for any MSRP stream
hold_supported = False
on_hold = False
on_hold_by_local = False
on_hold_by_remote = False
def __new__(cls, *args, **kw):
if cls is MSRPStreamBase:
raise TypeError("MSRPStreamBase cannot be instantiated directly")
return object.__new__(cls)
def __init__(self, account, direction='sendrecv'):
self.account = account
self.direction = direction
self.greenlet = None
self.local_identity = ChatIdentity(self.account.uri, self.account.display_name)
self.local_media = None
self.remote_media = None
self.remote_identity = None ## will be filled in by start()
self.msrp = None ## Placeholder for the MSRPTransport that will be set when started
self.msrp_connector = None
self.cpim_enabled = None ## Boolean value. None means it was not negotiated yet
self.session = None
self.msrp_session = None
self.shutting_down = False
self.local_role = None
self.remote_role = None
@property
def local_uri(self):
return URI(host=host.default_ip, port=0, use_tls=self.transport=='tls', credentials=self.account.tls_credentials)
def _create_local_media(self, uri_path):
transport = "TCP/TLS/MSRP" if uri_path[-1].use_tls else "TCP/MSRP"
attributes = [SDPAttribute("path", " ".join(str(uri) for uri in uri_path))]
if self.direction not in [None, 'sendrecv']:
attributes.append(SDPAttribute(self.direction, ''))
if self.accept_types is not None:
attributes.append(SDPAttribute("accept-types", " ".join(self.accept_types)))
if self.accept_wrapped_types is not None:
attributes.append(SDPAttribute("accept-wrapped-types", " ".join(self.accept_wrapped_types)))
attributes.append(SDPAttribute("setup", self.local_role))
return SDPMediaStream(self.media_type, uri_path[-1].port or 12345, transport, formats=["*"], attributes=attributes)
## The public API (the IMediaStream interface)
def get_local_media(self, for_offer=True):
return self.local_media
def new_from_sdp(self, account, remote_sdp, stream_index):
raise NotImplementedError
@run_in_green_thread
def initialize(self, session, direction):
self.greenlet = api.getcurrent()
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=self)
try:
self.session = session
self.transport = self.account.msrp.transport
outgoing = direction=='outgoing'
logger = NotificationProxyLogger()
if self.account is BonjourAccount():
if outgoing:
self.msrp_connector = DirectConnector(logger=logger)
self.local_role = 'active'
else:
if self.transport=='tls' and None in (self.account.tls_credentials.cert, self.account.tls_credentials.key):
raise MSRPStreamError("Cannot accept MSRP connection without a TLS certificate")
self.msrp_connector = DirectAcceptor(logger=logger)
self.local_role = 'passive'
else:
if self.account.msrp.connection_model == 'relay':
if not outgoing and self.remote_role in ('actpass', 'passive'):
# 'passive' not allowed by the RFC but play nice for interoperability. -Saul
self.msrp_connector = DirectConnector(logger=logger, use_sessmatch=True)
self.local_role = 'active'
elif outgoing and not self.account.nat_traversal.use_msrp_relay_for_outbound:
self.msrp_connector = DirectConnector(logger=logger, use_sessmatch=True)
self.local_role = 'active'
else:
if self.account.nat_traversal.msrp_relay is None:
relay_host = relay_port = None
else:
if self.transport != self.account.nat_traversal.msrp_relay.transport:
raise MSRPStreamError("MSRP relay transport conflicts with MSRP transport setting")
relay_host = self.account.nat_traversal.msrp_relay.host
relay_port = self.account.nat_traversal.msrp_relay.port
relay = MSRPRelaySettings(domain=self.account.uri.host,
username=self.account.uri.user,
password=self.account.credentials.password,
host=relay_host,
port=relay_port,
use_tls=self.transport=='tls')
self.msrp_connector = RelayConnection(relay, 'passive', logger=logger, use_sessmatch=True)
self.local_role = 'actpass' if outgoing else 'passive'
else:
if not outgoing and self.remote_role in ('actpass', 'passive'):
# 'passive' not allowed by the RFC but play nice for interoperability. -Saul
self.msrp_connector = DirectConnector(logger=logger, use_sessmatch=True)
self.local_role = 'active'
else:
if not outgoing and self.transport=='tls' and None in (self.account.tls_credentials.cert, self.account.tls_credentials.key):
raise MSRPStreamError("Cannot accept MSRP connection without a TLS certificate")
self.msrp_connector = DirectAcceptor(logger=logger, use_sessmatch=True)
self.local_role = 'actpass' if outgoing else 'passive'
full_local_path = self.msrp_connector.prepare(self.local_uri)
self.local_media = self._create_local_media(full_local_path)
except api.GreenletExit:
raise
except Exception, ex:
ndata = TimestampedNotificationData(context='initialize', failure=Failure(), reason=str(ex))
notification_center.post_notification('MediaStreamDidFail', self, ndata)
else:
notification_center.post_notification('MediaStreamDidInitialize', self, data=TimestampedNotificationData())
finally:
if self.msrp_session is None and self.msrp is None and self.msrp_connector is None:
notification_center.remove_observer(self, sender=self)
self.greenlet = None
@run_in_green_thread
def start(self, local_sdp, remote_sdp, stream_index):
self.greenlet = api.getcurrent()
notification_center = NotificationCenter()
try:
context = 'sdp_negotiation'
self.remote_identity = ChatIdentity(self.session.remote_identity.uri, self.session.remote_identity.display_name)
remote_media = remote_sdp.media[stream_index]
self.remote_media = remote_media
remote_accept_types = remote_media.attributes.getfirst('accept-types')
# TODO: update accept_types and accept_wrapped_types from remote_media
self.cpim_enabled = contains_mime_type(self.accept_types, 'message/cpim') and contains_mime_type(remote_accept_types.split(), 'message/cpim')
remote_uri_path = remote_media.attributes.getfirst('path')
if remote_uri_path is None:
raise AttributeError("remote SDP media does not have 'path' attribute")
full_remote_path = [parse_uri(uri) for uri in remote_uri_path.split()]
remote_transport = 'tls' if full_remote_path[0].use_tls else 'tcp'
if self.transport != remote_transport:
raise MSRPStreamError("remote transport ('%s') different from local transport ('%s')" % (remote_transport, self.transport))
if isinstance(self.account, Account) and self.local_role == 'actpass':
remote_setup = remote_media.attributes.getfirst('setup', 'passive')
if remote_setup == 'passive':
# If actpass is offered connectors are always started as passive
# We need to switch to active if the remote answers with passive
if self.account.msrp.connection_model == 'relay':
self.msrp_connector.mode = 'active'
else:
local_uri = self.msrp_connector.local_uri
logger = self.msrp_connector.logger
self.msrp_connector = DirectConnector(logger=logger, use_sessmatch=True)
self.msrp_connector.prepare(local_uri)
context = 'start'
self.msrp = self.msrp_connector.complete(full_remote_path)
if self.use_msrp_session:
self.msrp_session = MSRPSession(self.msrp, accept_types=self.accept_types, on_incoming_cb=self._handle_incoming)
self.msrp_connector = None
except api.GreenletExit:
raise
except Exception, ex:
ndata = TimestampedNotificationData(context=context, failure=Failure(), reason=str(ex) or type(ex).__name__)
notification_center.post_notification('MediaStreamDidFail', self, ndata)
else:
notification_center.post_notification('MediaStreamDidStart', self, data=TimestampedNotificationData())
finally:
self.greenlet = None
def deactivate(self):
self.shutting_down = True
@run_in_green_thread
def end(self):
if self.msrp_session is None and self.msrp is None and self.msrp_connector is None:
self.session = None
return
notification_center = NotificationCenter()
notification_center.post_notification('MediaStreamWillEnd', self, data=TimestampedNotificationData())
msrp = self.msrp
msrp_session = self.msrp_session
msrp_connector = self.msrp_connector
try:
if self.greenlet is not None:
api.kill(self.greenlet)
if msrp_session is not None:
msrp_session.shutdown()
elif msrp is not None:
msrp.loseConnection(wait=False)
if msrp_connector is not None:
msrp_connector.cleanup()
finally:
notification_center.post_notification('MediaStreamDidEnd', self, data=TimestampedNotificationData())
notification_center.remove_observer(self, sender=self)
self.msrp = None
self.msrp_session = None
self.msrp_connector = None
self.session = None
def validate_update(self, remote_sdp, stream_index):
return True #TODO
def update(self, local_sdp, remote_sdp, stream_index):
pass #TODO
def hold(self):
pass
def unhold(self):
pass
## Internal IObserver interface
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, None)
if handler is not None:
handler(notification)
## Internal message handlers
def _handle_incoming(self, chunk=None, error=None):
notification_center = NotificationCenter()
if error is not None:
if self.shutting_down and isinstance(error.value, ConnectionDone):
return
ndata = TimestampedNotificationData(context='reading', failure=error, reason=error.getErrorMessage())
notification_center.post_notification('MediaStreamDidFail', self, ndata)
elif chunk is not None:
method_handler = getattr(self, '_handle_%s' % chunk.method, None)
if method_handler is not None:
method_handler(chunk)
def _handle_REPORT(self, chunk):
pass
def _handle_SEND(self, chunk):
pass
class ChatStream(MSRPStreamBase):
type = 'chat'
priority = 1
use_msrp_session = True
media_type = 'message'
accept_types = ['message/cpim', 'text/*', 'application/im-iscomposing+xml']
accept_wrapped_types = ['*']
def __init__(self, account, direction='sendrecv'):
MSRPStreamBase.__init__(self, account, direction)
self.message_queue = queue()
self.sent_messages = set()
@classmethod
def new_from_sdp(cls, account, remote_sdp, stream_index):
remote_stream = remote_sdp.media[stream_index]
if remote_stream.media != 'message':
raise UnknownStreamError
expected_transport = 'TCP/TLS/MSRP' if account.msrp.transport=='tls' else 'TCP/MSRP'
if remote_stream.transport != expected_transport:
raise InvalidStreamError("expected %s transport in chat stream, got %s" % (expected_transport, remote_stream.transport))
if remote_stream.formats != ['*']:
raise InvalidStreamError("wrong format list specified")
stream = cls(account)
stream.remote_role = remote_stream.attributes.getfirst('setup', 'active')
if (remote_stream.direction, stream.direction) not in (('sendrecv', 'sendrecv'), ('sendonly', 'recvonly'), ('recvonly', 'sendonly')):
raise InvalidStreamError("mismatching directions in chat stream")
remote_accept_types = remote_stream.attributes.getfirst('accept-types')
if remote_accept_types is None:
raise InvalidStreamError("remote SDP media does not have 'accept-types' attribute")
if not any(contains_mime_type(cls.accept_types, mime_type) for mime_type in remote_accept_types.split()):
raise InvalidStreamError("no compatible media types found")
return stream
@property
def private_messages_allowed(self):
remote_chatroom_capabilities = chain(*(attr.split() for attr in self.remote_media.attributes.getall('chatroom')))
try:
return self.cpim_enabled and self.session.remote_focus and 'private-messages' in remote_chatroom_capabilities
except AttributeError:
return False
# TODO: chatroom, recvonly/sendonly (in start)?
def _NH_MediaStreamDidStart(self, notification):
spawn(self._message_queue_handler)
def _NH_MediaStreamDidEnd(self, notification):
self.message_queue.send_exception(ProcExit)
def _handle_REPORT(self, chunk):
# in theory, REPORT can come with Byte-Range which would limit the scope of the REPORT to the part of the message.
if chunk.message_id in self.sent_messages:
self.sent_messages.remove(chunk.message_id)
notification_center = NotificationCenter()
data = TimestampedNotificationData(message_id=chunk.message_id, message=chunk, code=chunk.status.code, reason=chunk.status.comment)
if chunk.status.code == 200:
notification_center.post_notification('ChatStreamDidDeliverMessage', self, data)
else:
notification_center.post_notification('ChatStreamDidNotDeliverMessage', self, data)
def _handle_SEND(self, chunk):
if self.direction=='sendonly':
return
if not chunk.data:
return
if chunk.content_type.lower() == 'message/cpim':
try:
message = CPIMMessage.parse(chunk.data)
except CPIMParserError:
# FIXME: should respond with negative code, but MSRPlib responds with a positive one for us
return
else:
if message.timestamp is None:
message.timestamp = datetime.now(tzlocal())
if message.sender is None:
message.sender = self.remote_identity
private = self.session.remote_focus and len(message.recipients) == 1 and message.recipients[0].uri != self.remote_identity.uri
else:
message = ChatMessage(chunk.data, chunk.content_type, self.remote_identity, self.local_identity, datetime.now(tzlocal()))
private = False
# Note: success reports are issued by msrplib
# TODO: check wrapped content-type and issue a report/responsd with negative code if it's invalid
notification_center = NotificationCenter()
- if message.content_type.lower() == IsComposingMessage.content_type:
- data = IsComposingMessage.parse(message.body)
+ if message.content_type.lower() == IsComposingDocument.content_type:
+ data = IsComposingDocument.parse(message.body)
ndata = TimestampedNotificationData(state=data.state.value,
refresh=data.refresh.value if data.refresh is not None else None,
content_type=data.contenttype.value if data.contenttype is not None else None,
last_active=data.last_active.value if data.last_active is not None else None,
sender=message.sender, recipients=message.recipients, private=private)
notification_center.post_notification('ChatStreamGotComposingIndication', self, ndata)
else:
notification_center.post_notification('ChatStreamGotMessage', self, TimestampedNotificationData(message=message, private=private))
def _on_transaction_response(self, message_id, response):
if message_id in self.sent_messages and response.code != 200:
self.sent_messages.remove(message_id)
data = TimestampedNotificationData(message_id=message_id, message=response, code=response.code, reason=response.comment)
NotificationCenter().post_notification('ChatStreamDidNotDeliverMessage', self, data)
def _message_queue_handler(self):
notification_center = NotificationCenter()
while True:
message_id, message, content_type, failure_report, success_report, notify_progress = self.message_queue.wait()
if self.msrp_session is None:
# should we generate ChatStreamDidNotDeliver per each message in the queue here?
break
chunk = self.msrp_session.make_message(message, content_type=content_type, message_id=message_id)
if failure_report is not None:
chunk.add_header(FailureReportHeader(failure_report))
if success_report is not None:
chunk.add_header(SuccessReportHeader(success_report))
try:
self.msrp_session.send_chunk(chunk, response_cb=partial(self._on_transaction_response, message_id))
except Exception, e:
ndata = TimestampedNotificationData(context='sending', failure=Failure(), reason=str(e))
notification_center.post_notification('MediaStreamDidFail', self, ndata)
break
else:
if notify_progress and success_report == 'yes' and failure_report != 'no':
self.sent_messages.add(message_id)
notification_center.post_notification('ChatStreamDidSendMessage', self, TimestampedNotificationData(message=chunk))
@run_in_twisted_thread
def _enqueue_message(self, message_id, message, content_type, failure_report=None, success_report=None, notify_progress=True):
self.message_queue.send((message_id, message, content_type, failure_report, success_report, notify_progress))
def send_message(self, content, content_type='text/plain', recipients=None, courtesy_recipients=None, subject=None, timestamp=None, required=None, additional_headers=None):
"""Send IM message. Prefer Message/CPIM wrapper if it is supported.
If called before the connection was established, the messages will be
queued until MediaStreamDidStart notification.
- content (str) - content of the message;
- remote_identity (CPIMIdentity) - "To" header of CPIM wrapper;
if None, use the default obtained from the session
'remote_identity' may only differ from the one obtained from the session if the remote
party supports private messages. If it does not, ChatStreamError will be raised;
- content_type (str) - Content-Type of wrapped message;
(Content-Type of MSRP message is always Message/CPIM in that case)
If Message/CPIM is not supported, Content-Type of MSRP message.
Return generated MSRP chunk (MSRPData); to get Message-ID use its 'message_id' attribute.
These MSRP headers are used to enable end-to-end success reports and
to disable hop-to-hop successful responses:
Failure-Report: partial
Success-Report: yes
"""
if self.direction=='recvonly':
raise ChatStreamError('Cannot send message on recvonly stream')
message_id = '%x' % random.getrandbits(64)
if self.cpim_enabled:
if not contains_mime_type(self.accept_wrapped_types, content_type):
raise ChatStreamError('Invalid content_type for outgoing message: %r' % content_type)
if not recipients:
recipients = [self.remote_identity]
elif not self.private_messages_allowed and recipients != [self.remote_identity]:
raise ChatStreamError('The remote end does not support private messages')
if timestamp is None:
timestamp = datetime.now()
msg = CPIMMessage(content, content_type, sender=self.local_identity, recipients=recipients, courtesy_recipients=courtesy_recipients,
subject=subject, timestamp=timestamp, required=required, additional_headers=additional_headers)
self._enqueue_message(message_id, str(msg), 'message/cpim', failure_report='yes', success_report='yes', notify_progress=True)
else:
if not contains_mime_type(self.accept_types, content_type):
raise ChatStreamError('Invalid content_type for outgoing message: %r' % content_type)
if recipients is not None and recipients != [self.remote_identity]:
raise ChatStreamError('Private messages are not available, because CPIM wrapper is not used')
if courtesy_recipients or subject or timestamp or required or additional_headers:
raise ChatStreamError('Additional message meta-data cannot be sent, because CPIM wrapper is not used')
self._enqueue_message(message_id, content, content_type, failure_report='yes', success_report='yes', notify_progress=True)
return message_id
def send_composing_indication(self, state, refresh, last_active=None, recipients=None):
if self.direction == 'recvonly':
raise ChatStreamError('Cannot send message on recvonly stream')
if state not in ('active', 'idle'):
raise ValueError('Invalid value for composing indication state')
message_id = '%x' % random.getrandbits(64)
- content = IsComposingMessage(state=State(state), refresh=Refresh(refresh), last_active=LastActive(last_active or datetime.now()), content_type=ContentType('text')).toxml()
+ content = IsComposingDocument.create(state=State(state), refresh=Refresh(refresh), last_active=LastActive(last_active or datetime.now()), content_type=ContentType('text'))
if self.cpim_enabled:
if recipients is None:
recipients = [self.remote_identity]
elif not self.private_messages_allowed and recipients != [self.remote_identity]:
raise ChatStreamError('The remote end does not support private messages')
- msg = CPIMMessage(content, IsComposingMessage.content_type, sender=self.local_identity, recipients=recipients, timestamp=datetime.now())
+ msg = CPIMMessage(content, IsComposingDocument.content_type, sender=self.local_identity, recipients=recipients, timestamp=datetime.now())
self._enqueue_message(message_id, str(msg), 'message/cpim', failure_report='partial', success_report='no')
else:
if recipients is not None and recipients != [self.remote_identity]:
raise ChatStreamError('Private messages are not available, because CPIM wrapper is not used')
- self._enqueue_message(message_id, content, IsComposingMessage.content_type, failure_report='partial', success_report='no', notify_progress=False)
+ self._enqueue_message(message_id, content, IsComposingDocument.content_type, failure_report='partial', success_report='no', notify_progress=False)
return message_id
# File transfer
#
class ComputeHash: pass
class FileSelector(object):
class __metaclass__(type):
_name_re = re.compile('name:"([^"]+)"')
_size_re = re.compile('size:(\d+)')
_type_re = re.compile('type:([^ ]+)')
_hash_re = re.compile('hash:([^ ]+)')
_byte_re = re.compile('..')
def __init__(self, name=None, type=None, size=None, hash=None, fd=None):
## If present, hash should be a sha1 object or a string in the form: sha-1:72:24:5F:E8:65:3D:DA:F3:71:36:2F:86:D4:71:91:3E:E4:A2:CE:2E
## According to the specification, only sha1 is supported ATM.
self.name = name
self.type = type
self.size = size
self.hash = hash
self.fd = fd
def _get_hash(self):
return self.__dict__['hash']
def _set_hash(self, value):
if value is None:
self.__dict__['hash'] = value
elif isinstance(value, str) and value.startswith('sha1:'):
self.__dict__['hash'] = value
elif hasattr(value, 'hexdigest') and hasattr(value, 'name'):
if value.name != 'sha1':
raise TypeError("Invalid hash type: '%s'. Only sha1 hashes are supported" % value.name)
# unexpected as it may be, using a regular expression is the fastest method to do this
self.__dict__['hash'] = 'sha1:' + ':'.join(self.__class__._byte_re.findall(value.hexdigest().upper()))
else:
raise ValueError("Invalid hash value")
hash = property(_get_hash, _set_hash)
del _get_hash, _set_hash
@classmethod
def parse(cls, string):
name_match = cls._name_re.search(string)
size_match = cls._size_re.search(string)
type_match = cls._type_re.search(string)
hash_match = cls._hash_re.search(string)
name = name_match and name_match.group(1)
size = size_match and int(size_match.group(1))
type = type_match and type_match.group(1)
hash = hash_match and hash_match.group(1)
return cls(name, type, size, hash)
@classmethod
def for_file(cls, path, type=None, hash=ComputeHash):
fd = open(path, 'r')
name = os.path.basename(path)
size = os.fstat(fd.fileno()).st_size
if type is None:
mime_type, encoding = mimetypes.guess_type(name)
if encoding is not None:
type = 'application/x-%s' % encoding
elif mime_type is not None:
type = mime_type
else:
type = 'application/octet-stream'
if hash is ComputeHash:
sha1 = hashlib.sha1()
while True:
content = fd.read(65536)
if not content:
break
sha1.update(content)
fd.seek(0)
# unexpected as it may be, using a regular expression is the fastest method to do this
hash = 'sha1:' + ':'.join(cls._byte_re.findall(sha1.hexdigest().upper()))
return cls(name, type, size, hash, fd)
@property
def sdp_repr(self):
items = [('name', self.name and '"%s"' % self.name), ('type', self.type), ('size', self.size), ('hash', self.hash)]
return ' '.join('%s:%s' % (name, value) for name, value in items if value is not None)
class FileTransferStream(MSRPStreamBase):
type = 'file-transfer'
priority = 10
use_msrp_session = True
media_type = 'message'
accept_types = ['*']
accept_wrapped_types = ['*']
def __init__(self, account, file_selector, direction):
if direction not in ('sendonly', 'recvonly'):
raise ValueError("direction must be one of 'sendonly' or 'recvonly'")
MSRPStreamBase.__init__(self, account, direction=direction)
self.file_selector = file_selector
@classmethod
def new_from_sdp(cls, account, remote_sdp, stream_index):
remote_stream = remote_sdp.media[stream_index]
if remote_stream.media != 'message' or 'file-selector' not in remote_stream.attributes:
raise UnknownStreamError
expected_transport = 'TCP/TLS/MSRP' if account.msrp.transport=='tls' else 'TCP/MSRP'
if remote_stream.transport != expected_transport:
raise InvalidStreamError("expected %s transport in file transfer stream, got %s" % (expected_transport, remote_stream.transport))
if remote_stream.formats != ['*']:
raise InvalidStreamError("wrong format list specified")
file_selector = FileSelector.parse(remote_stream.attributes.getfirst('file-selector'))
if remote_stream.direction == 'sendonly':
stream = cls(account, file_selector, 'recvonly')
elif remote_stream.direction == 'recvonly':
stream = cls(account, file_selector, 'sendonly')
else:
raise InvalidStreamError("wrong stream direction specified")
stream.remote_role = remote_stream.attributes.getfirst('setup', 'active')
return stream
def initialize(self, session, direction):
if self.direction == 'sendonly' and self.file_selector.fd is None:
ndata = TimestampedNotificationData(context='initialize', failure=None, reason='file descriptor not specified')
notification_center = NotificationCenter()
notification_center.post_notification('MediaStreamDidFail', self, ndata)
return
MSRPStreamBase.initialize(self, session, direction)
def _create_local_media(self, uri_path):
local_media = MSRPStreamBase._create_local_media(self, uri_path)
local_media.attributes.append(SDPAttribute('file-selector', self.file_selector.sdp_repr))
return local_media
def _NH_MediaStreamDidStart(self, notification):
if self.direction == 'sendonly':
outgoing_file = OutgoingFile(self.file_selector.fd, self.file_selector.size, content_type=self.file_selector.type)
outgoing_file.headers['Success-Report'] = SuccessReportHeader('yes')
outgoing_file.headers['Failure-Report'] = FailureReportHeader('yes')
self.msrp_session.send_file(outgoing_file)
def _handle_REPORT(self, chunk):
# in theory, REPORT can come with Byte-Range which would limit the scope of the REPORT to the part of the message.
notification_center = NotificationCenter()
data = TimestampedNotificationData(message_id=chunk.message_id, chunk=chunk, code=chunk.status.code, reason=chunk.status.comment)
if chunk.status.code == 200:
# Calculating the number of bytes transferred so far by looking at the Byte-Range of this message
# only works as long as chunks are delivered in order. -Luci
data.transferred_bytes = chunk.byte_range[1]
data.file_size = chunk.byte_range[2]
notification_center.post_notification('FileTransferStreamDidDeliverChunk', self, data)
if data.transferred_bytes == data.file_size:
notification_center.post_notification('FileTransferStreamDidFinish', self, TimestampedNotificationData())
else:
notification_center.post_notification('FileTransferStreamDidNotDeliverChunk', self, data)
def _handle_SEND(self, chunk):
notification_center = NotificationCenter()
if self.direction=='sendonly':
return # should we just ignore this? -Dan
if not chunk.data:
return
if chunk.content_type.lower() == 'message/cpim':
# In order to properly support the CPIM wrapper, msrplib needs to be refactored. -Luci
e = MSRPStreamError("CPIM wrapper is not supported")
notification_center.post_notification('MediaStreamDidFail', self, TimestampedNotificationData(failure=Failure(e), reason=str(e)))
return
# Note: success reports are issued by msrplib
# TODO: check wrapped content-type and issue a report if it's invalid
# Calculating the number of bytes transferred so far by looking at the Byte-Range of this message
# only works as long as chunks are delivered in order. -Luci
ndata = TimestampedNotificationData(content=chunk.data, content_type=chunk.content_type, transferred_bytes=chunk.byte_range[0]+chunk.size-1, file_size=chunk.byte_range[2])
notification_center.post_notification('FileTransferStreamGotChunk', self, ndata)
if ndata.transferred_bytes == ndata.file_size:
notification_center.post_notification('FileTransferStreamDidFinish', self, TimestampedNotificationData())
# Desktop sharing
#
class VNCConnectionError(Exception): pass
class IDesktopSharingHandler(Interface):
type = Attribute("A string identifying the direction: passive for a server, active for a client")
def initialize(self, stream):
pass
class DesktopSharingHandlerBase(object):
implements(IDesktopSharingHandler, IObserver)
type = None
def __new__(cls, *args, **kw):
if cls is DesktopSharingHandlerBase:
raise TypeError("DesktopSharingHandlerBase cannot be instantiated directly")
return object.__new__(cls)
def __init__(self):
self.incoming_msrp_queue = None
self.outgoing_msrp_queue = None
self.msrp_reader_thread = None
self.msrp_writer_thread = None
def initialize(self, stream):
self.incoming_msrp_queue = stream.incoming_queue
self.outgoing_msrp_queue = stream.outgoing_queue
NotificationCenter().add_observer(self, sender=stream)
def _msrp_reader(self):
raise NotImplementedError
def _msrp_writer(self):
raise NotImplementedError
## Internal IObserver interface
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, None)
if handler is not None:
handler(notification)
def _NH_MediaStreamDidStart(self, notification):
self.msrp_reader_thread = spawn(self._msrp_reader)
self.msrp_writer_thread = spawn(self._msrp_reader)
def _NH_MediaStreamWillEnd(self, notification):
NotificationCenter().remove_observer(self, sender=notification.sender)
if self.msrp_reader_thread is not None:
self.msrp_reader_thread.kill()
self.msrp_reader_thread = None
if self.msrp_writer_thread is not None:
self.msrp_writer_thread.kill()
self.msrp_writer_thread = None
class InternalVNCViewerHandler(DesktopSharingHandlerBase):
type = 'active'
@run_in_twisted_thread
def send(self, data):
self.outgoing_msrp_queue.send(data)
def _msrp_reader(self):
notification_center = NotificationCenter()
while True:
data = self.incoming_msrp_queue.wait()
notification_center.post_notification('DesktopSharingStreamGotData', self, NotificationData(data=data))
def _msrp_writer(self):
pass
class InternalVNCServerHandler(DesktopSharingHandlerBase):
type = 'passive'
@run_in_twisted_thread
def send(self, data):
self.outgoing_msrp_queue.send(data)
def _msrp_reader(self):
notification_center = NotificationCenter()
while True:
data = self.incoming_msrp_queue.wait()
notification_center.post_notification('DesktopSharingStreamGotData', self, NotificationData(data=data))
def _msrp_writer(self):
pass
class ExternalVNCViewerHandler(DesktopSharingHandlerBase):
type = 'active'
def __init__(self, address=('localhost', 0), connect_timeout=3):
DesktopSharingHandlerBase.__init__(self)
self.vnc_starter_thread = None
self.vnc_socket = GreenSocket(tcp_socket())
set_reuse_addr(self.vnc_socket)
self.vnc_socket.settimeout(connect_timeout)
self.vnc_socket.bind(address)
self.vnc_socket.listen(1)
self.address = self.vnc_socket.getsockname()
def _msrp_reader(self):
while True:
try:
data = self.incoming_msrp_queue.wait()
self.vnc_socket.sendall(data)
except ProcExit:
raise
except Exception, e:
self.msrp_reader_thread = None # avoid issues caused by the notification handler killing this greenlet during post_notification
ndata = TimestampedNotificationData(context='sending', failure=Failure(), reason=str(e))
NotificationCenter().post_notification('DesktopSharingHandlerDidFail', self, ndata)
break
def _msrp_writer(self):
while True:
try:
data = self.vnc_socket.recv(2048)
if not data:
raise VNCConnectionError("connection with the VNC viewer was closed")
self.outgoing_msrp_queue.send(data)
except ProcExit:
raise
except Exception, e:
self.msrp_writer_thread = None # avoid issues caused by the notification handler killing this greenlet during post_notification
ndata = TimestampedNotificationData(context='reading', failure=Failure(), reason=str(e))
NotificationCenter().post_notification('DesktopSharingHandlerDidFail', self, ndata)
break
def _start_vnc_connection(self):
try:
sock, addr = self.vnc_socket.accept()
self.vnc_socket.close()
self.vnc_socket = sock
self.vnc_socket.settimeout(None)
except ProcExit:
raise
except Exception, e:
self.vnc_starter_thread = None # avoid issues caused by the notification handler killing this greenlet during post_notification
ndata = TimestampedNotificationData(context='connecting', failure=Failure(), reason=str(e))
NotificationCenter().post_notification('DesktopSharingHandlerDidFail', self, ndata)
else:
self.msrp_reader_thread = spawn(self._msrp_reader)
self.msrp_writer_thread = spawn(self._msrp_writer)
finally:
self.vnc_starter_thread = None
def _NH_MediaStreamDidStart(self, notification):
self.vnc_starter_thread = spawn(self._start_vnc_connection)
def _NH_MediaStreamWillEnd(self, notification):
if self.vnc_starter_thread is not None:
self.vnc_starter_thread.kill()
self.vnc_starter_thread = None
DesktopSharingHandlerBase._NH_MediaStreamWillEnd(self, notification)
class ExternalVNCServerHandler(DesktopSharingHandlerBase):
type = 'passive'
def __init__(self, address, connect_timeout=3):
DesktopSharingHandlerBase.__init__(self)
self.address = address
self.vnc_starter_thread = None
self.vnc_socket = None
self.connect_timeout = connect_timeout
def _msrp_reader(self):
while True:
try:
data = self.incoming_msrp_queue.wait()
self.vnc_socket.sendall(data)
except ProcExit:
raise
except Exception, e:
self.msrp_reader_thread = None # avoid issues caused by the notification handler killing this greenlet during post_notification
ndata = TimestampedNotificationData(context='sending', failure=Failure(), reason=str(e))
NotificationCenter().post_notification('DesktopSharingHandlerDidFail', self, ndata)
break
def _msrp_writer(self):
while True:
try:
data = self.vnc_socket.recv(2048)
if not data:
raise VNCConnectionError("connection to the VNC server was closed")
self.outgoing_msrp_queue.send(data)
except ProcExit:
raise
except Exception, e:
self.msrp_writer_thread = None # avoid issues caused by the notification handler killing this greenlet during post_notification
ndata = TimestampedNotificationData(context='reading', failure=Failure(), reason=str(e))
NotificationCenter().post_notification('DesktopSharingHandlerDidFail', self, ndata)
break
def _start_vnc_connection(self):
try:
self.vnc_socket = GreenSocket(tcp_socket())
self.vnc_socket.settimeout(self.connect_timeout)
self.vnc_socket.connect(self.address)
self.vnc_socket.settimeout(None)
except ProcExit:
raise
except Exception, e:
self.vnc_starter_thread = None # avoid issues caused by the notification handler killing this greenlet during post_notification
ndata = TimestampedNotificationData(context='connecting', failure=Failure(), reason=str(e))
NotificationCenter().post_notification('DesktopSharingHandlerDidFail', self, ndata)
else:
self.msrp_reader_thread = spawn(self._msrp_reader)
self.msrp_writer_thread = spawn(self._msrp_writer)
finally:
self.vnc_starter_thread = None
def _NH_MediaStreamDidStart(self, notification):
self.vnc_starter_thread = spawn(self._start_vnc_connection)
def _NH_MediaStreamWillEnd(self, notification):
if self.vnc_starter_thread is not None:
self.vnc_starter_thread.kill()
self.vnc_starter_thread = None
DesktopSharingHandlerBase._NH_MediaStreamWillEnd(self, notification)
if self.vnc_socket is not None:
self.vnc_socket.close()
class DesktopSharingStream(MSRPStreamBase):
type = 'desktop-sharing'
priority = 1
use_msrp_session = False
media_type = 'application'
accept_types = ['application/x-rfb']
accept_wrapped_types = None
def __init__(self, account, handler):
MSRPStreamBase.__init__(self, account, direction='sendrecv')
self.handler = handler
self.incoming_queue = queue()
self.outgoing_queue = queue()
self.msrp_reader_thread = None
self.msrp_writer_thread = None
def _get_handler(self):
return self.__dict__['handler']
def _set_handler(self, handler):
if handler is None:
raise ValueError("handler cannot be None")
if 'handler' in self.__dict__ and self.handler.type != handler.type:
raise TypeError("cannot replace the handler with one with a different type")
self.__dict__['handler'] = handler
handler = property(_get_handler, _set_handler)
del _get_handler, _set_handler
@classmethod
def new_from_sdp(cls, account, remote_sdp, stream_index):
remote_stream = remote_sdp.media[stream_index]
if remote_stream.media != 'application':
raise UnknownStreamError
accept_types = remote_stream.attributes.getfirst('accept-types', None)
if accept_types is None or 'application/x-rfb' not in accept_types.split():
raise UnknownStreamError
expected_transport = 'TCP/TLS/MSRP' if account.msrp.transport=='tls' else 'TCP/MSRP'
if remote_stream.transport != expected_transport:
raise InvalidStreamError("expected %s transport in chat stream, got %s" % (expected_transport, remote_stream.transport))
if remote_stream.formats != ['*']:
raise InvalidStreamError("wrong format list specified")
remote_rfbsetup = remote_stream.attributes.getfirst('rfbsetup', 'active')
if remote_rfbsetup == 'active':
stream = cls(account, handler=InternalVNCServerHandler())
elif remote_rfbsetup == 'passive':
stream = cls(account, handler=InternalVNCViewerHandler())
else:
raise InvalidStreamError("unknown rfbsetup attribute in the remote desktop sharing stream")
stream.remote_role = remote_stream.attributes.getfirst('setup', 'active')
return stream
def initialize(self, session, direction):
NotificationCenter().add_observer(self, sender=self.handler)
self.handler.initialize(self)
MSRPStreamBase.initialize(self, session, direction)
def _create_local_media(self, uri_path):
local_media = MSRPStreamBase._create_local_media(self, uri_path)
local_media.attributes.append(SDPAttribute('rfbsetup', self.handler.type))
return local_media
def _msrp_reader(self):
while True:
try:
# it should be read_chunk(0) to read as much as available, but it doesn't work
# as it sends 1-2 bytes more than provided by the app to the other side. -Dan
chunk = self.msrp.read_chunk(None) # 0 means to return as much data as was read
if chunk.method in (None, 'REPORT'):
continue
elif chunk.method == 'SEND':
if chunk.content_type in self.accept_types:
self.incoming_queue.send(chunk.data)
response = make_response(chunk, 200, 'OK')
report = make_report(chunk, 200, 'OK')
else:
response = make_response(chunk, 415, 'Invalid Content-Type')
report = None
else:
response = make_response(chunk, 501, 'Unknown method')
report = None
if response is not None:
self.msrp.write_chunk(response)
if report is not None:
self.msrp.write_chunk(response)
except ProcExit:
raise
except Exception, e:
self.msrp_reader_thread = None # avoid issues caused by the notification handler killing this greenlet during post_notification
if self.shutting_down and isinstance(e, ConnectionDone):
break
ndata = TimestampedNotificationData(context='reading', failure=Failure(), reason=str(e))
NotificationCenter().post_notification('MediaStreamDidFail', self, ndata)
break
def _msrp_writer(self):
while True:
try:
data = self.outgoing_queue.wait()
chunk = self.msrp.make_chunk(data=data)
chunk.add_header(SuccessReportHeader('no'))
chunk.add_header(FailureReportHeader('partial'))
chunk.add_header(ContentTypeHeader('application/x-rfb'))
self.msrp.write_chunk(chunk)
except ProcExit:
raise
except Exception, e:
self.msrp_writer_thread = None # avoid issues caused by the notification handler killing this greenlet during post_notification
if self.shutting_down and isinstance(e, ConnectionDone):
break
ndata = TimestampedNotificationData(context='sending', failure=Failure(), reason=str(e))
NotificationCenter().post_notification('MediaStreamDidFail', self, ndata)
break
def _NH_MediaStreamDidStart(self, notification):
self.msrp_reader_thread = spawn(self._msrp_reader)
self.msrp_writer_thread = spawn(self._msrp_writer)
def _NH_MediaStreamWillEnd(self, notification):
NotificationCenter().remove_observer(self, sender=self.handler)
if self.msrp_reader_thread is not None:
self.msrp_reader_thread.kill()
self.msrp_reader_thread = None
if self.msrp_writer_thread is not None:
self.msrp_writer_thread.kill()
self.msrp_writer_thread = None
def _NH_DesktopSharingHandlerDidFail(self, notification):
NotificationCenter().post_notification('MediaStreamDidFail', self, notification.data)
# temporary solution. to be replaced later by a better logging system in msrplib -Dan
class NotificationProxyLogger(object):
def __init__(self):
from application import log
self.level = log.level
self.stripped_data_transactions = set()
self.text_transactions = set()
self.transaction_data = {}
def report_out(self, data, transport, new_chunk=True):
pass
def report_in(self, data, transport, new_chunk=False, packet_done=False):
pass
def received_new_chunk(self, data, transport, chunk):
content_type = chunk.content_type.split('/')[0].lower() if chunk.content_type else None
if chunk.method != 'SEND' or (chunk.content_type and content_type in ('text', 'message')):
self.text_transactions.add(chunk.transaction_id)
self.transaction_data[chunk.transaction_id] = data
def received_chunk_data(self, data, transport, transaction_id):
if transaction_id in self.text_transactions:
self.transaction_data[transaction_id] += data
elif transaction_id not in self.stripped_data_transactions:
self.transaction_data[transaction_id] += '<stripped data>'
self.stripped_data_transactions.add(transaction_id)
def received_chunk_end(self, data, transport, transaction_id):
chunk = self.transaction_data.pop(transaction_id) + data
self.stripped_data_transactions.discard(transaction_id)
self.text_transactions.discard(transaction_id)
NotificationCenter().post_notification('MSRPTransportTrace', sender=transport, data=TimestampedNotificationData(direction='incoming', data=chunk))
def sent_new_chunk(self, data, transport, chunk):
content_type = chunk.content_type.split('/')[0].lower() if chunk.content_type else None
if chunk.method != 'SEND' or (chunk.content_type and content_type in ('text', 'message')):
self.text_transactions.add(chunk.transaction_id)
self.transaction_data[chunk.transaction_id] = data
def sent_chunk_data(self, data, transport, transaction_id):
if transaction_id in self.text_transactions:
self.transaction_data[transaction_id] += data
elif transaction_id not in self.stripped_data_transactions:
self.transaction_data[transaction_id] += '<stripped data>'
self.stripped_data_transactions.add(transaction_id)
def sent_chunk_end(self, data, transport, transaction_id):
chunk = self.transaction_data.pop(transaction_id) + data
self.stripped_data_transactions.discard(transaction_id)
self.text_transactions.discard(transaction_id)
NotificationCenter().post_notification('MSRPTransportTrace', sender=transport, data=TimestampedNotificationData(direction='outgoing', data=chunk))
def debug(self, message, **context):
pass
def info(self, message, **context):
NotificationCenter().post_notification('MSRPLibraryLog', data=TimestampedNotificationData(message=message, level=self.level.INFO))
msg = info
def warn(self, message, **context):
NotificationCenter().post_notification('MSRPLibraryLog', data=TimestampedNotificationData(message=message, level=self.level.WARNING))
def error(self, message, **context):
NotificationCenter().post_notification('MSRPLibraryLog', data=TimestampedNotificationData(message=message, level=self.level.ERROR))
err = error
def fatal(self, message, **context):
NotificationCenter().post_notification('MSRPLibraryLog', data=TimestampedNotificationData(message=message, level=self.level.CRITICAL))

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 23, 6:27 AM (1 d, 14 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3408939
Default Alt Text
(539 KB)

Event Timeline