Statistics
| Revision:

root / sip-subscribe-presence @ 151

History | View | Download

1
#!/usr/bin/env python
2
# Copyright (C) 2008-2010 AG Projects. See LICENSE for details.
3
#
4

    
5
import datetime
6
import os
7
import random
8
import select
9
import sys
10
import termios
11
import urllib
12

    
13
from collections import deque
14
from optparse import OptionParser
15
from threading import Thread
16
from time import time
17

    
18
from application import log
19
from application.notification import IObserver, NotificationCenter, NotificationData
20
from application.python.queue import EventQueue
21
from eventlet.twistedutil import join_reactor
22
from twisted.internet import reactor
23
from twisted.internet.error import ReactorNotRunning
24
from zope.interface import implements
25

    
26
from sipsimple.account import Account, AccountManager, BonjourAccount
27
from sipsimple.application import SIPApplication
28
from sipsimple.lookup import DNSLookup
29
from sipsimple.configuration import ConfigurationError, ConfigurationManager
30
from sipsimple.configuration.settings import SIPSimpleSettings
31
from sipsimple.core import ContactHeader, Engine, FromHeader, RouteHeader, SIPCoreError, SIPURI, Subscription, ToHeader, Route
32
from sipsimple.payloads import ParserError
33
from sipsimple.payloads import rpid # needed to register RPID extensions
34
from sipsimple.payloads.pidf import Device, Person, Service, PIDF, PIDFDocument
35
from sipsimple.storage import FileStorage
36
from sipsimple.threading import run_in_twisted_thread
37

    
38
from sipclient.configuration import config_directory
39
from sipclient.configuration.account import AccountExtension
40
from sipclient.configuration.settings import SIPSimpleSettingsExtension
41
from sipclient.log import Logger
42

    
43

    
44
class InputThread(Thread):
45
    def __init__(self, application):
46
        Thread.__init__(self)
47
        self.application = application
48
        self.daemon = True
49
        self._old_terminal_settings = None
50

    
51
    def run(self):
52
        notification_center = NotificationCenter()
53
        while True:
54
            for char in self._getchars():
55
                if char == "\x04":
56
                    self.application.stop()
57
                    return
58
                else:
59
                    notification_center.post_notification('SAInputWasReceived', sender=self, data=NotificationData(input=char))
60

    
61
    def stop(self):
62
        self._termios_restore()
63

    
64
    def _termios_restore(self):
65
        if self._old_terminal_settings is not None:
66
            termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings)
67

    
68
    def _getchars(self):
69
        fd = sys.stdin.fileno()
70
        if os.isatty(fd):
71
            self._old_terminal_settings = termios.tcgetattr(fd)
72
            new = termios.tcgetattr(fd)
73
            new[3] = new[3] & ~termios.ICANON & ~termios.ECHO
74
            new[6][termios.VMIN] = '\000'
75
            try:
76
                termios.tcsetattr(fd, termios.TCSADRAIN, new)
77
                if select.select([fd], [], [], None)[0]:
78
                    return sys.stdin.read(4192)
79
            finally:
80
                self._termios_restore()
81
        else:
82
            return os.read(fd, 4192)
83

    
84

    
85
class SubscriptionApplication(object):
86
    implements(IObserver)
87

    
88
    def __init__(self, account_name, target, trace_sip, trace_pjsip, trace_notifications):
89
        self.account_name = account_name
90
        self.target = target
91
        self.input = InputThread(self)
92
        self.output = EventQueue(self._write)
93
        self.logger = Logger(sip_to_stdout=trace_sip, pjsip_to_stdout=trace_pjsip, notifications_to_stdout=trace_notifications)
94
        self.success = False
95
        self.account = None
96
        self.subscription = None
97
        self.stopping = False
98

    
99
        self._subscription_routes = None
100
        self._subscription_timeout = 0.0
101
        self._subscription_wait = 0.5
102

    
103
        account_manager = AccountManager()
104
        engine = Engine()
105
        notification_center = NotificationCenter()
106
        notification_center.add_observer(self, sender=account_manager)
107
        notification_center.add_observer(self, sender=engine)
108
        notification_center.add_observer(self, sender=self.input)
109

    
110
        log.level.current = log.level.WARNING
111

    
112
    def _write(self, message):
113
        if isinstance(message, unicode):
114
            message = message.encode(sys.getfilesystemencoding())
115
        sys.stdout.write(message+'\n')
116

    
117
    def run(self):
118
        account_manager = AccountManager()
119
        configuration = ConfigurationManager()
120
        engine = Engine()
121

    
122
        # start output thread
123
        self.output.start()
124

    
125
        # startup configuration
126
        Account.register_extension(AccountExtension)
127
        BonjourAccount.register_extension(AccountExtension)
128
        SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension)
129
        SIPApplication.storage = FileStorage(config_directory)
130
        try:
131
            configuration.start()
132
        except ConfigurationError, e:
133
            raise RuntimeError("failed to load sipclient's configuration: %s\nIf an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script." % str(e))
134
        account_manager.load()
135
        if self.account_name is None:
136
            self.account = account_manager.default_account
137
        else:
138
            possible_accounts = [account for account in account_manager.iter_accounts() if self.account_name in account.id and account.enabled]
139
            if len(possible_accounts) > 1:
140
                raise RuntimeError("More than one account exists which matches %s: %s" % (self.account_name, ", ".join(sorted(account.id for account in possible_accounts))))
141
            if len(possible_accounts) == 0:
142
                raise RuntimeError("No enabled account that matches %s was found. Available and enabled accounts: %s" % (self.account_name, ", ".join(sorted(account.id for account in account_manager.get_accounts() if account.enabled))))
143
            self.account = possible_accounts[0]
144
        if self.account is None:
145
            raise RuntimeError("Unknown account %s. Available accounts: %s" % (self.account_name, ', '.join(account.id for account in account_manager.iter_accounts())))
146
        elif self.account == BonjourAccount():
147
            raise RuntimeError("Cannot use bonjour account for presence subscription")
148
        elif not self.account.presence.enabled:
149
            raise RuntimeError("Presence is not enabled for account %s" % self.account.id)
150
        for account in account_manager.iter_accounts():
151
            if account == self.account:
152
                account.sip.register = False
153
            else:
154
                account.enabled = False
155
        self.output.put('Using account %s' % self.account.id)
156
        settings = SIPSimpleSettings()
157

    
158
        # start logging
159
        self.logger.start()
160

    
161
        # start the engine
162
        engine.start(
163
            auto_sound=False,
164
            events={'presence': [PIDFDocument.content_type]},
165
            udp_port=settings.sip.udp_port if "udp" in settings.sip.transport_list else None,
166
            tcp_port=settings.sip.tcp_port if "tcp" in settings.sip.transport_list else None,
167
            tls_port=settings.sip.tls_port if "tls" in settings.sip.transport_list else None,
168
            tls_protocol=settings.tls.protocol,
169
            tls_verify_server=self.account.tls.verify_server,
170
            tls_ca_file=os.path.expanduser(settings.tls.ca_list) if settings.tls.ca_list else None,
171
            tls_cert_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None,
172
            tls_privkey_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None,
173
            tls_timeout=settings.tls.timeout,
174
            ec_tail_length=settings.audio.tail_length,
175
            user_agent=settings.user_agent,
176
            sample_rate=settings.audio.sample_rate,
177
            rtp_port_range=(settings.rtp.port_range.start, settings.rtp.port_range.end),
178
            trace_sip=settings.logs.trace_sip or self.logger.sip_to_stdout,
179
            log_level=settings.logs.pjsip_level if (settings.logs.trace_pjsip or self.logger.pjsip_to_stdout) else 0
180
        )
181

    
182
        if self.target is None:
183
            self.target = ToHeader(SIPURI(user=self.account.id.username, host=self.account.id.domain))
184
        else:
185
            if '@' not in self.target:
186
                self.target = '%s@%s' % (self.target, self.account.id.domain)
187
            if not self.target.startswith('sip:') and not self.target.startswith('sips:'):
188
                self.target = 'sip:' + self.target
189
            try:
190
                self.target = ToHeader(SIPURI.parse(self.target))
191
            except SIPCoreError:
192
                self.output.put('Illegal SIP URI: %s' % self.target)
193
                engine.stop()
194
                return 1
195
        self.output.put('Subscribing to %s for the presence event' % self.target.uri)
196

    
197
        # start the input thread
198
        self.input.start()
199

    
200
        reactor.callLater(0, self._subscribe)
201

    
202
        # start twisted
203
        try:
204
            reactor.run()
205
        finally:
206
            self.input.stop()
207

    
208
        # stop the output
209
        self.output.stop()
210
        self.output.join()
211

    
212
        self.logger.stop()
213

    
214
        return 0 if self.success else 1
215

    
216
    def stop(self):
217
        self.stopping = True
218
        if self.subscription is not None and self.subscription.state.lower() in ('accepted', 'pending', 'active'):
219
            self.subscription.end(timeout=1)
220
        else:
221
            engine = Engine()
222
            engine.stop()
223

    
224
    def print_help(self):
225
        message  = 'Available control keys:\n'
226
        message += '  t: toggle SIP trace on the console\n'
227
        message += '  j: toggle PJSIP trace on the console\n'
228
        message += '  n: toggle notifications trace on the console\n'
229
        message += '  Ctrl-d: quit the program\n'
230
        message += '  ?: display this help message\n'
231
        self.output.put('\n'+message)
232

    
233
    def handle_notification(self, notification):
234
        handler = getattr(self, '_NH_%s' % notification.name, None)
235
        if handler is not None:
236
            handler(notification)
237

    
238
    def _NH_SIPSubscriptionDidStart(self, notification):
239
        route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
240
        self._subscription_routes = None
241
        self._subscription_wait = 0.5
242
        self.output.put('Subscription succeeded at %s:%d;transport=%s' % (route.address, route.port, route.transport))
243
        self.success = True
244

    
245
    def _NH_SIPSubscriptionChangedState(self, notification):
246
        route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
247
        if notification.data.state.lower() == "pending":
248
            self.output.put('Subscription pending at %s:%d;transport=%s' % (route.address, route.port, route.transport))
249
        elif notification.data.state.lower() == "active":
250
            self.output.put('Subscription active at %s:%d;transport=%s' % (route.address, route.port, route.transport))
251

    
252
    def _NH_SIPSubscriptionDidEnd(self, notification):
253
        notification_center = NotificationCenter()
254
        notification_center.remove_observer(self, sender=notification.sender)
255
        self.subscription = None
256
        route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
257
        self.output.put('Unsubscribed from %s:%d;transport=%s' % (route.address, route.port, route.transport))
258
        self.stop()
259

    
260
    def _NH_SIPSubscriptionDidFail(self, notification):
261
        notification_center = NotificationCenter()
262
        notification_center.remove_observer(self, sender=notification.sender)
263
        self.subscription = None
264
        route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp'))
265
        if notification.data.code:
266
            status = ': %d %s' % (notification.data.code, notification.data.reason)
267
        else:
268
            status = ': %s' % notification.data.reason
269
        self.output.put('Subscription failed at %s:%d;transport=%s%s' % (route.address, route.port, route.transport, status))
270
        if self.stopping or notification.data.code in (401, 403, 407) or self.success:
271
            self.success = False
272
            self.stop()
273
        else:
274
            if not self._subscription_routes or time() > self._subscription_timeout:
275
                self._subscription_wait = min(self._subscription_wait*2, 30)
276
                timeout = random.uniform(self._subscription_wait, 2*self._subscription_wait)
277
                reactor.callFromThread(reactor.callLater, timeout, self._subscribe)
278
            else:
279
                route = self._subscription_routes.popleft()
280
                route_header = RouteHeader(route.get_uri())
281
                self.subscription = Subscription(self.target.uri,
282
                                                 FromHeader(self.account.uri, self.account.display_name),
283
                                                 self.target,
284
                                                 ContactHeader(self.account.contact[route]),
285
                                                 "presence",
286
                                                 route_header,
287
                                                 credentials=self.account.credentials,
288
                                                 refresh=self.account.sip.subscribe_interval)
289
                notification_center.add_observer(self, sender=self.subscription)
290
                self.subscription.subscribe(timeout=5)
291

    
292
    def _NH_SIPSubscriptionGotNotify(self, notification):
293
        if notification.data.content_type == PIDFDocument.content_type:
294
            self.output.put('Received NOTIFY:')
295
            try:
296
                pidf = PIDF.parse(notification.data.body)
297
            except ParserError, e:
298
                self.output.put('Got illegal PIDF document: %s\n%s' % (str(e), notification.data.body))
299
            else:
300
                self._display_pidf(pidf)
301
            self.print_help()
302

    
303
    def _NH_DNSLookupDidSucceed(self, notification):
304
        # create subscription and register to get notifications from it
305
        self._subscription_routes = deque(notification.data.result)
306
        route = self._subscription_routes.popleft()
307
        route_header = RouteHeader(route.uri)
308
        self.subscription = Subscription(self.target.uri,
309
                                         FromHeader(self.account.uri, self.account.display_name),
310
                                         self.target,
311
                                         ContactHeader(self.account.contact[route]),
312
                                         "presence",
313
                                         route_header,
314
                                         credentials=self.account.credentials,
315
                                         refresh=self.account.sip.subscribe_interval)
316
        notification_center = NotificationCenter()
317
        notification_center.add_observer(self, sender=self.subscription)
318
        self.subscription.subscribe(timeout=5)
319

    
320
    def _NH_DNSLookupDidFail(self, notification):
321
        self.output.put('DNS lookup failed: %s' % notification.data.error)
322
        timeout = random.uniform(1.0, 2.0)
323
        reactor.callLater(timeout, self._subscribe)
324

    
325
    def _NH_SAInputWasReceived(self, notification):
326
        engine = Engine()
327
        settings = SIPSimpleSettings()
328
        key = notification.data.input
329
        if key == 't':
330
            self.logger.sip_to_stdout = not self.logger.sip_to_stdout
331
            engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip
332
            self.output.put('SIP tracing to console is now %s.' % ('activated' if self.logger.sip_to_stdout else 'deactivated'))
333
        elif key == 'j':
334
            self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout
335
            engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0
336
            self.output.put('PJSIP tracing to console is now %s.' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated'))
337
        elif key == 'n':
338
            self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout
339
            self.output.put('Notification tracing to console is now %s.' % ('activated' if self.logger.notifications_to_stdout else 'deactivated'))
340
        elif key == '?':
341
            self.print_help()
342

    
343
    @run_in_twisted_thread
344
    def _NH_SIPEngineDidEnd(self, notification):
345
        self._stop_reactor()
346

    
347
    @run_in_twisted_thread
348
    def _NH_SIPEngineDidFail(self, notification):
349
        self.output.put('Engine failed.')
350
        self._stop_reactor()
351

    
352
    def _NH_SIPEngineGotException(self, notification):
353
        self.output.put('An exception occured within the SIP core:\n'+notification.data.traceback)
354

    
355
    def _stop_reactor(self):
356
        try:
357
            reactor.stop()
358
        except ReactorNotRunning:
359
            pass
360

    
361
    def _subscribe(self):
362
        settings = SIPSimpleSettings()
363

    
364
        self._subscription_timeout = time()+30
365

    
366
        lookup = DNSLookup()
367
        notification_center = NotificationCenter()
368
        notification_center.add_observer(self, sender=lookup)
369
        if self.account.sip.outbound_proxy is not None:
370
            uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport})
371
        elif self.account.sip.always_use_my_proxy:
372
            uri = SIPURI(host=self.account.id.domain)
373
        else:
374
            uri = self.target.uri
375
        lookup.lookup_sip_proxy(uri, settings.sip.transport_list)
376

    
377
    def _format_note(self, note):
378
        text = "Note"
379
        if note.lang is not None:
380
            text += "(%s)" % note.lang
381
        text += ": %s" % note
382
        return text
383

    
384
    def _format_person(self, person, pidf):
385
        buf = []
386
        # display class
387
        if person.rpid_class is not None:
388
            buf.append("    Class: %s" % person.rpid_class)
389
        # display timestamp
390
        if person.timestamp is not None:
391
            buf.append("    Timestamp: %s" % person.timestamp)
392
        # display notes
393
        if person.notes:
394
            for note in person.notes:
395
                buf.append("    %s" % self._format_note(note))
396
        elif pidf.notes:
397
            for note in pidf.notes:
398
                buf.append("    %s" % self._format_note(note))
399
        # display activities
400
        if person.activities is not None:
401
            activities = list(person.activities)
402
            if len(activities) > 0:
403
                text = "    Activities"
404
                if person.activities.since is not None or person.activities.until is not None:
405
                    text += " valid"
406
                    if person.activities.since is not None:
407
                        text += " from %s" % person.activities.since
408
                    if person.activities.until is not None:
409
                        text += " until %s" % person.activities.until
410
                text += ": %s" % ', '.join(str(activity) for activity in activities)
411
                buf.append(text)
412
                if len(person.activities.notes) > 0:
413
                    for note in person.activities.notes:
414
                        buf.append("      %s" % self._format_note(note))
415
            elif len(person.activities.notes) > 0:
416
                buf.append("    Activities")
417
                for note in person.activities.notes:
418
                    buf.append("      %s" % self._format_note(note))
419
        # display mood
420
        if person.mood is not None:
421
            moods = list(person.mood)
422
            if len(moods) > 0:
423
                text = "    Mood"
424
                if person.mood.since is not None or person.mood.until is not None:
425
                    text += " valid"
426
                    if person.mood.since is not None:
427
                        text += " from %s" % person.mood.since
428
                    if person.mood.until is not None:
429
                        text += " until %s" % person.mood.until
430
                text += ": %s" % ', '.join(str(mood) for mood in moods)
431
                buf.append(text)
432
                if len(person.mood.notes) > 0:
433
                    for note in person.mood.notes:
434
                        buf.append("      %s" % self._format_note(note))
435
        # display place is
436
        if person.place_is is not None:
437
            place_info = ', '.join('%s %s' % (key.capitalize(), getattr(person.place_is, key).value) for key in ('audio', 'video', 'text') if getattr(person.place_is, key) and getattr(person.place_is, key).value)
438
            if place_info != '':
439
                buf.append("    Place information: " + place_info)
440
        # display privacy
441
        if person.privacy is not None:
442
            text = "    Private conversation possible with: "
443
            private = []
444
            if person.privacy.audio:
445
                private.append("Audio")
446
            if person.privacy.video:
447
                private.append("Video")
448
            if person.privacy.text:
449
                private.append("Text")
450
            if len(private) > 0:
451
                text += ", ".join(private)
452
            else:
453
                text += "None"
454
            buf.append(text)
455
        # display sphere
456
        if person.sphere is not None:
457
            timeinfo = []
458
            if person.sphere.since is not None:
459
                timeinfo.append('from %s' % str(person.sphere.since))
460
            if person.sphere.until is not None:
461
                timeinfo.append('until %s' % str(person.sphere.until))
462
            if len(timeinfo) != 0:
463
                timeinfo = ' (' + ', '.join(timeinfo) + ')'
464
            else:
465
                timeinfo = ''
466
            buf.append("    Current sphere%s: %s" % (timeinfo, person.sphere.value))
467
        # display status icon
468
        if person.status_icon is not None:
469
            buf.append("    Status icon: %s" % person.status_icon)
470
        # display time and time offset
471
        if person.time_offset is not None:
472
            ctime = datetime.datetime.utcnow() + datetime.timedelta(minutes=int(person.time_offset))
473
            time_offset = int(person.time_offset)/60.0
474
            if time_offset == int(time_offset):
475
                offset_info = '(UTC+%d%s)' % (time_offset, (person.time_offset.description is not None and (' (%s)' % person.time_offset.description) or ''))
476
            else:
477
                offset_info = '(UTC+%.1f%s)' % (time_offset, (person.time_offset.description is not None and (' (%s)' % person.time_offset.description) or ''))
478
            buf.append("    Current user time: %s %s" % (ctime.strftime("%H:%M"), offset_info))
479
        # display user input
480
        if person.user_input is not None:
481
            buf.append("    User is %s" % person.user_input)
482
            if person.user_input.last_input:
483
                buf.append("      Last input at: %s" % person.user_input.last_input)
484
            if person.user_input.idle_threshold:
485
                buf.append("      Idle threshold: %s seconds" % person.user_input.idle_threshold)
486
        return buf
487

    
488
    def _format_service(self, service, pidf):
489
        buf = []
490
        # display class
491
        if service.rpid_class is not None:
492
            buf.append("    Class: %s" % service.rpid_class)
493
        # display timestamp
494
        if service.timestamp is not None:
495
            buf.append("    Timestamp: %s" % service.timestamp)
496
        # display notes
497
        for note in service.notes:
498
            buf.append("    %s" % self._format_note(note))
499
        # display status
500
        if service.status is not None:
501
            if service.status.basic is not None:
502
                buf.append("    Basic status: %s" % service.status.basic)
503
            if service.status.extended is not None:
504
                buf.append("    Extended status: %s" % service.status.extended)
505
        # display contact
506
        if service.contact is not None:
507
            buf.append("    Contact%s: %s" % ((service.contact.priority is not None) and (' priority %s' % service.contact.priority) or '', urllib.unquote(service.contact.value)))
508
        # display device ID
509
        if service.device_info is not None:
510
            description = " (%s)" % urllib.unquote(service.device_info.description).decode('utf-8') if service.device_info.description else ""
511
            buf.append("    Service offered by device: %s%s" % (service.device_info.id, description))
512
        # display relationship
513
        if service.relationship is not None:
514
            buf.append("    Relationship: %s" % service.relationship.value)
515
        # display service-class
516
        if service.service_class is not None:
517
            buf.append("    Service class: %s" % service.service_class.value)
518
        # display status icon
519
        if service.status_icon is not None:
520
            buf.append("    Status icon: %s" % service.status_icon)
521
        # display user input
522
        if service.user_input is not None:
523
            buf.append("    Service is %s" % service.user_input)
524
            if service.user_input.last_input:
525
                buf.append("      Last input at: %s" % service.user_input.last_input)
526
            if service.user_input.idle_threshold:
527
                buf.append("      Idle threshold: %s seconds" % service.user_input.idle_threshold)
528
        return buf
529

    
530
    def _format_device(self, device, pidf):
531
        buf = []
532
        # display device ID
533
        if device.device_id is not None:
534
            buf.append("    Device id: %s" % device.device_id)
535
        # display class
536
        if device.rpid_class is not None:
537
            buf.append("    Class: %s" % device.rpid_class)
538
        # display timestamp
539
        if device.timestamp is not None:
540
            buf.append("    Timestamp: %s" % device.timestamp)
541
        # display notes
542
        for note in device.notes:
543
            buf.append("    %s" % self._format_note(note))
544
        # display user input
545
        if device.user_input is not None:
546
            buf.append("    Device is %s" % device.user_input)
547
            if device.user_input.last_input:
548
                buf.append("      Last input at: %s" % device.user_input.last_input)
549
            if device.user_input.idle_threshold:
550
                buf.append("      Idle threshold: %s seconds" % device.user_input.idle_threshold)
551
        return buf
552

    
553
    def _display_pidf(self, pidf):
554
        buf = ["-"*16]
555
        buf.append("Presence for %s:" % urllib.unquote(pidf.entity))
556
        persons = {}
557
        devices = {}
558
        services = {}
559
        printed_sep = True
560
        for child in pidf:
561
            if isinstance(child, Person):
562
                persons[child.id] = child
563
            elif isinstance(child, Device):
564
                devices[child.id] = child
565
            elif isinstance(child, Service):
566
                services[child.id] = child
567

    
568
        # handle person information
569
        if len(persons) == 0:
570
            if list(pidf.notes):
571
                buf.append("  Person information:")
572
                for note in pidf.notes:
573
                    buf.append("    %s" % self._format_note(note))
574
                printed_sep = False
575
        else:
576
            for person in persons.values():
577
                buf.append("  Person: %s" % person.id)
578
                buf.extend(self._format_person(person, pidf))
579
            printed_sep = False
580

    
581

    
582
        # handle services informaation
583
        if len(services) > 0:
584
            if not printed_sep:
585
                buf.append("  " + "-"*3)
586
            for service in services.values():
587
                buf.append("  Service: %s" % service.id)
588
                buf.extend(self._format_service(service, pidf))
589

    
590
        # handle devices informaation
591
        if len(devices) > 0:
592
            if not printed_sep:
593
                buf.append("  " + "-"*3)
594
            for device in devices.values():
595
                buf.append("  Device: %s" % device.id)
596
                buf.extend(self._format_device(device, pidf))
597

    
598
        buf.append("-"*16)
599

    
600
        # push the data
601
        self.output.put('\n'.join(buf))
602

    
603

    
604
if __name__ == "__main__":
605
    description = "This script subscribes to the presence event package published by the specified SIP target. If a SIP target is not specified, it will subscribe to its own address. It will then interprete PIDF bodies contained in NOTIFYs and display their meaning. The program will un-SUBSCRIBE and quit when CTRL+D is pressed."
606
    usage = "%prog [options] [target-user@target-domain.com]"
607
    parser = OptionParser(usage=usage, description=description)
608
    parser.print_usage = parser.print_help
609
    parser.add_option("-a", "--account-name", type="string", dest="account_name", help="The name of the account to use.")
610
    parser.add_option("-s", "--trace-sip", action="store_true", dest="trace_sip", default=False, help="Dump the raw contents of incoming and outgoing SIP messages (disabled by default).")
611
    parser.add_option("-j", "--trace-pjsip", action="store_true", dest="trace_pjsip", default=False, help="Print PJSIP logging output (disabled by default).")
612
    parser.add_option("-n", "--trace-notifications", action="store_true", dest="trace_notifications", default=False, help="Print all notifications (disabled by default).")
613
    options, args = parser.parse_args()
614

    
615
    try:
616
        application = SubscriptionApplication(options.account_name, args[0] if args else None, options.trace_sip, options.trace_pjsip, options.trace_notifications)
617
        return_code = application.run()
618
    except RuntimeError, e:
619
        print "Error: %s" % str(e)
620
        sys.exit(1)
621
    except SIPCoreError, e:
622
        print "Error: %s" % str(e)
623
        sys.exit(1)
624
    else:
625
        sys.exit(return_code)
626

    
627