Esempio n. 1
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()
Esempio n. 2
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