history.py 7.22 KB
Newer Older
1

2
import bisect
3
import cPickle as pickle
4 5
import re

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

from application.notification import IObserver, NotificationCenter
from application.python import Null
from application.python.types import Singleton
Dan Pascu's avatar
Dan Pascu committed
11
from datetime import date
12
from dateutil.tz import tzlocal
13 14 15 16 17
from zope.interface import implements

from sipsimple.account import BonjourAccount
from sipsimple.addressbook import AddressbookManager
from sipsimple.threading import run_in_thread
18
from sipsimple.util import ISOTimestamp
19

20
from blink.resources import ApplicationData, Resources
21 22 23
from blink.util import run_in_gui_thread


24 25 26
__all__ = ['HistoryManager']


27 28 29 30
class HistoryManager(object):
    __metaclass__ = Singleton
    implements(IObserver)

31 32
    history_size = 20

33 34 35
    def __init__(self):
        try:
            data = pickle.load(open(ApplicationData.get('calls_history')))
36
            if not isinstance(data, list) or not all(isinstance(item, HistoryEntry) and item.text and isinstance(item.call_time, ISOTimestamp) for item in data):
37
                raise ValueError("invalid save data")
38
        except Exception:
39
            self.calls = []
40
        else:
41
            self.calls = data[-self.history_size:]
42 43 44 45 46 47
        notification_center = NotificationCenter()
        notification_center.add_observer(self, name='SIPSessionDidEnd')
        notification_center.add_observer(self, name='SIPSessionDidFail')

    @run_in_thread('file-io')
    def save(self):
48 49
        with open(ApplicationData.get('calls_history'), 'wb+') as history_file:
            pickle.dump(self.calls, history_file)
50 51 52 53 54 55 56 57 58 59 60

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

    def _NH_SIPSessionDidEnd(self, notification):
        if notification.sender.account is BonjourAccount():
            return
        session = notification.sender
        entry = HistoryEntry.from_session(session)
61 62
        bisect.insort(self.calls, entry)
        self.calls = self.calls[-self.history_size:]
63 64 65 66 67 68 69 70
        self.save()

    def _NH_SIPSessionDidFail(self, notification):
        if notification.sender.account is BonjourAccount():
            return
        session = notification.sender
        entry = HistoryEntry.from_session(session)
        if session.direction == 'incoming':
71 72
            if notification.data.code != 487 or notification.data.failure_reason != 'Call completed elsewhere':
                entry.failed = True
73
        else:
74 75 76 77
            if notification.data.code == 0:
                entry.reason = 'Internal Error'
            elif notification.data.code == 487:
                entry.reason = 'Cancelled'
78
            else:
79
                entry.reason = notification.data.reason or notification.data.failure_reason
80 81 82
            entry.failed = True
        bisect.insort(self.calls, entry)
        self.calls = self.calls[-self.history_size:]
83 84 85
        self.save()


86 87 88 89
class IconDescriptor(object):
    def __init__(self, filename):
        self.filename = filename
        self.icon = None
90 91

    def __get__(self, instance, owner):
92 93 94 95
        if self.icon is None:
            self.icon = QIcon(self.filename)
            self.icon.filename = self.filename
        return self.icon
96

97 98
    def __set__(self, obj, value):
        raise AttributeError("attribute cannot be set")
99

100 101 102 103
    def __delete__(self, obj):
        raise AttributeError("attribute cannot be deleted")


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

107 108 109 110 111
    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'))

112
    def __init__(self, direction, name, uri, account_id, call_time, duration, failed=False, reason=None):
113
        self.direction = direction
114 115
        self.name = name
        self.uri = uri
116 117 118
        self.account_id = account_id
        self.call_time = call_time
        self.duration = duration
119
        self.failed = failed
120
        self.reason = reason
121 122

    def __reduce__(self):
123
        return self.__class__, (self.direction, self.name, self.uri, self.account_id, self.call_time, self.duration, self.failed, self.reason)
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148

    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
149

150 151
    @property
    def text(self):
152
        result = unicode(self.name or self.uri)
153
        if self.call_time:
154 155
            call_time = self.call_time.astimezone(tzlocal())
            call_date = call_time.date()
Dan Pascu's avatar
Dan Pascu committed
156
            today = date.today()
157 158
            days = (today - call_date).days
            if call_date == today:
159
                result += call_time.strftime(" at %H:%M")
160
            elif days == 1:
161
                result += call_time.strftime(" Yesterday at %H:%M")
162
            elif days < 7:
163
                result += call_time.strftime(" on %A")
164
            elif call_date.year == today.year:
165
                result += call_time.strftime(" on %B %d")
166
            else:
167
                result += call_time.strftime(" on %Y-%m-%d")
168
        if self.duration:
169 170 171 172 173 174 175
            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()
176 177
        return result

178 179 180
    @classmethod
    def from_session(cls, session):
        if session.start_time is None and session.end_time is not None:
181
            # Session may have ended before it fully started
182
            session.start_time = session.end_time
183
        call_time = session.start_time or ISOTimestamp.now()
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
        if session.start_time and session.end_time:
            duration = session.end_time - session.start_time
        else:
            duration = None
        remote_uri = '%s@%s' % (session.remote_identity.uri.user, session.remote_identity.uri.host)
        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
        return cls(session.direction, display_name, remote_uri, unicode(session.account.id), call_time, duration)

200