Example #1
0
    def __init__(self, name, protocols = [], ids = [], *children):

        if not isinstance(name, basestring):
            raise TypeError('name must be a string, it was %s' %type(name))
        if len(protocols) != len(ids):
            raise AssertionError('protocols and ids must have same length: '
                                 '%d %d' % (len(protocols), len(ids)))

        BuddyListElement.__init__(self)
        ObservableList.__init__(self, *children)

        # assert that the incoming name string is a unicode object, or can
        # be converted to one trivially
        self.name = unicode(name)

        self.protocols = protocols
        self.ids = ids
        self.id_lookup = dict(izip(protocols, ids)) # stores {protocol: id}

        # This will store the number of buddies moved out of this group during
        # the filter process so __str__ can still return an accurate offline
        # count.
        self._offline_moved = 0

        # Whether or not to show the offline count, i.e. the 6 in Buddies (5/6)
        self._show_offline_count = True
Example #2
0
 def __init__(self, name, protocol, id, *children):
     BuddyListElement.__init__(self)
     ObservableList.__init__(self, *children)
     self.name = name
     self.protocol = protocol
     self.id = id
     self.watching = set()
Example #3
0
 def __init__(self, name, protocol, id, *children):
     BuddyListElement.__init__(self)
     ObservableList.__init__(self, *children)
     self.name = name
     self.protocol = protocol
     self.id = id
     self.watching = set()
Example #4
0
    def __init__(self, name, protocols=[], ids=[], *children):

        if not isinstance(name, basestring):
            raise TypeError('name must be a string, it was %s' % type(name))
        if len(protocols) != len(ids):
            raise AssertionError('protocols and ids must have same length: '
                                 '%d %d' % (len(protocols), len(ids)))

        BuddyListElement.__init__(self)
        ObservableList.__init__(self, *children)

        # assert that the incoming name string is a unicode object, or can
        # be converted to one trivially
        self.name = unicode(name)

        self.protocols = protocols
        self.ids = ids
        self.id_lookup = dict(izip(protocols, ids))  # stores {protocol: id}

        # This will store the number of buddies moved out of this group during
        # the filter process so __str__ can still return an accurate offline
        # count.
        self._offline_moved = 0

        # Whether or not to show the offline count, i.e. the 6 in Buddies (5/6)
        self._show_offline_count = True
Example #5
0
 def test_add_remove(self):
     from util.observe import ObservableList
     l = ObservableList()
     b = bar()
     l.add_observer(b.bar)
     l.remove_observer(b.bar)
     self.failIf(any(l.observers.values()), 'still have observers')
Example #6
0
    def __init__(self, profile):
        Observable.__init__(self)

        self.accounts_loaded = False

        self.profile = profile
        self.connected_accounts = ObservableList()
        self.reconnect_timers   = {}

        # holds "cancel" objects from Popups
        self.cancellers = {}

        self.profile.add_observer(self.on_state_change,   'state')
        self.profile.add_observer(self.on_offline_change, 'offline_reason')
        self.profile.add_observer(self.profile_state_changed, 'state')

        import wx
        wx.GetApp().OnBuddyListShown.append(lambda *a, **k: Timer(.25,
            threaded(self.release_accounts), *a, **k).start())

        self._hash = sentinel

        self.got_accounts = False

        self.acct_calls = Delegate()
        self.delay_accounts = True
        self.acct_delay_lock = RLock()

        self._all_acct_hash = {}
        self.last_server_order = None

        self._all_accounts = Storage()
        for type_ in ('im', 'em', 'so'):

            s = Storage(accounts = ObservableList(),
                        old = [])

            setattr(self._all_accounts, type_, s)

            # when the order of accounts changes, or accounts are added or deleted,
            # calls profile.accounts_changed('im', list)
            s.accounts.add_observer(getattr(self, type_ + '_accounts_changed'))

        self.accounts = self._all_accounts.im.accounts
        self.emailaccounts = self._all_accounts.em.accounts
        self.socialaccounts = self._all_accounts.so.accounts

        self.buddywatcher = BuddyWatcher()

        import services.service_provider as sp
        container = sp.ServiceProviderContainer(self.profile)
        container.on_order_changed += self._set_order
Example #7
0
    def __init__(self, enabled=True, updateNow=True, **options):

        AccountBase.__init__(self, **options)
        UpdateMixin.__init__(self, **options)
        FromNetMixin.__init__(self, **options)

        self.emails = ObservableList()
        self.count = 0
        self.seen = set()
        self._dirty_error = True  # The next error is new.

        log.info('Created EmailAccount: %r. Setting enabled to %r', self,
                 enabled)

        self.enabled = enabled
Example #8
0
    def Construct(self):
        parent = self

        self.line1 = wx.StaticText(
            parent,
            label="We hope you've enjoyed using Digsby.",
            style=wx.TE_CENTER)
        self.line2 = wx.StaticText(
            parent,
            label="Please show your support and invite your friends.",
            style=wx.TE_CENTER)

        self.separator = wx.StaticLine(parent)

        self.name_label = wx.StaticText(parent, label='Full Name: ')
        self.name_text = wx.TextCtrl(parent)

        self.acct_list = AnyList(parent,
                                 ObservableList(self.data),
                                 row_control=StaticEmailRow,
                                 multiselect=False,
                                 edit_buttons=None,
                                 draggable_items=False,
                                 style=0,
                                 velocity=None)
        self.acct_list.SetMinSize(wx.Size(-1, (16 + 10) * 4))

        self.acct_panel = PrefPanel(parent, self.acct_list, 'Account')
        self.acct_panel._bg_brush = lambda: wx.Brush(
            wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DFACE))
        self.acct_panel._fg_pen = lambda: wx.Pen(
            wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DSHADOW))

        self.send_button = wx.Button(parent, wx.ID_OK, label='Send Invite!')
        self.send_button.MoveAfterInTabOrder(self.name_text)
Example #9
0
 def __eq__(self, other):
     if self is other:
         return True
     elif hasattr(other, 'name') and self.name == other.name and (self is self.id or self.id == other.id):
         return ObservableList.__eq__(self, other)
     else:
         return False
Example #10
0
 def __eq__(self, other):
     if self is other:
         return True
     elif hasattr(other, 'name') and self.name == other.name and (
             self is self.id or self.id == other.id):
         return ObservableList.__eq__(self, other)
     else:
         return False
Example #11
0
def main():
    from tests.testapp import testapp
    a = testapp(skinname='Windows 7')
    f = wx.Frame(None, -1, 'roomlist')

    AIM = ('aim', [
        caps.BLOCKABLE, caps.EMAIL, caps.FILES, caps.IM, caps.PICTURES,
        caps.SMS
    ])
    MSN = ('msn', [caps.BLOCKABLE, caps.EMAIL, caps.FILES, caps.IM])
    JBR = ('jabber', [caps.EMAIL, caps.FILES, caps.IM])
    YHO = ('yahoo',
           [caps.BLOCKABLE, caps.EMAIL, caps.FILES, caps.IM, caps.SMS])

    global contacts
    contacts = ObservableList([
        MockBuddy('Aaron', 'away', *JBR),
        MockBuddy('Chris', 'available', *JBR),
        MockBuddy('Jeff', 'offline', *AIM),
        MockBuddy('Kevin', 'away', *YHO),
        MockBuddy('Mike', 'available', *MSN),
        MockBuddy('Steve', 'offline', *AIM),
    ])

    buddies = dict((c.name, c) for c in contacts)

    contacts.extend([
        MockBuddy('Agatha', 'offline', *AIM),
        MockBuddy('Abel', 'away', *YHO),
        MockBuddy('Adam', 'available', *MSN),
        MockBuddy('Amanda', 'offline', *AIM),
        MockBuddy('Beatrice', 'offline', *AIM),
        MockBuddy('Betty', 'away', *YHO),
        MockBuddy('Brian', 'available', *MSN),
        MockBuddy('Biff', 'away', *YHO),
        MockBuddy('Bart', 'available', *MSN),
    ])

    rl = RoomListPanel(f, buddies)
    rl.RoomList = contacts

    f.SetSize((200, 400))
    f.Show()

    a.MainLoop()
Example #12
0
def main():
    from tests.testapp import testapp
    a = testapp(skinname='Windows 7')
    f = wx.Frame(None, -1, 'roomlist')


    AIM=('aim',    [caps.BLOCKABLE, caps.EMAIL, caps.FILES, caps.IM, caps.PICTURES, caps.SMS])
    MSN=('msn',    [caps.BLOCKABLE, caps.EMAIL, caps.FILES, caps.IM])
    JBR=('jabber', [caps.EMAIL, caps.FILES, caps.IM])
    YHO=('yahoo',  [caps.BLOCKABLE, caps.EMAIL, caps.FILES, caps.IM, caps.SMS])

    global contacts
    contacts = ObservableList([MockBuddy('Aaron', 'away', *JBR),
                               MockBuddy('Chris', 'available', *JBR),
                               MockBuddy('Jeff',  'offline', *AIM),
                               MockBuddy('Kevin', 'away', *YHO),
                               MockBuddy('Mike',  'available', *MSN),

                               MockBuddy('Steve', 'offline', *AIM),])

    buddies = dict((c.name, c) for c in contacts)

    contacts.extend([
                               MockBuddy('Agatha',  'offline', *AIM),
                               MockBuddy('Abel', 'away', *YHO),
                               MockBuddy('Adam',  'available', *MSN),
                               MockBuddy('Amanda',  'offline', *AIM),
                               MockBuddy('Beatrice',  'offline', *AIM),
                               MockBuddy('Betty', 'away', *YHO),
                               MockBuddy('Brian',  'available', *MSN),
                               MockBuddy('Biff', 'away', *YHO),
                               MockBuddy('Bart',  'available', *MSN),
    ])


    rl = RoomListPanel(f, buddies)
    rl.RoomList = contacts

    f.SetSize((200,400))
    f.Show()

    a.MainLoop()
Example #13
0
 def test_add_remove(self):
     from util.observe import ObservableList
     l = ObservableList()
     b = bar()
     l.add_observer(b.bar)
     l.remove_observer(b.bar)
     self.failIf(any(l.observers.values()), 'still have observers')
Example #14
0
    def __init__(self, enabled = True, updateNow = True, **options):

        AccountBase.__init__(self, **options)
        UpdateMixin.__init__(self, **options)
        FromNetMixin.__init__(self, **options)

        self.emails = ObservableList()
        self.count = 0
        self.seen = set()
        self._dirty_error = True # The next error is new.

        log.info('Created EmailAccount: %r. Setting enabled to %r', self, enabled)

        self.enabled = enabled
Example #15
0
    def __init__(self):
        Observable.__init__(self)

        bud = MockBuddy('fakebuddy')

        self.name = 'fakebuddy'
        self.me = MockBuddy('digsby007')

        self.room_list = ObservableList([bud, self.me])
        self.typing_status = ObservableDict()
        self.buddy = bud
        self.messages = Queue()
        self.protocol = Storage(self_buddy=self.me,
                                buddies={'digsby007': self.me})
        self.ischat = False
Example #16
0
    def __init__(self, protocol):

        Observable.__init__(self)

        self.room_list = ObservableList()

        self.protocol = protocol
        self.autoresponded = False
        self.typing_status = observable_dict()
        self.just_had_error = False

        self.pending_contacts_callbacks = set()

        self.start_time_utc = datetime.utcnow()

        self._bind_reconnect()
        self.protocol.add_observer(self.__on_proto_state, 'state')

        self.videochat_urlhandler = None
Example #17
0
def main():
    from util.observe import ObservableList, Observable

    app = wx.PySimpleApp()

    f = wx.Frame(None, -1, 'AnyList Test')
    f.Sizer = sz = wx.BoxSizer(wx.VERTICAL)

    class Foo(Observable):
        def __init__(self, name):
            Observable.__init__(self)
            self.name = name

        def __call__(self):
            wx.Bell()

        def __repr__(self):
            return '<Foo %s>' % self.name

    def on_doubleclick(e):
        print e

    foos = [
        Foo(n) for n in 'one two three four five six seven eight nine'.split()
    ]
    data = ObservableList(foos)

    splist = AnyList(f, data)
    splist.Bind(wx.EVT_LISTBOX_DCLICK, on_doubleclick)

    sz.Add(splist, 1, wx.EXPAND | wx.SOUTH,
           15 if 'wxMac' in wx.PlatformInfo else 0)
    splist.Bind(wx.EVT_CHECKBOX, lambda e: e.EventObject.Parent.data())

    f.Show()
    app.MainLoop()
Example #18
0
 def _init_contacts(self):
     self.contacts_view = []
     self.contact_enter_times = {}
     self.contact_leave_times = {}
     self.pending_contacts = ObservableList()
Example #19
0
class RoomListModel(EventMixin):
    events = set(('contacts_changed', ))

    def __init__(self, contacts):
        EventMixin.__init__(self)
        self.contacts = None
        self.offline = False
        self.set_contacts(contacts)

    def _init_contacts(self):
        self.contacts_view = []
        self.contact_enter_times = {}
        self.contact_leave_times = {}
        self.pending_contacts = ObservableList()

    def set_contacts(self, contacts):
        if hasattr(self.contacts, 'remove_observer'):
            self.contacts.remove_observer(self._on_contacts_changed)

        self.contacts = contacts
        self._init_contacts()
        for contact in contacts:
            self.contact_enter_times[contact] = 0

        if hasattr(self.contacts, 'add_observer'):
            self.contacts.add_observer(self._on_contacts_changed)

        self._on_contacts_changed()

    def _on_contacts_changed(self, *a, **k):
        self._update_pending()
        self._update_view()
        self.fire_contacts_changed()

    def fire_contacts_changed(self):
        self.event('contacts_changed')

    TIME_LEAVING = 4

    def _update_view(self):
        self.leaving_contacts = view = []
        now = time()
        for gone_contact, t in list(self.contact_leave_times.items()):
            if now - t > self.TIME_LEAVING:
                self.contact_leave_times.pop(gone_contact, None)
            else:  #leaving
                view.append(gone_contact)

        old_length = len(self.contacts_view)
        self.contacts_view = sorted(list(self.contacts) + view)
        return len(self.contacts_view) != old_length

    def _update_pending(self):
        for pending_contact in list(self.pending_contacts):
            for contact in self.contacts:
                if pending_contact.equals_chat_buddy(contact):
                    self.pending_contacts.remove(pending_contact)

        contacts = set(self.contacts)
        now = time()
        for contact in list(self.contact_enter_times.keys()):
            if contact not in contacts:
                self.contact_enter_times.pop(contact, None)
                self.contact_leave_times.setdefault(contact, now)

        for contact in contacts:
            if contact not in self.contact_enter_times:
                self.contact_enter_times.setdefault(contact, now)

    def contact_is_new(self, contact):
        time_joined = self.contact_enter_times.get(contact, None)
        if time_joined is None:
            return False
        TIME_NEW = 2
        now = time()
        return now - time_joined < TIME_NEW

    def contact_is_leaving(self, contact):
        time_left = self.contact_leave_times.get(contact, None)
        return time_left is not None and time() - time_left < self.TIME_LEAVING

    def add_pending_contact(self, contact):
        if contact not in self.pending_contacts:
            self.pending_contacts.append(contact)
            self.event('contacts_changed')
            return True

    def remove_pending_contact(self, contact):
        try:
            self.pending_contacts.remove(contact)
        except ValueError:
            return False
        else:
            self.event('contacts_changed')
            return True

    @property
    def length_including_pending(self):
        return self.length + len(self.pending_contacts)

    @property
    def length(self):
        return len(self.contacts_view)

    def get_contact(self, n):
        return self.contacts_view[n]

    def get_pending_contact(self, n):
        return self.pending_contacts[n]
Example #20
0
def main():
    def on_close(e):
        twitter.disconnect()

        import AsyncoreThread
        AsyncoreThread.join()

        f.Destroy()

    def droptables():
        if wx.YES == wx.MessageBox(
                'Are you sure you want to drop all tables?',
                style = wx.YES_NO, parent = f):
            twitter.clear_cache()

    def build_test_frame():
        f = wx.Frame(None, title='Twitter Test')
        f.SetSize((500, 700))
        f.Bind(wx.EVT_CLOSE, on_close)

        buttons = []
        def button(title, callback):
            b = wx.Button(f, -1, title)
            b.Bind(wx.EVT_BUTTON, lambda e: callback())
            buttons.append(b)

        def infobox():
            from gui.toolbox import Monitor
            from gui.infobox.infobox import DEFAULT_INFOBOX_WIDTH
            from gui.infobox.infoboxapp import init_host, set_hosted_content

            f = wx.Frame(None)
            size = (DEFAULT_INFOBOX_WIDTH, Monitor.GetFromWindow(f).ClientArea.height * .75)
            f.SetClientSize(size)

            w = wx.webview.WebView(f)

            init_host(w)
            set_hosted_content(w, MockTwitterAccount(twitter))
            f.Show()

        def popup():
            twitter.webkitcontroller.evaljs('account.showTimelinePopup();')

        def fake_tweets():
            j('fakeTweets(%d);' % int(fake_tweets_txt.Value))
            twitter.webkitcontroller.webview.GarbageCollect()

        button('Open Window', twitter.open_timeline_window)
        button('Update',      twitter.update)
        button('Infobox',     infobox)
        button('Drop Tables', droptables)
        button('Popup',       popup)
        button('Fake Tweets', fake_tweets)

        s = f.Sizer = wx.BoxSizer(wx.HORIZONTAL)

        v = wx.BoxSizer(wx.VERTICAL)
        v.AddMany(buttons)

        fake_tweets_txt = wx.TextCtrl(f, -1, '1000')
        v.Add(fake_tweets_txt)

        s.Add(v, 0, wx.EXPAND)

        v2 = wx.BoxSizer(wx.VERTICAL)
        stxt = wx.StaticText(f)
        v2.Add(stxt, 0, wx.EXPAND)

        from pprint import pformat
        from common.commandline import wkstats
        def update_text():
            debug_txt = '\n\n'.join([
                pformat(wkstats()),
                j('debugCounts()')
            ])

            stxt.Label = debug_txt
            f.Sizer.Layout()

        f._timer = wx.PyTimer(update_text)
        f._timer.StartRepeating(1000)
        f.SetBackgroundColour(wx.WHITE)

        s.Add((50, 50))
        s.Add(v2, 0, wx.EXPAND)

        return f

    from tests.testapp import testapp

    username, password = '******', 'no passwords'
    if len(sys.argv) > 2:
        username, password = sys.argv[1:3]

    app = testapp(skinname='Windows 7', plugins=True)

    global twitter # on console
    from twitter.twitter import TwitterProtocol
    twitter = TwitterProtocol(username, password)
    twitter.events.state_changed += lambda state: print('state changed:', state)
    twitter.events.reply += lambda screen_name: print('reply:', screen_name)

    import common.protocolmeta
    account_opts = common.protocolmeta.protocols['twitter']['defaults'].copy()

    if '--twitter-offline' in sys.argv:
        account_opts['offlineMode'] = True

    #import simplejson
    #account_opts['feeds'] = simplejson.loads('[{"type": "timeline", "name": "timeline"}, {"type": "mentions", "name": "mentions"}, {"type": "directs", "name": "directs"}, {"name": "group:1", "popups": false, "ids": [14337372, 9499402, 8517312, 7341872, 32218792, 9853162], "filter": false, "groupName": ".syntax", "type": "group"}, {"name": "search:1", "title": "foramilliondollars", "popups": false, "merge": false, "query": "#foramilliondollars", "save": true, "type": "search"}]')

    twitter.connect(account_opts)

    # mock an accountmanager/social networks list for the global status dialog
    from util.observe import ObservableList, Observable
    class TwitterAccount(Observable):
        service = protocol = 'twitter'
        enabled = True
        ONLINE = True
        def __init__(self, connection):
            Observable.__init__(self)
            self.display_name = self.name = connection.username
            self.connection = connection
            self.connection.account = self

    import digsbyprofile
    from util import Storage as S

    acctmgr = digsbyprofile.profile.account_manager = S(
        socialaccounts = ObservableList([TwitterAccount(twitter)])
    )
    digsbyprofile.profile.socialaccounts = acctmgr.socialaccounts

    global j
    j = twitter.webkitcontroller.evaljs

    def _html():
        txt = twitter.webkitcontroller.FeedWindow.PageSource.encode('utf-8')
        from path import path
        p = path(r'c:\twitter.html')
        p.write_bytes(txt)
        wx.LaunchDefaultBrowser(p.url())

    global html
    html = _html

    f = build_test_frame()
    f.Show()

    if '--drop' in sys.argv:
        twitter.clear_cache()

    wx.CallLater(500, twitter.open_timeline_window)

    app.MainLoop()
Example #21
0
class EmailAccount(AccountBase, UpdateMixin, FromNetMixin):

    retry_time = 3
    error_max = 3

    def __init__(self, enabled = True, updateNow = True, **options):

        AccountBase.__init__(self, **options)
        UpdateMixin.__init__(self, **options)
        FromNetMixin.__init__(self, **options)

        self.emails = ObservableList()
        self.count = 0
        self.seen = set()
        self._dirty_error = True # The next error is new.

        log.info('Created EmailAccount: %r. Setting enabled to %r', self, enabled)

        self.enabled = enabled

    def timestamp_is_time(self, tstamp):
        return True

    #
    # local prefs
    #
    mailclient         = localprefprop(localprefs_key('mailclient'), None)
    custom_inbox_url   = localprefprop(localprefs_key('custom_inbox_url'), None)
    custom_compose_url = localprefprop(localprefs_key('custom_compose_url'), None)

    email_address = util.iproperty('get_email_address', 'set_email_address')

    @property
    def extra_header_func(self):
        return None # inviting disabled

        if self.protocol not in ('aolmail', 'ymail', 'gmail', 'hotmail'):
            return None
        import hooks
        d = {}
        for attr in ('protocol', 'name', 'password'):
            d[attr] = getattr(self, attr)
        return ("Invite Contacts", lambda *a, **k: hooks.notify('digsby.email.invite_clicked', **d))

    def get_email_address(self):
        from util import EmailAddress
        try:
            return str(EmailAddress(self.name, self.default_domain)).decode('ascii')
        except (AttributeError, ValueError):
            try:
                ret = self.name if '@' in self.name else self.name + '@' + self.default_domain
                if isinstance(ret, bytes):
                    return ret.decode('ascii')
                else:
                    return ret
            except Exception:
                # hopefully bad data has been caught before here.
                if isinstance(self.name, bytes):
                    return self.name.decode('ascii', 'replace')
                else:
                    return self.name

    def set_email_address(self, val):
        # Not supported
        pass

    @property
    def display_name(self):
        return try_this(lambda: getattr(self, pref('email.display_attr')), self.email_address)

    def on_error(self, task=None):
        '''
        Called when an error occurs. task is a callable that can be used to make another attempt
        at whatever caused the error (if error_count is less than max_error).
        '''
        self.error_count += 1

        log.error('%r\'s error count is now: %d',self, self.error_count)
        log.error('on_error called from %s', get_func_name(2))

        if self.error_count < pref('email.err_max_tolerance', self.error_max):
            if task is None:
                task = self.update_now
            log.error('error count is under, calling %r now', task)

            if not callable(task):
                # If it was an exception assume that update_now was called. (the account type
                # probably just hasn't been fixed yet
                task = self.update_now
            util.call_later(pref('email.err_retry_time', type=int, default=2), task)
        else:
            log.error('assuming the connection has died')
            self.set_offline(self.Reasons.CONN_FAIL)
            self.error_count = 0

        del self.emails[:]

    def bad_pw(self):
        log.info('%r: changing state to BAD_PASSWORD', self)
        self.set_offline(self.Reasons.BAD_PASSWORD)
        self.timer.stop()

    def no_mailbox(self):
        log.info('%r: changing state to NO_MAILBOX', self)
        self.set_offline(self.Reasons.NO_MAILBOX)
        self.timer.stop()

    def __repr__(self):
        r = AccountBase.__repr__(self)[:-1]
        r += ', '
        r += 'enabled' if self.enabled else 'disabled'
        return r + '>'

    @property
    def web_login(self):
        return (pref('privacy.www_auto_signin') and self.state in (self.Statuses.ONLINE, self.Statuses.CHECKING))

    def error_link(self):

        reason  = self.Reasons
        linkref = {
            reason.BAD_PASSWORD : ('Edit Account', lambda *a: profile.account_manager.edit(self,True)),
            reason.CONN_FAIL    : ('Retry',        lambda *a: self.update_now())
        }
        if self.offline_reason in linkref:
            name, callback = linkref[self.offline_reason]
            return name, callback
        else:
            return None

    def sort_emails(self, new=None):
        self.emails.sort()

        if new is not None:
            new.sort()

    def filter_new(self, new, old):
        if old:
            return [e for e in new if e <= old[-1]]
        elif self.seen:
            new_ids = set(e.id for e in new)
            keep = set()
            for email in self.emails:
                if email.id in new_ids:
                    keep.add(email.id)
                else:
                    break
            return [e for e in new if e.id in keep]
        else:
            return list(new)

    def _see_new(self, new_messages):
        new_seen = set()
        for email in new_messages:
            new_seen.add(email.id)
        self.seen.update(new_seen)

    def _get_new(self):
        new = []
        for email in self.emails:
            if email.id not in self.seen:
                new.append(email)
        return new

    def _received_emails(self, emails, inboxCount = None):
        '''
        Subclasses should call this method with any new emails received.

        @param emails: a sequence of Email objects
        '''
        old, self.emails[:] = self.emails[:], list(emails)

        new = self._get_new()
        for email in new:
            import plugin_manager.plugin_hub as plugin_hub
            plugin_hub.act('digsby.mail.newmessage.async', self, email)
        self._see_new(new)

        self.sort_emails(new)
        new = self.filter_new(new, old)
        del old

        info('%s - %s: %d new emails', self.__class__.__name__, self.name, len(new))

        if inboxCount is not None:
            self._setInboxCount(inboxCount)

        self.new = new
        if new:
            profile.when_active(self.fire_notification)

        self.error_count = 0
        self.change_state(self.Statuses.ONLINE)
        self._dirty_error = True # Next error will be new

    def fire_notification(self):
        if self.new:
            self._notify_emails(self.new)

    def popup_buttons(self, item):
        return []

    def _notify_emails(self, emails, always_show = None, allow_click = True):
        if self.enabled:
            fire('email.new', emails = emails,
                              onclick = self.OnClickEmail if allow_click else None,
                              always_show = always_show,
                              buttons = self.popup_buttons,
                              icon = self.icon)

    def _setInboxCount(self, inboxCount):
        self.setnotifyif('count', inboxCount)

    @action()
    @callsback
    def OnComposeEmail(self, to='', subject='', body='', cc='', bcc='', callback = None):
        import hooks; hooks.notify('digsby.statistics.email.compose')
        for name in ('to','subject', 'body', 'cc', 'bcc'):
            assert isinstance(vars()[name], basestring), (name, type(vars()[name]), vars()[name])

        if self.mailclient and try_this(lambda: self.mailclient.startswith('file:'), False):
            os.startfile(self.mailclient[5:])

        elif self.mailclient == 'sysdefault':
            kw = {}
            for name in ('subject', 'body', 'cc',  'bcc'):
                if vars()[name]:
                    kw[name] = vars()[name]

            query = UrlQuery('mailto:' + quote(to), **kw)
            log.info('OnComposeEmail is launching query: %s' % query)
            try:
                os.startfile(query)
            except WindowsError:
                # WindowsError: [Error 1155] No application is associated with the specified file for this operation: 'mailto:'
                mailclient_error()
                raise

        elif self.mailclient == '__urls__':
            url = self.custom_compose_url
            if url is not None: launch_browser(url)

        else:
            url = self.compose(to, subject, body, cc, bcc)
            if url:
                launch_browser(url)

        callback.success()

    @property
    def client_name(self):
        "Returns a string representing this email account's mail client."

        mc = self.mailclient

        if mc in (None, True, False):
            return self.protocol_info().name

        elif mc.startswith('file:'):
            return path(mc).basename().title()

        elif mc == 'sysdefault':
            #HACK: Use platform specific extensions to extract the actual
            #application name from the executable. for now, use ugly path
            #TLDR: needs the registry
            return ''

        elif mc == '__urls__':
            return ''

        else:
            log.warning('unknown mailclient attribute in %r: %s', self, mc)
            return _('Email Client')

    @action()
    def OnClickInboxURL(self, e = None):
        import hooks; hooks.notify('digsby.statistics.email.inbox_opened')
        if self.mailclient:
            url = self.start_client_email()
            if url is None:
                return
        else:
            url = self.inbox_url

        launch_browser(self.inbox_url)

    DefaultAction = OnClickHomeURL = OnClickInboxURL

    opening_email_marks_as_read = True

    def OnClickEmail(self, email):
        import hooks; hooks.notify('digsby.statistics.email.email_opened')
        if self.mailclient:
            self.start_client_email(email)
        else:
            url = self.urlForEmail(email)
            launch_browser(url)

        # For accounts where we are guaranteed to actually read the email
        # on click (i.e., ones that use webclients and have autologin on),
        # decrement the email count.
        if self.opening_email_marks_as_read:
            self._remove_email(email)

    @callsback
    def OnClickSend(self, to='', subject='', body='', cc='', bcc='', callback = None):
        '''
        Sends an email.
        '''
        getattr(self, 'send_email', self.OnComposeEmail)(to=to,
                    subject=subject, body=body, cc=cc, bcc=bcc, callback = callback)

    def start_client_email(self, email=None):
        log.info('mailclient: %s', self.mailclient)
        import os.path

        if self.mailclient == 'sysdefault':
            launch_sysdefault_email(email)

        elif self.mailclient == '__urls__':
            url = self.custom_inbox_url
            if url is not None: launch_browser(url)

        elif try_this(lambda:self.mailclient.startswith('file:'), False):
            filename = self.mailclient[5:]

            if os.path.exists(filename):
                os.startfile(filename)
            else:
                log.warning('cannot find %s', filename)

    def __len__(self):
        return self.count

    def __iter__(self):
        return iter(self.emails)

    can_has_preview = False

    @property
    def icon(self):
        from gui import skin
        from util import try_this
        return try_this(lambda: skin.get('serviceicons.%s' % self.protocol), None)

    @property
    def inbox_url(self):
        '''
        Return the url for the user's inbox to be opened in browser.
        This should adhere to the 'privacy.www_auto_signin' pref.
        '''
        raise NotImplementedError

    def observe_count(self,callback):
        self.add_gui_observer(callback, 'count')
        self.emails.add_gui_observer(callback)

    def unobserve_count(self, callback):
        self.remove_gui_observer(callback, 'count')
        self.emails.remove_gui_observer(callback)

    def observe_state(self, callback):
        self.add_gui_observer(callback, 'enabled')
        self.add_gui_observer(callback, 'state')

    def unobserve_state(self, callback):
        self.remove_gui_observer(callback, 'enabled')
        self.remove_gui_observer(callback, 'state')

    @property
    def header_funcs(self):
        return [('Inbox',self.OnClickInboxURL), ('Compose', self.OnComposeEmail)]

    def _get_options(self):
        opts = UpdateMixin.get_options(self)
        return opts

    def update_info(self, **info):
        flush_state = False
        with self.frozen():
            for k, v in info.iteritems():
                if k in ('password', 'username', 'server') and getattr(self, k, None) != v:
                    flush_state = True


                self.setnotifyif(k, v)

        # Tell the server.
        profile.update_account(self)

        if flush_state:
            log.info('Resetting state for %r', self)
            self._reset_state()

        self._dirty_error = True

    def _reset_state(self):
        return NotImplemented

    def update(self):
        # Protocols must override this method to check for new messages.
        if self.update == EmailAccount.update:
            log.warning('not implemented: %s.update', self.__class__.__name__)
            raise NotImplementedError

        if not self.enabled:
            return

        log.info('%s (%s) -- preparing for update. update called from: %s', self, self.state, get_func_name(2))

        if self.state == self.Statuses.OFFLINE:
            # First check -- either after creation or failing to connect for some reason
            self.change_state(self.Statuses.CONNECTING)
        elif self.state == self.Statuses.ONLINE:
            # A follow-up check.
            self.change_state(self.Statuses.CHECKING)
        elif self.state == self.Statuses.CONNECTING:
            # Already connecting -- if there have been errors this is just the Nth attempt.
            # if there are not errors, something is wrong! -- disconnect
            if not self.error_count:
                log.error('%s -- called update while connecting, and no errors! disconnecting...',self)
                self.set_offline(self.Reasons.CONN_FAIL)
        else:
            log.error('Unexpected state for update: %r', self.state)

    @action(lambda self: (self.state != self.Statuses.CHECKING))
    def update_now(self):
        'Invoked by the GUI.'

        netcall(self.update)
        self.timer.reset(self.updatefreq)


    @action()
    def tell_me_again(self):

        if self.emails:
            emails = self.emails
            allow_click = True
        else:
            from mail.emailobj import Email
            emails = [Email(id = -1, fromname = _('No unread mail'))]
            allow_click = False

        # Use "always_show" to always show a popup, regardless of whether the
        # user has popups enabled or not.
        self._notify_emails(emails,
                            always_show = ['Popup'],
                            allow_click = allow_click)

    @action()
    def auth(self):
        netcall(self.authenticate)

    def Connect(self):
        self.change_reason(self.Reasons.NONE)
        call_later(1.5, self.update)

    @action()
    def compose(self, to, subject, body, cc, bcc):
        '''
        Return a link for a browser that will bring the user to a compose window
        (or as close as possible, adhering to the 'privacy.www_auto_signin' pref).

        '''
        raise NotImplementedError

    def urlForEmail(self, email):
        '''
        Return a link to be opened by a browser that will show the user the email
        (or as close as possible, adhering to the 'privacy.www_auto_signin' pref).

        email -- the email OBJECT that we want the URL for.
        '''
        raise NotImplementedError

    @action()
    def open(self, email_message):
        '''

        '''
        if type(self) is EmailAccount:
            raise NotImplementedError

    def _remove_email(self, email_message):
        try:
            self.emails.remove(email_message)
        except ValueError:
            # already removed
            pass
        else:
            self.setnotifyif('count', self.count - 1)

    @action()
    def markAsRead(self, email_message):
        '''
        Mark the email object as read.
        '''
        import hooks; hooks.notify('digsby.statistics.email.mark_as_read')
        self._remove_email(email_message)

    @action()
    def delete(self, email_message):
        import hooks; hooks.notify('digsby.statistics.email.delete')
        self._remove_email(email_message)

    @action()
    def archive(self, email_message):
        import hooks; hooks.notify('digsby.statistics.email.archive')
        self._remove_email(email_message)

    @action()
    def reportSpam(self, email_message):
        import hooks; hooks.notify('digsby.statistics.email.spam')
        self._remove_email(email_message)
Example #22
0
class Statuses(SerializableNetData):
    xml_element_namespace = DIGSBY_STATUS_NS

    fallback = lambda self: ObservableList()
    basetype = list
Example #23
0
class RoomListModel(EventMixin):
    events = set((
        'contacts_changed',
    ))

    def __init__(self, contacts):
        EventMixin.__init__(self)
        self.contacts = None
        self.offline = False
        self.set_contacts(contacts)

    def _init_contacts(self):
        self.contacts_view = []
        self.contact_enter_times = {}
        self.contact_leave_times = {}
        self.pending_contacts = ObservableList()

    def set_contacts(self, contacts):
        if hasattr(self.contacts, 'remove_observer'):
            self.contacts.remove_observer(self._on_contacts_changed)

        self.contacts = contacts
        self._init_contacts()
        for contact in contacts:
            self.contact_enter_times[contact] = 0

        if hasattr(self.contacts, 'add_observer'):
            self.contacts.add_observer(self._on_contacts_changed)

        self._on_contacts_changed()

    def _on_contacts_changed(self, *a, **k):
        self._update_pending()
        self._update_view()
        self.fire_contacts_changed()

    def fire_contacts_changed(self):
        self.event('contacts_changed')

    TIME_LEAVING = 4

    def _update_view(self):
        self.leaving_contacts = view = []
        now = time()
        for gone_contact, t in list(self.contact_leave_times.items()):
            if now - t > self.TIME_LEAVING:
                self.contact_leave_times.pop(gone_contact, None)
            else: #leaving
                view.append(gone_contact)

        old_length = len(self.contacts_view)
        self.contacts_view = sorted(list(self.contacts) + view)
        return len(self.contacts_view) != old_length

    def _update_pending(self):
        for pending_contact in list(self.pending_contacts):
            for contact in self.contacts:
                if pending_contact.equals_chat_buddy(contact):
                    self.pending_contacts.remove(pending_contact)

        contacts = set(self.contacts)
        now = time()
        for contact in list(self.contact_enter_times.keys()):
            if contact not in contacts:
                self.contact_enter_times.pop(contact, None)
                self.contact_leave_times.setdefault(contact, now)

        for contact in contacts:
            if contact not in self.contact_enter_times:
                self.contact_enter_times.setdefault(contact, now)

    def contact_is_new(self, contact):
        time_joined = self.contact_enter_times.get(contact, None)
        if time_joined is None:
            return False
        TIME_NEW = 2
        now = time()
        return now - time_joined < TIME_NEW

    def contact_is_leaving(self, contact):
        time_left = self.contact_leave_times.get(contact, None)
        return time_left is not None and time() - time_left < self.TIME_LEAVING

    def add_pending_contact(self, contact):
        if contact not in self.pending_contacts:
            self.pending_contacts.append(contact)
            self.event('contacts_changed')
            return True

    def remove_pending_contact(self, contact):
        try:
            self.pending_contacts.remove(contact)
        except ValueError:
            return False
        else:
            self.event('contacts_changed')
            return True

    @property
    def length_including_pending(self):
        return self.length + len(self.pending_contacts)

    @property
    def length(self):
        return len(self.contacts_view)

    def get_contact(self, n): return self.contacts_view[n]

    def get_pending_contact(self, n): return self.pending_contacts[n]
Example #24
0
    def __init__(self, identity):
        Observable.__init__(self)
        ChatProtocol.__init__(self)

        self.identity = identity

        from AccountManager import AccountManager

        self.PreDisconnectHooks  = Delegate()
        self.PostDisconnectHooks = Delegate()

        if not getattr(getattr(sys, 'opts', None), 'limit_log', True):
            DelayedStreamLimiter = lambda s: s
        else:
            from fileutil import DelayedStreamLimiter

        self.consolehandlers = defaultdict(lambda: console_handler_class(DelayedStreamLimiter(sys.stdout)))

        self._status = None

        self.error_count = 0
        self.offline_reason = StateMixin.Reasons.NONE
        self.account_manager = AccountManager(profile = self)
        self.last_hiber_req = None
        self.hibernated = False
        self.linked_observers = False
        self.xfers = ObservableList()
        self.prefs = ObservableDict()
        self.defaultprefs = ObservableDict(prefs.defaultprefs())
        self.quiet = False
        self.prefs_loaded = False

        # set the common.pref lookup to point to our prefs
        import common
        common.set_active_prefs(self.prefs, self.defaultprefs)

        self.prefs.add_observer(self._prefs_changed)

        self.has_authorized = False

        self.statuses = ObservableList()
        self.statuses.add_observer(self._statuses_changed)

        self._xfercount = 0
        self.xfers.add_observer(self._on_file_transfer)

        self.widgets = ObservableList()

        self._encrypter, self._decrypter = util.cryptography.cipher_functions(sha1(self.password.encode('utf8')).digest()[:16])

        self.log_sizes = LogSizeDict()

        global profile
        if profile not in (self, None):
            warnmsg = 'Another DigsbyProfile has been created but the old one is still around!'
            if __debug__:
                raise ValueError(warnmsg)
            else:
                log.critical(warnmsg)

        profile = self  # hack! BuddyListStore needs profile.username

        from contacts.buddyliststore import BuddyListStore
        self.blist = BuddyListStore(self.account_manager.connected_accounts)

        self.set_contact_info = self.blist.set_contact_info
        self.get_contact_info = self.blist.get_contact_info

        from BlobManager import BlobManager
        self.blob_manager = BlobManager(self)

        self.account_manager.add_observer(self.check_loading, 'got_accounts')
        self.account_manager.add_observer(self.on_accounts_loaded, 'accounts_loaded')

        self.blob_manager.add_observer(self.check_loading, 'loading')
        self.loaded = False

        self.OnReturnFromIdle = Delegate()
        self.on_message = PausableDelegate()

        self.OnStatusChange = Delegate()

        self.setup_hub()

        self.idle_timer = None
        self.idle = False

        self.plugins_setup = False
        self.connection = None
        self.setup_plugins()

        self.do_local_load()
Example #25
0
class DigsbyProfile(Observable, ChatProtocol):
    'A collection of accounts and preferences.'

    MAX_ICON_SIZE  = 96
    MAX_ICON_BYTES = 64 * 1024

    protocol = 'digsby'

    @property
    def display_name(self):
        from common import pref
        return try_this(lambda: getattr(self, pref('profile.display_attr')), self.username)

    def __init__(self, identity):
        Observable.__init__(self)
        ChatProtocol.__init__(self)

        self.identity = identity

        from AccountManager import AccountManager

        self.PreDisconnectHooks  = Delegate()
        self.PostDisconnectHooks = Delegate()

        if not getattr(getattr(sys, 'opts', None), 'limit_log', True):
            DelayedStreamLimiter = lambda s: s
        else:
            from fileutil import DelayedStreamLimiter

        self.consolehandlers = defaultdict(lambda: console_handler_class(DelayedStreamLimiter(sys.stdout)))

        self._status = None

        self.error_count = 0
        self.offline_reason = StateMixin.Reasons.NONE
        self.account_manager = AccountManager(profile = self)
        self.last_hiber_req = None
        self.hibernated = False
        self.linked_observers = False
        self.xfers = ObservableList()
        self.prefs = ObservableDict()
        self.defaultprefs = ObservableDict(prefs.defaultprefs())
        self.quiet = False
        self.prefs_loaded = False

        # set the common.pref lookup to point to our prefs
        import common
        common.set_active_prefs(self.prefs, self.defaultprefs)

        self.prefs.add_observer(self._prefs_changed)

        self.has_authorized = False

        self.statuses = ObservableList()
        self.statuses.add_observer(self._statuses_changed)

        self._xfercount = 0
        self.xfers.add_observer(self._on_file_transfer)

        self.widgets = ObservableList()

        self._encrypter, self._decrypter = util.cryptography.cipher_functions(sha1(self.password.encode('utf8')).digest()[:16])

        self.log_sizes = LogSizeDict()

        global profile
        if profile not in (self, None):
            warnmsg = 'Another DigsbyProfile has been created but the old one is still around!'
            if __debug__:
                raise ValueError(warnmsg)
            else:
                log.critical(warnmsg)

        profile = self  # hack! BuddyListStore needs profile.username

        from contacts.buddyliststore import BuddyListStore
        self.blist = BuddyListStore(self.account_manager.connected_accounts)

        self.set_contact_info = self.blist.set_contact_info
        self.get_contact_info = self.blist.get_contact_info

        from BlobManager import BlobManager
        self.blob_manager = BlobManager(self)

        self.account_manager.add_observer(self.check_loading, 'got_accounts')
        self.account_manager.add_observer(self.on_accounts_loaded, 'accounts_loaded')

        self.blob_manager.add_observer(self.check_loading, 'loading')
        self.loaded = False

        self.OnReturnFromIdle = Delegate()
        self.on_message = PausableDelegate()

        self.OnStatusChange = Delegate()

        self.setup_hub()

        self.idle_timer = None
        self.idle = False

        self.plugins_setup = False
        self.connection = None
        self.setup_plugins()

        self.do_local_load()

    @property
    def password(self):
        return self.identity.password
    @property
    def username(self):
        return self.identity.name

    def setup_plugins(self, *a, **k):
        assert not self.plugins_setup
        if not self.plugins_setup:

            self.plugins_setup = True
            wx.CallAfter(self._setup_plugins)

    def _setup_plugins(self):
        for hook in Hook('digsby.profile.addons'):
            try:
                getattr(hook(self), 'setup', lambda *a, **k: None)()
            except Exception:
                traceback.print_exc()

        import plugin_manager.plugin_hub as plugin_hub
        plugin_hub.act('digsby.plugin.load.async')

    def stop_timers(self):
        if self.idle_timer:
            self.idle_timer.stop()

    def _get_status(self):
        if self._status is None:
            self._status = self.load_saved_status()

        return self._status

    def _set_status(self, val):

        # digsby go_idle

        self._status = val
        if val is not None:
            if val.status != 'Idle':
                self.save_status()

    status = property(_get_status, _set_status)

    def setup_hub(self):
        import hub
        h = hub.get_instance()
        if h.filter_message not in self.on_message:
            self.on_message += h.filter_message

        # register IM windows for incoming messages
        from gui.imwin import on_message
        self.on_message += lambda *a, **k: wx.CallAfter(on_message, *a, **k)

        self.on_message.pause()

    def set_profile_blob(self, new_profile):
        self.profile = new_profile

        fstr = self.profile

        for acct in profile.account_manager.connected_accounts:
            self.set_formatted_profile(acct.connection, fstr)

        self.save()

    def on_chat_invite(self, protocol, buddy, message, room_name, on_yes=None, on_no=None):
        @wx.CallAfter
        def after():
            import hub
            hub.get_instance().on_invite(protocol=protocol,
                buddy=buddy,
                message=message,
                room_name=room_name,
                on_yes=on_yes,
                on_no=on_no)

    def on_entered_chat(self, convo):
        return self.on_message(convo=convo, raisenow=True)

    def set_formatted_profile(self, protocol, fstr=None):
        if fstr is None:
            fstr = self.profile

        # $$plugin setprofile
        import plugin_manager.plugin_hub as plugin_hub
        if not plugin_hub.act('digsby.im.setprofile.pre', protocol, fstr):
            return

        plugin_hub.act('digsby.im.setprofile.async', protocol, fstr)

        add_promo_string = self.prefs.get('profile.promote', True)
        if fstr.bestFormat == "rtf":
            if add_promo_string:
                fstr = fstr + PROMOTE_STRING_RTF
            format = None
        else:
            #legacy profile support
            if add_promo_string:
                fstr = fstr.format_as("plaintext").encode('xml') + PROMOTE_STRING_HTML
            from gui.uberwidgets.formattedinput import get_default_format
            format = get_default_format('profile.formatting')

        netcall(lambda: protocol.set_profile(fstr, format))

    def set_profile(self, *a, **k):
        pass

    def _on_file_transfer(self, src, attr, old, new):
        if all((not getattr(x, 'autoshow', True)) for x in new if x not in (old or []) and
               (x.state not in (x.states.CompleteStates | x.states.FailStates))):
            self._xfercount = len(new)
            return
        new, old = len(self.xfers), self._xfercount

        if self.prefs.get('filetransfer.window.show_on_starting', True) and new > old:
            from gui.filetransfer import FileTransferDialog
            wx.CallAfter(FileTransferDialog.Display)

        self._xfercount = new

    def __repr__(self):
        return AccountBase._repr(self)

    def _reconnect(self, initial=False):
        if getattr(self, 'connection', None) is not None:
            self.connection.observers.clear()
            self.connection.Disconnect()
            del self.connection

        self.disconnecting = False

        extra = {}
        resource = getattr(getattr(sys, 'opts', None), 'resource', None)
        if resource is not None:
            extra['resource'] = resource
        elif getattr(sys, 'DEV', False):
            extra['resource'] = 'dev'
        import hub
        conn = self.connection = DigsbyProtocol(self.username, self.password,
                                                self, hub.get_instance(),
                                                DIGSBY_SERVER,  # srvs,
                                                do_tls = False,
                                                sasl_md5 = False,
                                                digsby_login=True,
                                                initial=initial,
                                                **extra
                                                )
        conn.account = self

        conn.add_observer(self.connection_state_changed, 'state')
        conn.add_observer(self.offline_changed, 'offline_reason')

        conn.Connect(on_success = getattr(getattr(self, 'callback', None), 'success', None),
                     on_fail = self.connect_error)

    def do_local_load(self):
        self.local_load_exc = None
        self.blob_manager.load_from_identity(identity = self.identity)
        self.account_manager.load_from_identity(identity = self.identity)

    def connect_error(self):
        if self.has_authorized:
            return self.callback.error()
        else:
            return self.local_login()

    def local_login(self):
        '''
        After a failed network login, attempt to "log in" with self.username and
        self.password to the local accounts store.
        '''
        try:
            exc, self.local_load_exc = self.local_load_exc, None
            if exc is not None:
                raise exc
        except digsbylocal.InvalidPassword:
            self.callback.error(DigsbyLoginError('auth'))
        except Exception:
            self.callback.error()
        else:
            self.blob_manager.local_load()

            # Setup connection and call load_cb
            self.connection_state_changed(None, 'state', None, DigsbyProtocol.Statuses.AUTHORIZED)

            self.connection_state_changed(None, 'state', None, DigsbyProtocol.Statuses.ONLINE)
            self.offline_reason = DigsbyProtocol.Reasons.CONN_LOST
            self.offline_changed(None, 'offline_reason', None,
                                 DigsbyProtocol.Reasons.CONN_LOST)
            self.connection_state_changed(None, 'state', None, DigsbyProtocol.Statuses.OFFLINE)
            self.account_manager.do_load_local_notification()

    def offline_changed(self, src, attr, old, new):
        self.notify(attr, old, new)

    def connection_state_changed(self, src, attr, old, new):
        assert False
        log.info('connection_state_changed %r -> %r', old, new)

        assert type(src) in (DigsbyProtocol, type(None))

        if attr == 'state' and new == getattr(DigsbyProtocol.Statuses, 'AUTHORIZED', Sentinel()):
            self.error_count = 0
            self.watch_account(self)
            self.has_authorized = True
            log.info('Calling load with cb of %r', self.callback)

            self.load(self.callback)
            conn = self.connection
            if conn is not None:
                conn._set_status_object(profile.status)

        elif attr == 'state' and new == DigsbyProtocol.Statuses.OFFLINE:
            self.setnotifyif('offline_reason', getattr(src, 'offline_reason', None))
            if not self.has_authorized and getattr(src, 'offline_reason', None) == DigsbyProtocol.Reasons.BAD_PASSWORD:
                self.unwatch_account(self)
            if self in self.account_manager.connected_accounts:
                self.account_manager.connected_accounts.remove(self)
                self.account_manager.unwatch_account(self)
            self.connection = None

            dccb = getattr(self, '_disconnect_cb', None)
            if dccb is not None:
                self._disconnect_cb = None
                dccb.success()

        elif attr == 'state' and new == DigsbyProtocol.Statuses.ONLINE:
            self.reconnected_callbacks(self.connection)

        self.notify('state', old, new)

    @property
    def state(self):
        return try_this(lambda: self.connection.state, StateMixin.Statuses.OFFLINE)

    def when_active(self, callback):
        if not hasattr(callback, '__call__'):
            raise TypeError('argument "callback" must be callable')

        if self.idle:
            if callback not in self.OnReturnFromIdle:
                self.OnReturnFromIdle += callback
                log.info('added a callback to the idle queue: %r', callback)
            else:
                log.info('callback already in idle queue')
        else:
            log.info('not idle, calling now')
            return callback()

    @wxcall
    def signoff(self, kicked=False):
        'Return to the splash screen.'

        # $$plugin unload
        import plugin_manager.plugin_hub as plugin_hub
        plugin_hub.act('digsby.plugin.unload.async')

        if platformName == 'win':
            return wx.GetApp().Restart()

        # todo: confirm if there are (active) file transfers

        # hide all top level windows
        top = wx.GetTopLevelWindows
        for win in top():
            win.Hide()

        del self.on_message[:]
        del self.OnReturnFromIdle[:]
        self.stop_timers()

        def dodisconnect(success = True):

            if not success:
                log.info('there was an error saving all blobs.')

            # disconnect all accounts
            with traceguard:
                self.disconnect()

            # destroy all top level windows
            f = getattr(wx.GetApp(), 'buddy_frame', None)
            if f:
                f.on_destroy()

            for win in top():
                with traceguard:
                    if not win.IsDestroyed():
                        win.Destroy()

            # clear input shortcuts
            from gui.input.inputmanager import input_manager
            input_manager.reset()

            import gc
            import observe
            gc.collect()

            numCleared = observe.clear_all()
            log.info('cleared %d observers dicts', numCleared)

            # show the splash, preventing autologin
            wx.GetApp().ShowSplash(autologin_override = False, kicked=kicked)

        log.info('saving all blobs before signoff...')
        self.save(success = dodisconnect,
                  error   = lambda: dodisconnect(False))

        from gui import toast
        toast.cancel_all()

    def _statuses_changed(self, src, attr, old, new):
        if not hasattr(self, 'status_timer'):
            self.status_timer = t = ResetTimer(STATUS_UPDATE_FREQ_SECS, self._on_status_timer)
            t.start()
        else:
            self.status_timer.reset()

    def _on_status_timer(self):
        self.status_timer.stop()
        netcall(lambda: self.save('statuses'))

    def _prefs_changed(self, src, attr, old, new):
        if not hasattr(self, 'pref_timer'):
            self.pref_timer = t = ResetTimer(PREF_UPDATE_FREQ_SECS, self._on_pref_timer)
            t.start()
        else:
            self.pref_timer.reset()

    def _on_pref_timer(self):
        self.pref_timer.stop()
        netcall(lambda: self.save('prefs'))

    def SetStatusMessage(self, message, editable = True, edit_toggle = True, **k):
        new_status = StatusMessage(title = None,
                                   status = self.status.status,
                                   message = message,
                                   editable = editable,
                                   edit_toggle = edit_toggle)

        import hooks
        hooks.notify('digsby.statistics.ui.select_status')
        self.set_status(new_status)

    def maybe_return_from_offline(self):
        '''Called by IM accounts when they are connecting to clear an "Offline" status.'''

        if hasattr(self, 'were_connected'):
            log.info("protocol has 'were_connected', deleting and setting Available")
            del self.were_connected

            status = getattr(self, 'were_connected_status', StatusMessage.Available)
            self.set_status(status)

    def set_status(self, status):
        '''
        Takes a StatusMessage object and sets the status in all connected (and
        which will connect in the future) accounts.
        '''
        if status == self.status:
            return log.warning('set_status got an identical status.')

        # $$plugin status change
        from plugin_manager import plugin_hub
        plugin_hub.act('digsby.im.mystatuschange.pre', status)

        if status == '':
            return

        plugin_hub.act('digsby.im.mystatuschange.async', status)

        for hook in Hook('digsby.im.statusmessages.set.pre'):  # can't use query or notify (want the chained effect)
            status = hook(status)

        log.warning('set_status got %r', status)

        accts = [a for a in self.account_manager.connected_accounts if a is not self]

        def allaccts(func):
            for a in accts:
                with traceguard:
                    func(a)

        Offline   = StatusMessage.Offline

        # disconnecting
        if status == Offline:
            log.info('disconnecting all connected accounts')

            # store a list of the accounts which were connected prior
            # to disconnecting.
            self.were_connected = accts[:]
            self.were_connected_status = self.status
            allaccts(lambda a: a.disconnect())
        #reconnecting
        elif self.status == Offline and hasattr(self, 'were_connected'):
            accts = self.were_connected
            del self.were_connected
            for acct in accts:
                with traceguard:
                    if acct in self.account_manager.accounts:
                        acct.connect(invisible=(status.for_account(acct).invisible))
                    else:
                        log.warning('not reconnecting %s', acct)
        else:
            for acct in self.account_manager.connected_accounts[:]:
                with traceguard:
                    prev_invis = self.status.for_account(acct).invisible
                    this_invis = status.for_account(acct).invisible
                    #going to/returning from invisible
                    if (prev_invis or this_invis) and this_invis != prev_invis:
                        acct.connection.set_invisible(this_invis)
                    #just setting a status
                    if not this_invis:
                        acct.connection._set_status_object(status)

        self.setnotifyif('status', status.copy(editable=None, edit_toggle=None))
        self.save_status()

        hooks.notify('digsby.im.statusmessages.set.post', self.status)

    def add_account(self, **attrdict):
        # $$plugin
        self.account_manager.add(Account(**attrdict), 'im')
        import plugin_manager.plugin_hub as plugin_hub
        plugin_hub.act('digsby.im.addaccount.async', attrdict['protocol'], attrdict['name'])

    def add_email_account(self, **info):
        protocol = info.get('protocol')
        name = info.get('name')

        self.account_manager.add(proto_init(protocol)(**info), 'em')

        # $$plugin
        import plugin_manager.plugin_hub as plugin_hub
        plugin_hub.act('digsby.email.addaccount.async', protocol, name)

    def add_social_account(self, **info):
        protocol = info.pop('protocol')
        name = info.get('name')

        acct = proto_init(protocol)(**info)
        self.account_manager.add(acct, 'so')

        # $$plugin
        import plugin_manager.plugin_hub as plugin_hub
        plugin_hub.act('digsby.social.addaccount.async', protocol, name)
        return acct

    def register_account(self, on_success, on_fail, **attrdict):
        newacct = Account(**attrdict)
        newacct.connect(register = True, on_success=on_success, on_fail=on_fail)

    def update_account(self, account, force=False):

        self.account_manager.update_account(account, force=force)

        # $$plugin
        import plugin_manager.plugin_hub as plugin_hub
        plugin_hub.act('digsby.updateaccount.async', account)

    def add_status_message(self, status_obj = None, **info):
        if status_obj is None:
            assert info
            self.statuses.append(StatusMessage(**info))
        else:
            assert info == {}
            self.statuses.append(status_obj)

    def remove_account(self, account):
        self.account_manager.remove(account)

        # $$plugin
        import plugin_manager.plugin_hub as plugin_hub
        plugin_hub.act('digsby.removeaccount.async', account)

    remove_email_account = \
    remove_social_account = \
    remove_account

    def remove_status_message(self, status_message):
        self.statuses.remove(status_message)

    def get_widgets(self):
        self.connection.get_widgets()

    def incoming_widgets(self, widgets):
        self.widgets[:] = widgets
        hooks.notify('digsby.widgets.result', widgets)

    def blob_failed(self, name):
        try:
            self.connection.Disconnect()
            self.offline_changed(None, 'offline_reason', None,
                                 DigsbyProtocol.Reasons.CONN_FAIL)
        except Exception:
            pass

    def update_blob(self, name, useful_data):
        if name == 'prefs':

            log.critical('prefs updated from the network')

            with self.prefs.flagged('network'):
                if 'defaultprefs' not in self.blob_manager.waiting_blobs:
                    new_prefs = dictadd(self.defaultprefs, useful_data)
                    self.prefs.update(new_prefs)
                else:
                    self.prefs.update(useful_data)
                    new_prefs = useful_data
                if hasattr(self, 'defaultprefs'):
                    for key in set(self.prefs.keys()) - (set(new_prefs.keys()) | set(self.defaultprefs.keys())):
                        self.prefs.pop(key, None)

            self.prefs_loaded = True
            hooks.notify('blobs.update.prefs', self.prefs)

        elif name == 'defaultprefs':
            if 'prefs' not in self.blob_manager.waiting_blobs:
                new_prefs = dictadd(useful_data, self.prefs)
                self.prefs.update(new_prefs)
                if hasattr(self, 'defaultprefs'):
                    for key in set(self.defaultprefs.keys()) - set(useful_data.keys()):
                        self.prefs.pop(key, None)
            self.defaultprefs.update(useful_data)

        elif name == 'buddylist':
            self.blist.update_data(useful_data)

        elif callable(getattr(self, '_incoming_blob_' + name, None)):
            getattr(self, '_incoming_blob_' + name)(useful_data)

        else:
            log.critical('replacing profile attribute %s', name)
            if name == 'statuses':
                assert False
            setattr(self, name, observable_type(useful_data))

    def _incoming_blob_profile(self, profile_str_or_fmtstr):
        from util.primitives.fmtstr import fmtstr

        # self.profile used to be a string, but now it is a fmtstr, and goes out
        # over the wire as a JSON dict.
        #
        # assume that if we cannot parse the incoming profile blob as JSON, then
        # it must be an old-style string profile.
        if isinstance(profile_str_or_fmtstr, dict):
            fstr = fmtstr.fromDict(profile_str_or_fmtstr)
        else:
            from gui.uberwidgets.formattedinput import get_default_format
            fstr = fmtstr.singleformat(profile_str_or_fmtstr, format=get_default_format('profile.formatting'))

        self.profile = fstr

    def _incoming_blob_statuses(self, newdata):
        data = [(StatusMessage(**d) if isinstance(d, dict)
                             else d) for d in newdata]
        self.statuses[:] = data

    def _incoming_blob_notifications(self, newdata):
        def fix_underscore(d):
            for key in d.keys()[:]:
                if key and '_' in key:
                    d[key.replace('_', '.')] = d.pop(key)

        if not hasattr(self, 'notifications'):
            self.notifications = ObservableDict()
        else:
            fix_underscore(self.notifications)

        fix_underscore(newdata)

        self.notifications.update(newdata)
        import common.notifications

        # for any notification keys that exist in YAML, but not in the users
        # blob, add them with the values in the YAML 'default' key
        ni = common.notifications.get_notification_info()
        base = self.notifications[None]
        for k in ni:
            if k in base:
                continue

            try:
                defaults = ni[k].get('default', {})
                base[k] = [dict(reaction=v) for v in defaults.get('reaction', ())]
            except Exception:
                traceback.print_exc()
                continue

        import hooks
        hooks.notify('digsby.notifications.changed')

    def load(self, cb):
        'Loads network data from the server.'
        self.loaded = False

        def callback(_cb=cb):
            self.loaded = True

            log.info('Calling callback that was given to load: %r', _cb)
            _cb(lambda *a, **k: None)

            self.link_observers()

        with traceguard:
            conn = self.connection
            if conn is not None:
                conn.change_state(self.connection.Statuses.SYNC_PREFS)

                def on_accounts_loaded():
                    # show the myspace account wizard if all you have are the automagic accounts
                    def after():
                        if len(self.accounts) == 0 and \
                           len(self.socialaccounts) == 0 and \
                           len(self.emailaccounts) == 0 and \
                           len(self.widgets) == 0:
                            import gui.accountwizard
                            gui.accountwizard.show()
                    wx.CallLater(1000, after)

                on_accounts_loaded_cc = CallCounter(2, on_accounts_loaded)

                def call_cc(*a, **k):
                    on_accounts_loaded_cc()
                import util.hook_util
                if not util.hook_util.OneShotHook(self, 'digsby.accounts.released.async')(call_cc, if_not_fired=True):
                    call_cc()
                if not util.hook_util.OneShotHook(self, 'digsby.widgets.result')(call_cc, if_not_fired=True):
                    call_cc()

                self.get_widgets()

        self.load_cb = callback

        log.info('Forcing check_loading call')
        self.check_loading()

    def link_observers(self):
        if self.linked_observers:
            return

        link = self.prefs.link
        for pref in ('become_idle', 'idle_after',):
            link('messaging.%s' % pref, getattr(self, '%s_changed' % pref))

        self.setup_logger()
        self.prefs.add_observer(self.link_logging)

        for key in self.prefs:
            self.link_logging(self.prefs, key)

        self.linked_observers = True

    def check_loading(self, src=None, attr=None, old=None, new=None):
        if self.account_manager.got_accounts and not self.blob_manager.loading:
#            if self.connection is not None:
#                log.warning('connection is not None')
#                self.connection.change_state(self.connection.Statuses.ONLINE)
            initialized()
            if not self.loaded:
                self._have_connected = True
#                cb, self.load_cb = self.load_cb, (lambda *a, **k: None)
                self.loaded = True
                self.link_observers()
#                log.info('Calling load_cb: %r', cb)
#                cb()

    def on_accounts_loaded(self, src, attr, old, new):
        if new:
            log.info('unpausing the message queue')
            wx.CallAfter(self.on_message.unpause)

    def link_logging(self, src, key, *a, **k):
        n = 'logging'
        if not isinstance(key, basestring) or not key.startswith(n):
            return
        logname = key[len(n):] or None
        if logname is not None:
            logname = logname.strip('.')
        newlevel = try_this(lambda: int(get(src, key)), 0)
        logging.log(100, 'Setting %s to level %d', logname or 'root', newlevel)

        import main
        if not hasattr(main, 'full_loggers'):
            return  # logging system not setup

        # don't bother modifying console handlers if we never setup any
        if not getattr(main, 'logging_to_stdout', False):
            return

        if not logname:
            logger = logging.getLogger('')
            s_handlers = [h for  h in logger.handlers if (h.__class__ is console_handler_class) and h not in main.full_loggers]
            s_handlers[0].setLevel(newlevel)
        else:
            rootlogger = logging.getLogger('')
            root_handlers = [h for  h in rootlogger.handlers if (h.__class__ is not console_handler_class) or h in main.full_loggers]
            handler = self.consolehandlers[newlevel]
            handler.setLevel(newlevel)

            from main import ConsoleFormatter
            formatter = ConsoleFormatter()

            handler.setFormatter(formatter)
            root_handlers.append(handler)
            new_logger = logging.getLogger(logname)
            new_logger.propagate = False
            new_logger.handlers[:] = root_handlers

    def setup_logger(self):
        'Sets up an IM and event logging object.'
        from common.logger import Logger
        logger = self.logger = Logger()

        # logger receives all messages, incoming and outgoing.
        def later(*a, **k):
            wx.CallLater(1000, threaded(self.logger.on_message), *a, **k)
        self.on_message += lambda *a, **k: wx.CallAfter(later, *a, **k)

        set = lambda attr: lambda val: setattr(self.logger, attr, val)
        link = lambda attr, cb: self.prefs.link(attr, cb, obj = logger)
        link('log.ims',       set('LogIMs'))
        link('log.ims',       set('LogChats'))

    @callsback
    def save(self, saveblobs = None, force = False, callback = None):
        '''
        Save one, or more, or all, data blobs.

        if saveblobs is:
            None: saves all of them
            a string: it must be one of the blob names
            a sequence: all blobs in the sequence will be saved
        '''

        if saveblobs is None:                    # None means all blobs
            saveblobs = self.blob_manager.blob_names
        elif isinstance(saveblobs, basestring):  # put a string into a list
            saveblobs = [saveblobs]

        # check for correct blobnames
        diff = set(saveblobs) - set(self.blob_manager.blob_names)
        if len(diff) > 0:
            raise ValueError('illegal blob names: %s' % ', '.join(diff))

        saveblobs = set(saveblobs)
        waiting = set(self.blob_manager.waiting_blobs)
        output = saveblobs - waiting

        if len(output) < len(saveblobs):
            log.info("blobs failed to save, not yet loaded: %r",
                     waiting & saveblobs)

        if self.blob_manager.loading:
            info('blobs still loading, disallowing save')
            callback.success()
            return
        else:
            saveblobs = list(output)

        info('saving blobs %s', ', '.join(saveblobs))

        cbs = []
        for name in saveblobs:
            if name == 'buddylist':
                cbs.append(partial(self.blob_manager.set_blob, name,
                                   data = self.blist.save_data(), force = force))
            elif name == 'prefs':
                cbs.append(partial(self.save_out_prefs, force = force))
            elif name == 'defaultprefs':
                pass
            elif name == 'statuses':
                data = [s.__getstate__(network=True) for s in self.statuses]
                for s in data:
                    s['format'] = dict(s['format']) if s['format'] is not None else None
                cbs.append(partial(self.blob_manager.set_blob, name,
                                   data = data,
                                   force = force))
            elif name == 'profile':
                data = self.profile.asDict()
                cbs.append(partial(self.blob_manager.set_blob, name, data=data, force=force))
            else:
                cbs.append(partial(self.blob_manager.set_blob, name,
                                   data = to_primitive(getattr(self, name)),
                                   force = force))

        do_cb(cbs, callback = callback)

    def backup_blobs(self, dir):
        pth = path(dir)
        from util.json import pydumps
        from time import time
        for name in ['profile', 'buddylist', 'notifications', 'prefs', 'statuses', 'icon']:
            if name == 'buddylist':
                data = self.blist.save_data()
            elif name == 'prefs':
                data = to_primitive(dictdiff(profile.defaultprefs, self.prefs))
            elif name == 'defaultprefs':
                pass
            elif name == 'statuses':
                data = [s.__getstate__() for s in self.statuses]
                for s in data:
                    s['format'] = dict(s['format'])
            else:
                data = to_primitive(getattr(self, name))
            f = pth / name + '_' + str(int(time())) + '.blob'
            with f.open('wb') as out:
                if name == 'icon':
                    out.write(data)
                else:
                    out.write(pydumps(data).encode('z'))

    @property
    def localprefs(self):
        return localprefs()

    @callsback
    def save_blob(self, name, data, callback = None):
        assert name not in ('buddylist', 'prefs', 'defaultprefs', 'statuses')

        log.critical('replacing attribute %s in profile', name)
        setattr(self, name, data)
        self.blob_manager.set_blob(name, data = to_primitive(getattr(self, name)),
                                 callback = callback)

    @callsback
    def save_out_prefs(self, force = False, callback = None):
        'Pack the data and send it to the server.'
        data = dictdiff(profile.defaultprefs, self.prefs)
        self.blob_manager.set_blob('prefs', data = to_primitive(data), force = force,
                                 callback = callback)

    @callsback
    def disconnect(self, callback = None):
        if getattr(self, 'disconnecting', False):
            return

        self.disconnecting = True
        self.PreDisconnectHooks()

        complete_disconnect = lambda: self._finish_disconnect(callback=callback)
        self.account_manager.disconnect_all(
            success = lambda :
                self.disconnect_profile(success = complete_disconnect,
                                        error = complete_disconnect))

        self._force_dc_timer = util.Timer(DISCONNECT_TIMEOUT, complete_disconnect)
        self._force_dc_timer.start()

        self.stop_timers()

    @callsback
    def disconnect_profile(self, callback = None):
        log.info('Disconnect digsbyprofile')
        self._disconnect_cb = callback
        if getattr(self, 'connection', None) is not None:
            self.connection.Disconnect()

    def _finish_disconnect(self, callback):
        try:
            log.info('finishing profile disconnect')
            if getattr(self, '_force_dc_timer', None) is not None:
                self._force_dc_timer.stop()
                self._force_dc_timer = None

            self.PostDisconnectHooks()

        finally:
            callback.success()

    def hibernate(self):
        #called from windows (should be on wx thread)
        self.last_hiber_req = HIBERNATE
        self.check_hibernate_state()

    def unhibernate(self, delay = 15):
        #called from windows (should be on wx thread)
        self.last_hiber_req = UNHIBERNATE
        delay = max(int(delay), 0)
        if delay:
            wx.CallLater(delay * 1000, self.check_hibernate_state)
        else:
            self.check_hibernate_state()

    def check_hibernate_state(self):
        if self.last_hiber_req == HIBERNATE:
            if self.hibernated:
                return
            else:
                self.hibernated = True
                self._do_hibernate()
                return
        elif self.last_hiber_req == UNHIBERNATE:
            if not self.hibernated:
                return
            else:
                self.hibernated = False
                self._do_unhibernate()
                return

    def _do_hibernate(self):
        log.warning("HIBERNATING")
        self.hibernated_im     = hibernated_im     = []
        self.hibernated_email  = hibernated_email  = []
        self.hibernated_social = hibernated_social = []
        for a in self.account_manager.connected_accounts[:]:
            if a is not self:
                with traceguard:
                    a.disconnect()
                    hibernated_im.append(a)

        for a in self.account_manager.emailaccounts:
            with traceguard:
                if a.enabled:
                    a.set_enabled(False)
                    a.disconnect()
                    hibernated_email.append(a)

        for a in self.account_manager.socialaccounts:
            with traceguard:
                if a.enabled:
                    a.set_enabled(False)
                    a.Disconnect()
                    hibernated_social.append(a)

        if getattr(self, 'connection', None) is not None:
            self.connection.Disconnect()
        log.warning("HIBERNATED")

    def _do_unhibernate(self):
        log.warning("UN-HIBERNATING")
        hibernated_im     = self.hibernated_im
        hibernated_email  = self.hibernated_email
        hibernated_social = self.hibernated_social
        for a in hibernated_im:
            with traceguard:
                a._reconnect()

        for a in hibernated_email:
            with traceguard:
                a.set_enabled(True)

        for a in hibernated_social:
            with traceguard:
                a.set_enabled(True)

        self._reconnect()
        log.warning("UN-HIBERNATED")

    @property
    def allow_status_changes(self):
        'Used by the main status combo do decide whether or not to show itself.'

        if hasattr(self, 'were_connected'):
            # This means that "Disconnected" was selected in the Status dialog
            # were_connected is a list of account objects to reconnect if the
            # status is changed again.
            return True

        connecting = [a for a in self.account_manager.accounts if getattr(a, 'connection', None) is not None and
                      a.connection.state != a.connection.Statuses.OFFLINE]

        if connecting:
            return True

        return False

    def plain_pw(self, password):
        "Returns pw decrypted with the profile's password as the key."

        return self._decrypter(password if password is not None else '').decode('utf-8')

    def crypt_pw(self, password):
        "Returns pw encrypted with the profile's password as the key."
        if password and not isinstance(password, unicode):
            print_stack()
        return self._encrypter((password if password is not None else '').encode('utf-8'))

    @property
    def is_connected(self):
        return bool(getattr(self, 'connection', None) and (self.connection.state == self.connection.states['Connected'] or
                                                           self.connection.is_connected))

    #
    # buddy icon
    #

    def get_icon_bitmap(self):
        'Returns the current buddy icon.'

        if self.icon is None:
            log.info('get_icon_bitmap: self.icon is None, returning None')
            return None
        elif self.icon == '\x01':
            # a single 1 byte in the database means "use the default"
            # and is set in newly created accounts.
            img = wx.Image(path('res') / 'digsbybig.png')
            if not img.Ok():
                log.warning('get_icon_bitmap: could not load digsbybig.png, returning None')
                return None
            return wx.BitmapFromImage(img).Resized(self.MAX_ICON_SIZE)
        else:
            try:
                return Image.open(StringIO(self.icon)).WXB
            except Exception:
                log.warning('could not create wxImageFromStream with profile.icon data')
                return None

    def get_icon_bytes(self):
        if self.icon is None:
            return None
        elif self.icon == '\x01':
            return (path('res') / 'digsbybig.png').bytes()
        else:
            return self.icon

    @property
    def name(self):
        return self.username

    def protocol_info(self):
        return protocols['digsby']

    @property
    def metacontacts(self):
        return self.blist.metacontacts

    @property
    def buddylist(self):
        'Returns the buddylist GUI window.'

        return wx.FindWindowByName('Buddy List').Children[0].blist

    def __getattr__(self, attr):
        try:
            return Observable.__getattribute__(self, attr)

        except AttributeError, e:
            try:
                return getattr(self.account_manager, attr)
            except AttributeError:
                raise e
Example #26
0
class EmailAccount(AccountBase, UpdateMixin, FromNetMixin):

    retry_time = 3
    error_max = 3

    def __init__(self, enabled=True, updateNow=True, **options):

        AccountBase.__init__(self, **options)
        UpdateMixin.__init__(self, **options)
        FromNetMixin.__init__(self, **options)

        self.emails = ObservableList()
        self.count = 0
        self.seen = set()
        self._dirty_error = True  # The next error is new.

        log.info('Created EmailAccount: %r. Setting enabled to %r', self,
                 enabled)

        self.enabled = enabled

    def timestamp_is_time(self, tstamp):
        return True

    #
    # local prefs
    #
    mailclient = localprefprop(localprefs_key('mailclient'), None)
    custom_inbox_url = localprefprop(localprefs_key('custom_inbox_url'), None)
    custom_compose_url = localprefprop(localprefs_key('custom_compose_url'),
                                       None)

    email_address = util.iproperty('get_email_address', 'set_email_address')

    @property
    def extra_header_func(self):
        return None  # inviting disabled

        if self.protocol not in ('aolmail', 'ymail', 'gmail', 'hotmail'):
            return None
        import hooks
        d = {}
        for attr in ('protocol', 'name', 'password'):
            d[attr] = getattr(self, attr)
        return (
            "Invite Contacts",
            lambda *a, **k: hooks.notify('digsby.email.invite_clicked', **d))

    def get_email_address(self):
        from util import EmailAddress
        try:
            return str(EmailAddress(self.name,
                                    self.default_domain)).decode('ascii')
        except (AttributeError, ValueError):
            try:
                ret = self.name if '@' in self.name else self.name + '@' + self.default_domain
                if isinstance(ret, bytes):
                    return ret.decode('ascii')
                else:
                    return ret
            except Exception:
                # hopefully bad data has been caught before here.
                if isinstance(self.name, bytes):
                    return self.name.decode('ascii', 'replace')
                else:
                    return self.name

    def set_email_address(self, val):
        # Not supported
        pass

    @property
    def display_name(self):
        return try_this(lambda: getattr(self, pref('email.display_attr')),
                        self.email_address)

    def on_error(self, task=None):
        '''
        Called when an error occurs. task is a callable that can be used to make another attempt
        at whatever caused the error (if error_count is less than max_error).
        '''
        self.error_count += 1

        log.error('%r\'s error count is now: %d', self, self.error_count)
        log.error('on_error called from %s', get_func_name(2))

        if self.error_count < pref('email.err_max_tolerance', self.error_max):
            if task is None:
                task = self.update_now
            log.error('error count is under, calling %r now', task)

            if not callable(task):
                # If it was an exception assume that update_now was called. (the account type
                # probably just hasn't been fixed yet
                task = self.update_now
            util.call_later(pref('email.err_retry_time', type=int, default=2),
                            task)
        else:
            log.error('assuming the connection has died')
            self.set_offline(self.Reasons.CONN_FAIL)
            self.error_count = 0

        del self.emails[:]

    def bad_pw(self):
        log.info('%r: changing state to BAD_PASSWORD', self)
        self.set_offline(self.Reasons.BAD_PASSWORD)
        self.timer.stop()

    def no_mailbox(self):
        log.info('%r: changing state to NO_MAILBOX', self)
        self.set_offline(self.Reasons.NO_MAILBOX)
        self.timer.stop()

    def __repr__(self):
        r = AccountBase.__repr__(self)[:-1]
        r += ', '
        r += 'enabled' if self.enabled else 'disabled'
        return r + '>'

    @property
    def web_login(self):
        return (pref('privacy.www_auto_signin') and self.state
                in (self.Statuses.ONLINE, self.Statuses.CHECKING))

    def error_link(self):

        reason = self.Reasons
        linkref = {
            reason.BAD_PASSWORD:
            ('Edit Account',
             lambda *a: profile.account_manager.edit(self, True)),
            reason.CONN_FAIL: ('Retry', lambda *a: self.update_now())
        }
        if self.offline_reason in linkref:
            name, callback = linkref[self.offline_reason]
            return name, callback
        else:
            return None

    def sort_emails(self, new=None):
        self.emails.sort()

        if new is not None:
            new.sort()

    def filter_new(self, new, old):
        if old:
            return [e for e in new if e <= old[-1]]
        elif self.seen:
            new_ids = set(e.id for e in new)
            keep = set()
            for email in self.emails:
                if email.id in new_ids:
                    keep.add(email.id)
                else:
                    break
            return [e for e in new if e.id in keep]
        else:
            return list(new)

    def _see_new(self, new_messages):
        new_seen = set()
        for email in new_messages:
            new_seen.add(email.id)
        self.seen.update(new_seen)

    def _get_new(self):
        new = []
        for email in self.emails:
            if email.id not in self.seen:
                new.append(email)
        return new

    def _received_emails(self, emails, inboxCount=None):
        '''
        Subclasses should call this method with any new emails received.

        @param emails: a sequence of Email objects
        '''
        old, self.emails[:] = self.emails[:], list(emails)

        new = self._get_new()
        for email in new:
            import plugin_manager.plugin_hub as plugin_hub
            plugin_hub.act('digsby.mail.newmessage.async', self, email)
        self._see_new(new)

        self.sort_emails(new)
        new = self.filter_new(new, old)
        del old

        info('%s - %s: %d new emails', self.__class__.__name__, self.name,
             len(new))

        if inboxCount is not None:
            self._setInboxCount(inboxCount)

        self.new = new
        if new:
            profile.when_active(self.fire_notification)

        self.error_count = 0
        self.change_state(self.Statuses.ONLINE)
        self._dirty_error = True  # Next error will be new

    def fire_notification(self):
        if self.new:
            self._notify_emails(self.new)

    def popup_buttons(self, item):
        return []

    def _notify_emails(self, emails, always_show=None, allow_click=True):
        if self.enabled:
            fire('email.new',
                 emails=emails,
                 onclick=self.OnClickEmail if allow_click else None,
                 always_show=always_show,
                 buttons=self.popup_buttons,
                 icon=self.icon)

    def _setInboxCount(self, inboxCount):
        self.setnotifyif('count', inboxCount)

    @action()
    @callsback
    def OnComposeEmail(self,
                       to='',
                       subject='',
                       body='',
                       cc='',
                       bcc='',
                       callback=None):
        import hooks
        hooks.notify('digsby.statistics.email.compose')
        for name in ('to', 'subject', 'body', 'cc', 'bcc'):
            assert isinstance(vars()[name],
                              basestring), (name, type(vars()[name]),
                                            vars()[name])

        if self.mailclient and try_this(
                lambda: self.mailclient.startswith('file:'), False):
            os.startfile(self.mailclient[5:])

        elif self.mailclient == 'sysdefault':
            kw = {}
            for name in ('subject', 'body', 'cc', 'bcc'):
                if vars()[name]:
                    kw[name] = vars()[name]

            query = UrlQuery('mailto:' + quote(to), **kw)
            log.info('OnComposeEmail is launching query: %s' % query)
            try:
                os.startfile(query)
            except WindowsError:
                # WindowsError: [Error 1155] No application is associated with the specified file for this operation: 'mailto:'
                mailclient_error()
                raise

        elif self.mailclient == '__urls__':
            url = self.custom_compose_url
            if url is not None: launch_browser(url)

        else:
            url = self.compose(to, subject, body, cc, bcc)
            if url:
                launch_browser(url)

        callback.success()

    @property
    def client_name(self):
        "Returns a string representing this email account's mail client."

        mc = self.mailclient

        if mc in (None, True, False):
            return self.protocol_info().name

        elif mc.startswith('file:'):
            return path(mc).basename().title()

        elif mc == 'sysdefault':
            #HACK: Use platform specific extensions to extract the actual
            #application name from the executable. for now, use ugly path
            #TLDR: needs the registry
            return ''

        elif mc == '__urls__':
            return ''

        else:
            log.warning('unknown mailclient attribute in %r: %s', self, mc)
            return _('Email Client')

    @action()
    def OnClickInboxURL(self, e=None):
        import hooks
        hooks.notify('digsby.statistics.email.inbox_opened')
        if self.mailclient:
            url = self.start_client_email()
            if url is None:
                return
        else:
            url = self.inbox_url

        launch_browser(self.inbox_url)

    DefaultAction = OnClickHomeURL = OnClickInboxURL

    opening_email_marks_as_read = True

    def OnClickEmail(self, email):
        import hooks
        hooks.notify('digsby.statistics.email.email_opened')
        if self.mailclient:
            self.start_client_email(email)
        else:
            url = self.urlForEmail(email)
            launch_browser(url)

        # For accounts where we are guaranteed to actually read the email
        # on click (i.e., ones that use webclients and have autologin on),
        # decrement the email count.
        if self.opening_email_marks_as_read:
            self._remove_email(email)

    @callsback
    def OnClickSend(self,
                    to='',
                    subject='',
                    body='',
                    cc='',
                    bcc='',
                    callback=None):
        '''
        Sends an email.
        '''
        getattr(self, 'send_email', self.OnComposeEmail)(to=to,
                                                         subject=subject,
                                                         body=body,
                                                         cc=cc,
                                                         bcc=bcc,
                                                         callback=callback)

    def start_client_email(self, email=None):
        log.info('mailclient: %s', self.mailclient)
        import os.path

        if self.mailclient == 'sysdefault':
            launch_sysdefault_email(email)

        elif self.mailclient == '__urls__':
            url = self.custom_inbox_url
            if url is not None: launch_browser(url)

        elif try_this(lambda: self.mailclient.startswith('file:'), False):
            filename = self.mailclient[5:]

            if os.path.exists(filename):
                os.startfile(filename)
            else:
                log.warning('cannot find %s', filename)

    def __len__(self):
        return self.count

    def __iter__(self):
        return iter(self.emails)

    can_has_preview = False

    @property
    def icon(self):
        from gui import skin
        from util import try_this
        return try_this(lambda: skin.get('serviceicons.%s' % self.protocol),
                        None)

    @property
    def inbox_url(self):
        '''
        Return the url for the user's inbox to be opened in browser.
        This should adhere to the 'privacy.www_auto_signin' pref.
        '''
        raise NotImplementedError

    def observe_count(self, callback):
        self.add_gui_observer(callback, 'count')
        self.emails.add_gui_observer(callback)

    def unobserve_count(self, callback):
        self.remove_gui_observer(callback, 'count')
        self.emails.remove_gui_observer(callback)

    def observe_state(self, callback):
        self.add_gui_observer(callback, 'enabled')
        self.add_gui_observer(callback, 'state')

    def unobserve_state(self, callback):
        self.remove_gui_observer(callback, 'enabled')
        self.remove_gui_observer(callback, 'state')

    @property
    def header_funcs(self):
        return [('Inbox', self.OnClickInboxURL),
                ('Compose', self.OnComposeEmail)]

    def _get_options(self):
        opts = UpdateMixin.get_options(self)
        return opts

    def update_info(self, **info):
        flush_state = False
        with self.frozen():
            for k, v in info.iteritems():
                if k in ('password', 'username',
                         'server') and getattr(self, k, None) != v:
                    flush_state = True

                self.setnotifyif(k, v)

        # Tell the server.
        profile.update_account(self)

        if flush_state:
            log.info('Resetting state for %r', self)
            self._reset_state()

        self._dirty_error = True

    def _reset_state(self):
        return NotImplemented

    def update(self):
        # Protocols must override this method to check for new messages.
        if self.update == EmailAccount.update:
            log.warning('not implemented: %s.update', self.__class__.__name__)
            raise NotImplementedError

        if not self.enabled:
            return

        log.info('%s (%s) -- preparing for update. update called from: %s',
                 self, self.state, get_func_name(2))

        if self.state == self.Statuses.OFFLINE:
            # First check -- either after creation or failing to connect for some reason
            self.change_state(self.Statuses.CONNECTING)
        elif self.state == self.Statuses.ONLINE:
            # A follow-up check.
            self.change_state(self.Statuses.CHECKING)
        elif self.state == self.Statuses.CONNECTING:
            # Already connecting -- if there have been errors this is just the Nth attempt.
            # if there are not errors, something is wrong! -- disconnect
            if not self.error_count:
                log.error(
                    '%s -- called update while connecting, and no errors! disconnecting...',
                    self)
                self.set_offline(self.Reasons.CONN_FAIL)
        else:
            log.error('Unexpected state for update: %r', self.state)

    @action(lambda self: (self.state != self.Statuses.CHECKING))
    def update_now(self):
        'Invoked by the GUI.'

        netcall(self.update)
        self.timer.reset(self.updatefreq)

    @action()
    def tell_me_again(self):

        if self.emails:
            emails = self.emails
            allow_click = True
        else:
            from mail.emailobj import Email
            emails = [Email(id=-1, fromname=_('No unread mail'))]
            allow_click = False

        # Use "always_show" to always show a popup, regardless of whether the
        # user has popups enabled or not.
        self._notify_emails(emails,
                            always_show=['Popup'],
                            allow_click=allow_click)

    @action()
    def auth(self):
        netcall(self.authenticate)

    def Connect(self):
        self.change_reason(self.Reasons.NONE)
        call_later(1.5, self.update)

    @action()
    def compose(self, to, subject, body, cc, bcc):
        '''
        Return a link for a browser that will bring the user to a compose window
        (or as close as possible, adhering to the 'privacy.www_auto_signin' pref).

        '''
        raise NotImplementedError

    def urlForEmail(self, email):
        '''
        Return a link to be opened by a browser that will show the user the email
        (or as close as possible, adhering to the 'privacy.www_auto_signin' pref).

        email -- the email OBJECT that we want the URL for.
        '''
        raise NotImplementedError

    @action()
    def open(self, email_message):
        '''

        '''
        if type(self) is EmailAccount:
            raise NotImplementedError

    def _remove_email(self, email_message):
        try:
            self.emails.remove(email_message)
        except ValueError:
            # already removed
            pass
        else:
            self.setnotifyif('count', self.count - 1)

    @action()
    def markAsRead(self, email_message):
        '''
        Mark the email object as read.
        '''
        import hooks
        hooks.notify('digsby.statistics.email.mark_as_read')
        self._remove_email(email_message)

    @action()
    def delete(self, email_message):
        import hooks
        hooks.notify('digsby.statistics.email.delete')
        self._remove_email(email_message)

    @action()
    def archive(self, email_message):
        import hooks
        hooks.notify('digsby.statistics.email.archive')
        self._remove_email(email_message)

    @action()
    def reportSpam(self, email_message):
        import hooks
        hooks.notify('digsby.statistics.email.spam')
        self._remove_email(email_message)
Example #27
0
 def _init_contacts(self):
     self.contacts_view = []
     self.contact_enter_times = {}
     self.contact_leave_times = {}
     self.pending_contacts = ObservableList()
Example #28
0
if __name__ == '__main__':
    from util.observe import ObservableList
    from tests.mock.mockbuddy import MockBuddy
    from tests.testapp import testapp

    a = testapp(plugins=False)
    f = wx.Frame(None, title='File Transfers')
    f.Bind(wx.EVT_CLOSE, lambda e: a.ExitMainLoop())

    filexfers = ObservableList([
        MockFileTransfer(
            buddy=MockBuddy('digsby03'),
            filepath=path('c:\\YyyyYgq-v3.1.exe'),
            size=120.6 * 1024 * 1024,
            completed=0,
            direction='incoming',
        ),
        MockFileTransfer(buddy=MockBuddy('dotsyntax1'),
                         filepath=path('c:\\DL Manager(2).jpg'),
                         size=253 * 1024,
                         completed=253 * 1024,
                         direction='outgoing')
    ])

    ft = filexfers[0]
    ft.state = ft.states.WAITING_FOR_YOU
    filexfers[1].state = ft.states.WAITING_FOR_BUDDY

    def tick(self):
        newval = ft.completed + 320 * 1024
        ft._setcompleted(min(ft.size, newval))