history.py 25.4 KB
Newer Older
1

2
import bisect
Adrian Georgescu's avatar
Adrian Georgescu committed
3
import pickle as pickle
4 5
import re

Dan Pascu's avatar
Dan Pascu committed
6
from PyQt5.QtGui import QIcon
7

8
from application.notification import IObserver, NotificationCenter, NotificationData
9 10
from application.python import Null
from application.python.types import Singleton
11
from application.system import makedirs
12

13
from datetime import date, timezone
14
from dateutil.parser import parse
15
from dateutil.tz import tzlocal
16
from zope.interface import implementer
17 18 19 20

from sipsimple.account import BonjourAccount
from sipsimple.addressbook import AddressbookManager
from sipsimple.threading import run_in_thread
21
from sipsimple.util import ISOTimestamp
22

23
from blink.configuration.settings import BlinkSettings
24
from blink.logging import MessagingTrace as log
25
from blink.messages import BlinkMessage
26
from blink.resources import ApplicationData, Resources
27
from blink.util import run_in_gui_thread, translate
Adrian Georgescu's avatar
Adrian Georgescu committed
28
import traceback
29

Tijmen de Mes's avatar
Tijmen de Mes committed
30
from sqlobject import SQLObject, StringCol, DateTimeCol, IntCol, UnicodeCol, DatabaseIndex
31 32
from sqlobject import connectionForURI
from sqlobject import dberrors
33

34 35 36
__all__ = ['HistoryManager']


37
@implementer(IObserver)
Adrian Georgescu's avatar
Adrian Georgescu committed
38
class HistoryManager(object, metaclass=Singleton):
39

40
    history_size = 20
41
    sip_prefix_re = re.compile('^sips?:')
42

43
    def __init__(self):
44
        self.calls = []
45 46
        self.message_history = MessageHistory()

47
        notification_center = NotificationCenter()
48
        notification_center.add_observer(self, name='SIPApplicationDidStart')
49 50
        notification_center.add_observer(self, name='SIPSessionDidEnd')
        notification_center.add_observer(self, name='SIPSessionDidFail')
51 52 53 54 55 56 57 58 59
        notification_center.add_observer(self, name='ChatStreamGotMessage')
        notification_center.add_observer(self, name='ChatStreamWillSendMessage')
        notification_center.add_observer(self, name='ChatStreamDidSendMessage')
        notification_center.add_observer(self, name='ChatStreamDidDeliverMessage')
        notification_center.add_observer(self, name='ChatStreamDidNotDeliverMessage')
        notification_center.add_observer(self, name='BlinkMessageIsParsed')
        notification_center.add_observer(self, name='BlinkMessageIsPending')
        notification_center.add_observer(self, name='BlinkMessageDidSucceed')
        notification_center.add_observer(self, name='BlinkMessageDidFail')
60 61
        notification_center.add_observer(self, name='BlinkMessageDidEncrypt')
        notification_center.add_observer(self, name='BlinkMessageDidDecrypt')
62 63
        notification_center.add_observer(self, name='BlinkGotDispositionNotification')
        notification_center.add_observer(self, name='BlinkDidSendDispositionNotification')
64
        notification_center.add_observer(self, name='BlinkGotHistoryMessage')
65 66
        notification_center.add_observer(self, name='BlinkGotHistoryMessageRemove')
        notification_center.add_observer(self, name='BlinkGotHistoryConversationRemove')
67 68 69

    @run_in_thread('file-io')
    def save(self):
70 71
        with open(ApplicationData.get('calls_history'), 'wb+') as history_file:
            pickle.dump(self.calls, history_file)
72

73 74 75
    def load(self, uri, session):
        return self.message_history.load(uri, session)

76 77 78
    def get_last_contacts(self, number=5):
        return self.message_history.get_last_contacts(number)

79 80 81 82 83
    @run_in_gui_thread
    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

84 85 86 87 88
    def _NH_SIPApplicationDidStart(self, notification):
        try:
            data = pickle.load(open(ApplicationData.get('calls_history'), "rb"))
            if not isinstance(data, list) or not all(isinstance(item, HistoryEntry) and item.text and isinstance(item.call_time, ISOTimestamp) for item in data):
                raise ValueError("invalid save data")
89 90
        except FileNotFoundError:
            pass
91 92 93 94 95
        except Exception as e:
            traceback.print_exc()
        else:
            self.calls = data[-self.history_size:]

96 97 98 99 100
    def _NH_SIPSessionDidEnd(self, notification):
        if notification.sender.account is BonjourAccount():
            return
        session = notification.sender
        entry = HistoryEntry.from_session(session)
101 102
        bisect.insort(self.calls, entry)
        self.calls = self.calls[-self.history_size:]
103 104 105 106 107 108 109
        self.save()

    def _NH_SIPSessionDidFail(self, notification):
        if notification.sender.account is BonjourAccount():
            return
        session = notification.sender
        entry = HistoryEntry.from_session(session)
Tijmen de Mes's avatar
Tijmen de Mes committed
110

111
        if session.direction == 'incoming':
112 113
            if notification.data.code != 487 or notification.data.failure_reason != 'Call completed elsewhere':
                entry.failed = True
114
        else:
115 116 117 118
            if notification.data.code == 0:
                entry.reason = 'Internal Error'
            elif notification.data.code == 487:
                entry.reason = 'Cancelled'
119
            else:
120
                entry.reason = notification.data.reason or notification.data.failure_reason
121 122 123
            entry.failed = True
        bisect.insort(self.calls, entry)
        self.calls = self.calls[-self.history_size:]
124 125
        self.save()

126 127 128
    def _NH_ChatStreamGotMessage(self, notification):
        message = notification.data.message

Tijmen de Mes's avatar
Tijmen de Mes committed
129
        if notification.sender.blink_session.remote_focus and self.sip_prefix_re.sub('', str(message.sender.uri)) not in notification.sender.blink_session.server_conference.participants:
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
            return

        is_status_message = any(h.name == 'Message-Type' and h.value == 'status' and h.namespace == 'urn:ag-projects:xml:ns:cpim' for h in message.additional_headers)
        if not is_status_message:
            blink_message = BlinkMessage(**{slot: getattr(message, slot) for slot in message.__slots__})
            self.message_history.add_with_session(notification.sender.blink_session, blink_message, 'incoming', 'delivered')

    def _NH_ChatStreamWillSendMessage(self, notification):
        self.message_history.add_with_session(notification.sender, notification.data, 'outgoing')

    def _NH_ChatStreamDidSendMessage(self, notification):
        self.message_history.update(notification.data.message.message_id, 'accepted')

    def _NH_ChatStreamDidDeliverMessage(self, notification):
        self.message_history.update(notification.data.message.message_id, 'delivered')

    def _NH_ChatStreamDidNotDeliverMessage(self, notification):
        self.message_history.update(notification.data.message.message_id, 'failed')

    def _NH_BlinkMessageIsParsed(self, notification):
        session = notification.sender
        message = notification.data

        self.message_history.add_with_session(session, message, 'incoming')

    def _NH_BlinkMessageIsPending(self, notification):
        session = notification.sender
        data = notification.data

        self.message_history.add_with_session(session, data.message, 'outgoing')

161 162 163 164
    def _NH_BlinkGotHistoryMessage(self, notification):
        account = notification.sender
        self.message_history.add_from_history(account, **notification.data.__dict__)

165 166 167 168 169 170
    def _NH_BlinkGotHistoryMessageRemove(self, notification):
        self.message_history.remove_message(notification.data)

    def _NH_BlinkGotHistoryConversationRemove(self, notification):
        self.message_history.remove_contact_messages(notification.sender, notification.data)

171 172 173 174 175 176 177 178
    def _NH_BlinkMessageDidSucceed(self, notification):
        data = notification.data
        self.message_history.update(data.id, 'accepted')

    def _NH_BlinkMessageDidFail(self, notification):
        data = notification.data
        self.message_history.update(data.id, 'failed')

179 180 181 182 183 184
    def _NH_BlinkMessageDidDecrypt(self, notification):
        self.message_history.update_encryption(notification)

    def _NH_BlinkMessageDidEncrypt(self, notification):
        self.message_history.update_encryption(notification)

185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
    def _NH_BlinkGotDispositionNotification(self, notification):
        data = notification.data
        self.message_history.update(data.id, data.status)

    def _NH_BlinkDidSendDispositionNotification(self, notification):
        data = notification.data
        self.message_history.update(data.id, data.status)


class TableVersion(SQLObject):
    class sqlmeta:
        table = 'table_versions'
    table_name        = StringCol(alternateID=True)
    version           = IntCol()


class Message(SQLObject):
    class sqlmeta:
        table = 'messages'
    message_id      = StringCol()
    account_id      = UnicodeCol(length=128)
    remote_uri      = UnicodeCol(length=128)
    display_name    = UnicodeCol(length=128)
    uri             = UnicodeCol(length=128, default='')
    timestamp       = DateTimeCol()
    direction       = StringCol()
    content         = UnicodeCol(sqlType='LONGTEXT')
    content_type    = StringCol(default='text')
    state           = StringCol(default='pending')
    encryption_type = StringCol(default='')
    disposition     = StringCol(default='')
    remote_idx      = DatabaseIndex('remote_uri')
    id_idx          = DatabaseIndex('message_id')
218
    unq_idx         = DatabaseIndex(message_id, account_id, remote_uri, unique=True)
219

Tijmen de Mes's avatar
Tijmen de Mes committed
220

221 222 223 224 225
class TableVersions(object, metaclass=Singleton):
    __version__ = 1
    __versions__ = {}

    def __init__(self):
226
        db_file = ApplicationData.get('message_history.db')
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
        db_uri = f'sqlite://{db_file}'
        self._initialize(db_uri)

    @run_in_thread('db')
    def _initialize(self, db_uri):
        self.db = connectionForURI(db_uri)
        TableVersion._connection = self.db

        if not TableVersion.tableExists():
            try:
                TableVersion.createTable()
            except Exception as e:
                pass
            else:
                self.set_version(TableVersion.sqlmeta.table, self.__version__)
        else:
            self._load_versions()

    @run_in_thread('db')
    def _load_versions(self):
        contents = TableVersion.select()
        for table_version in list(contents):
            self.__versions__[table_version.table_name] = table_version.version

    def version(self, table):
        try:
            return self.__versions__[table]
        except KeyError:
            return None

    @run_in_thread('db')
    def set_version(self, table, version):
        try:
            TableVersion(table_name=table, version=version)
        except (dberrors.DuplicateEntryError, dberrors.IntegrityError):
            try:
                record = TableVersion.selectBy(table_name=table).getOne()
                record.version = version
            except Exception as e:
                pass
        except Exception as e:
            pass
        self.__versions__[table] = version


class MessageHistory(object, metaclass=Singleton):
273
    __version__ = 2
274 275 276
    phone_number_re = re.compile(r'^(?P<number>(0|00|\+)[1-9]\d{7,14})@')

    def __init__(self):
Tijmen de Mes's avatar
Tijmen de Mes committed
277
        db_file = ApplicationData.get('message_history.db')
278
        db_uri = f'sqlite://{db_file}'
279
        makedirs(ApplicationData.directory)
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299
        self._initialize(db_uri)

    @run_in_thread('db')
    def _initialize(self, db_uri):
        self.db = connectionForURI(db_uri)
        Message._connection = self.db
        self.table_versions = TableVersions()
        if not Message.tableExists():
            try:
                Message.createTable()
            except Exception as e:
                pass
            else:
                self.table_versions.set_version(Message.sqlmeta.table, self.__version__)
        else:
            self._check_table_version()

    def _check_table_version(self):
        db_table_version = self.table_versions.version(Message.sqlmeta.table)
        if self.__version__ != db_table_version:
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
            if db_table_version == 1:
                query = f'CREATE UNIQUE INDEX messages_msg_id ON {Message.sqlmeta.table} (message_id, account_id, remote_uri)'
                try:
                    self.db.queryAll(query)
                except (dberrors.IntegrityError, dberrors.DuplicateEntryError):
                    fix_query = f'select message_id from {Message.sqlmeta.table} group by message_id having count(id) > 1'
                    result = self.db.queryAll(fix_query)
                    for row in result:
                        messages = Message.selectBy(message_id=row[0])
                        for message in list(messages)[1:]:
                            message.destroySelf()
                    try:
                        self.db.queryAll(query)
                    except (dberrors.IntegrityError, dberrors.DuplicateEntryError):
                        pass
                    else:
                        self.table_versions.set_version(Message.sqlmeta.table, self.__version__)
                else:
                    self.table_versions.set_version(Message.sqlmeta.table, self.__version__)
319

320 321 322 323 324 325
    @classmethod
    @run_in_thread('db')
    def add_from_history(cls, account, remote_uri, message, state=None, encryption=None):
        if message.content.startswith('?OTRv'):
            return

326
        log.info(f"== Adding {message.direction} history message to storage: {message.id} {state} {remote_uri}")
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343

        match = cls.phone_number_re.match(remote_uri)
        if match:
            remote_uri = match.group('number')

        if message.direction == 'outgoing':
            display_name = message.sender.display_name
        else:
            try:
                contact = next(contact for contact in AddressbookManager().get_contacts() if remote_uri in (addr.uri for addr in contact.uris))
            except StopIteration:
                display_name = ''
            else:
                display_name = contact.name

        timestamp_native = message.timestamp
        timestamp_utc = timestamp_native.replace(tzinfo=timezone.utc)
344 345
        timestamp_fixed = timestamp_utc - message.timestamp.utcoffset()
        timestamp = parse(str(timestamp_fixed))
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372

        optional_fields = {}
        if state is not None:
            optional_fields['state'] = state

        if encryption is not None:
            optional_fields['encryption_type'] = str([f'{encryption}'])

        uri = str(message.sender.uri)
        if not uri.startswith(('sip:', 'sips:')):
            uri = f'sip:{uri}'

        try:
            Message(remote_uri=remote_uri,
                    display_name=display_name,
                    uri=uri,
                    content=message.content,
                    content_type=message.content_type,
                    message_id=message.id,
                    account_id=str(account.id),
                    direction=message.direction,
                    timestamp=timestamp,
                    disposition=str(message.disposition),
                    **optional_fields)
        except dberrors.DuplicateEntryError:
            pass

373 374 375 376 377 378
    @classmethod
    @run_in_thread('db')
    def add_with_session(cls, session, message, direction, state=None):
        if message.content.startswith('?OTRv'):
            return

379
        log.info(f"== Adding message to storage: {message.id}")
380 381 382 383 384 385 386 387 388 389 390
        user = session.uri.user
        domain = session.uri.host

        user = user.decode() if isinstance(user, bytes) else user
        domain = domain.decode() if isinstance(domain, bytes) else domain

        remote_uri = '%s@%s' % (user, domain)
        match = cls.phone_number_re.match(remote_uri)
        if match:
            remote_uri = match.group('number')

391
        if direction == 'outgoing':
392 393
            display_name = message.sender.display_name
        else:
394 395 396 397 398 399
            try:
                contact = next(contact for contact in AddressbookManager().get_contacts() if remote_uri in (addr.uri for addr in contact.uris))
            except StopIteration:
                display_name = message.sender.display_name
            else:
                display_name = contact.name
400 401 402

        timestamp_native = message.timestamp
        timestamp_utc = timestamp_native.replace(tzinfo=timezone.utc)
403 404
        timestamp_fixed = timestamp_utc - message.timestamp.utcoffset()
        timestamp = parse(str(timestamp_fixed))
405 406 407 408 409 410 411 412 413 414 415 416 417

        optional_fields = {}
        if state is not None:
            optional_fields['state'] = state
        if session.chat_type is not None:
            chat_info = session.info.streams.chat

            if chat_info.encryption is not None and chat_info.transport == 'tls':
                optional_fields['encryption_type'] = str(['TLS', '{0.encryption} ({0.encryption_cipher}'.format(chat_info)])
            elif chat_info.encryption is not None:
                optional_fields['encryption_type'] = str(['{0.encryption} ({0.encryption_cipher}'.format(chat_info)])
            elif chat_info.transport == 'tls':
                optional_fields['encryption_type'] = str(['TLS'])
418 419 420 421
        else:
            message_info = session.info.streams.messages
            if message_info.encryption is not None and message.is_secure:
                optional_fields['encryption_type'] = str([f'{message_info.encryption}'])
422 423 424 425 426 427 428 429 430 431 432 433 434
        try:
            Message(remote_uri=remote_uri,
                    display_name=display_name,
                    uri=str(message.sender.uri),
                    content=message.content,
                    content_type=message.content_type,
                    message_id=message.id,
                    account_id=str(session.account.id),
                    direction=direction,
                    timestamp=timestamp,
                    disposition=str(message.disposition),
                    **optional_fields)
        except dberrors.DuplicateEntryError:
435 436 437 438 439 440 441
            try:
                dbmessage = Message.selectBy(message_id=message.id)[0]
            except IndexError:
                pass
            else:
                if message.content != dbmessage.content:
                    dbmessage.content = message.content
442 443 444

    @run_in_thread('db')
    def update(self, id, state):
445 446
        messages = Message.selectBy(message_id=id)
        for message in messages:
447
            if message.direction == 'outgoing' and state == 'received':
448
                continue
449 450

            if message.state != 'displayed' and message.state != state:
451
                log.info(f'== Updating {message.direction} {id} {message.state} -> {state}')
452
                message.state = state
453

454 455 456 457 458 459 460
    @run_in_thread('db')
    def update_encryption(self, notification):
        message = notification.data.message
        session = notification.sender
        message_info = session.info.streams.messages

        if message_info.encryption is not None and message.is_secure:
461 462
            db_messages = Message.selectBy(message_id=message.id)
            for db_message in db_messages:
463 464
                encryption_type = str([f'{message_info.encryption}'])
                if db_message.encryption_type != encryption_type:
465
                    log.debug(f'== Updating {message.id} encryption {db_message.encryption_type} -> {encryption_type}')
466 467
                    db_message.encryption_type = encryption_type

468 469
    @run_in_thread('db')
    def load(self, uri, session):
470
        log.debug(f'== Loading messages for {uri}')
471 472
        notification_center = NotificationCenter()
        try:
473
            result = Message.selectBy(remote_uri=uri).orderBy('timestamp')[-100:]
474 475 476
        except Exception as e:
            notification_center.post_notification('BlinkMessageHistoryLoadDidFail', sender=session, data=NotificationData(uri=uri))
            return
477
        log.debug(f"== Messages loaded for {uri}: {len(list(result))}")
478 479 480 481
        notification_center.post_notification('BlinkMessageHistoryLoadDidSucceed', sender=session, data=NotificationData(messages=list(result), uri=uri))

    @run_in_thread('db')
    def get_last_contacts(self, number=5):
Tijmen de Mes's avatar
Tijmen de Mes committed
482
        log.debug(f'== Getting last {number} contacts with messages')
483

484
        query = f'select remote_uri, max(timestamp) from messages group by remote_uri order by timestamp desc limit {Message.sqlrepr(number)}'
485 486 487 488 489 490
        notification_center = NotificationCenter()
        try:
            result = self.db.queryAll(query)
        except Exception as e:
            return

491
        log.debug(f"== Contacts fetched: {len(list(result))}")
492
        result = [''.join(uri) for (uri, timestamp) in result]
493
        notification_center.post_notification('BlinkMessageHistoryLastContactsDidSucceed', data=NotificationData(contacts=list(result)))
494 495 496 497 498 499

    @run_in_thread('db')
    def remove(self, account):
        Message.deleteBy(account=account)

    @run_in_thread('db')
500
    def remove_contact_messages(self, account, contact):
501
        log.info(f'== Removing conversation between {account.id} <-> {contact}')
502
        Message.deleteBy(remote_uri=contact, account_id=str(account.id))
503 504 505

    @run_in_thread('db')
    def remove_message(self, id):
506
        log.debug(f'== Trying to removing message: {id}')
507 508 509 510
        result = Message.selectBy(message_id=id)
        for message in result:
            log.info(f'== Removing message: {id}')
            message.destroySelf()
511

512

513 514 515 516
class IconDescriptor(object):
    def __init__(self, filename):
        self.filename = filename
        self.icon = None
517 518

    def __get__(self, instance, owner):
519 520 521 522
        if self.icon is None:
            self.icon = QIcon(self.filename)
            self.icon.filename = self.filename
        return self.icon
523

524 525
    def __set__(self, obj, value):
        raise AttributeError("attribute cannot be set")
526

527 528 529 530
    def __delete__(self, obj):
        raise AttributeError("attribute cannot be deleted")


531
class HistoryEntry(object):
532
    phone_number_re = re.compile(r'^(?P<number>(0|00|\+)[1-9]\d{7,14})@')
533

534 535 536 537 538
    incoming_normal_icon = IconDescriptor(Resources.get('icons/arrow-inward-blue.svg'))
    outgoing_normal_icon = IconDescriptor(Resources.get('icons/arrow-outward-green.svg'))
    incoming_failed_icon = IconDescriptor(Resources.get('icons/arrow-inward-red.svg'))
    outgoing_failed_icon = IconDescriptor(Resources.get('icons/arrow-outward-red.svg'))

539
    def __init__(self, direction, name, uri, account_id, call_time, duration, failed=False, reason=None):
540
        self.direction = direction
541 542
        self.name = name
        self.uri = uri
543 544 545
        self.account_id = account_id
        self.call_time = call_time
        self.duration = duration
546
        self.failed = failed
547
        self.reason = reason
548 549

    def __reduce__(self):
550
        return self.__class__, (self.direction, self.name, self.uri, self.account_id, self.call_time, self.duration, self.failed, self.reason)
551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575

    def __eq__(self, other):
        return self is other

    def __ne__(self, other):
        return self is not other

    def __lt__(self, other):
        return self.call_time < other.call_time

    def __le__(self, other):
        return self.call_time <= other.call_time

    def __gt__(self, other):
        return self.call_time > other.call_time

    def __ge__(self, other):
        return self.call_time >= other.call_time

    @property
    def icon(self):
        if self.failed:
            return self.incoming_failed_icon if self.direction == 'incoming' else self.outgoing_failed_icon
        else:
            return self.incoming_normal_icon if self.direction == 'incoming' else self.outgoing_normal_icon
576

577 578
    @property
    def text(self):
Adrian Georgescu's avatar
Adrian Georgescu committed
579
        result = str(self.name or self.uri)
580 581 582 583
        blink_settings = BlinkSettings()
        if blink_settings.interface.show_history_name_and_uri:
            result = f'{str(self.name)} ({str(self.uri)})'

584
        if self.call_time:
585 586
            call_time = self.call_time.astimezone(tzlocal())
            call_date = call_time.date()
Dan Pascu's avatar
Dan Pascu committed
587
            today = date.today()
588 589
            days = (today - call_date).days
            if call_date == today:
590
                result += call_time.strftime(translate("history", " at %H:%M"))
591
            elif days == 1:
592
                result += call_time.strftime(translate("history", " Yesterday at %H:%M"))
593
            elif days < 7:
594
                result += call_time.strftime(translate("history", " on %A"))
595
            elif call_date.year == today.year:
596
                result += call_time.strftime(translate("history", " on %B %d"))
597
            else:
598
                result += call_time.strftime(translate("history", " on %Y-%m-%d"))
599
        if self.duration:
600 601 602 603 604 605 606
            seconds = int(self.duration.total_seconds())
            if seconds >= 3600:
                result += """ (%dh%02d'%02d")""" % (seconds / 3600, (seconds % 3600) / 60, seconds % 60)
            else:
                result += """ (%d'%02d")""" % (seconds / 60, seconds % 60)
        elif self.reason:
            result += ' (%s)' % self.reason.title()
607 608
        return result

609 610 611
    @classmethod
    def from_session(cls, session):
        if session.start_time is None and session.end_time is not None:
612
            # Session may have ended before it fully started
613
            session.start_time = session.end_time
614
        call_time = session.start_time or ISOTimestamp.now()
615 616 617 618
        if session.start_time and session.end_time:
            duration = session.end_time - session.start_time
        else:
            duration = None
Adrian Georgescu's avatar
Adrian Georgescu committed
619 620 621 622 623
        user = session.remote_identity.uri.user
        domain = session.remote_identity.uri.host

        user = user.decode() if isinstance(user, bytes) else user
        domain = domain.decode() if isinstance(domain, bytes) else domain
Tijmen de Mes's avatar
Tijmen de Mes committed
624

Adrian Georgescu's avatar
Adrian Georgescu committed
625
        remote_uri = '%s@%s' % (user, domain)
626 627 628 629 630 631 632 633 634
        match = cls.phone_number_re.match(remote_uri)
        if match:
            remote_uri = match.group('number')
        try:
            contact = next(contact for contact in AddressbookManager().get_contacts() if remote_uri in (addr.uri for addr in contact.uris))
        except StopIteration:
            display_name = session.remote_identity.display_name
        else:
            display_name = contact.name
Adrian Georgescu's avatar
Adrian Georgescu committed
635
        return cls(session.direction, display_name, remote_uri, str(session.account.id), call_time, duration)