Пример #1
0
class Controller(object):
    """Controller of the program."""

    INFO_MODE = 0
    TIMELINE_MODE = 1
    HELP_MODE = 2

    # -- Initialization -------------------------------------------------------

    def __init__(self, configuration, ui, api_backend):
        self.configuration = configuration
        self.ui = ui

        # Mode
        self.mode = self.INFO_MODE

        # API
        self.info_message(_('Initializing API'))
        oauth_token = self.configuration.oauth_token 
        oauth_token_secret = self.configuration.oauth_token_secret
        self.api = AsyncApi(api_backend,
                            access_token_key=oauth_token,
                            access_token_secret=oauth_token_secret,)
        self.api.init_api(on_error=self.api_init_error,
                          on_success=self.init_timelines,)

        # start main loop
        try:
            self.main_loop()
        except TweepError:
            self.error_message(_('API error'))
        except:
            exit(1)

    def main_loop(self):
        """
        Main loop of the program, `Controller` subclasses must override this 
        method.
        """
        raise NotImplementedError

    def exit(self):
        """Exit the program."""
        raise NotImplementedError

    @async
    def init_timelines(self):
        # API has to be authenticated
        while (not self.api.is_authenticated):
            pass
        self.user = self.api.verify_credentials()
        self.info_message(_('Initializing timelines'))
        self.timelines = VisibleTimelineList()
        self.append_default_timelines()
        seconds = self.configuration.update_frequency
        self.loop.set_alarm_in(seconds, self.update_alarm)

    def reload_configuration(self):
        raise NotImplementedError

    # -- Callbacks ------------------------------------------------------------

    def api_init_error(self):
        # TODO retry
        self.error_message(_('Couldn\'t initialize API'))

    def update_alarm(self, *args, **kwargs):
        seconds = self.configuration.update_frequency
        self.update_all_timelines()
        self.loop.set_alarm_in(seconds, self.update_alarm)

    # -- Modes ----------------------------------------------------------------

    def timeline_mode(self):
        """
        Activates the Timeline mode if there are Timelines.
        
        If not, shows program info.
        """
        if self.is_in_timeline_mode():
            return
        elif self.timelines.has_timelines():
            self.mode = self.TIMELINE_MODE
            self.draw_timelines()
        else:
            self.mode = self.INFO_MODE
            self.ui.show_info()
        self.clear_status()
        self.redraw_screen()

    def is_in_timeline_mode(self):
        return self.mode == self.TIMELINE_MODE

    def info_mode(self):
        self.mode = self.INFO_MODE
        self.ui.show_info()
        self.redraw_screen()

    def is_in_info_mode(self):
        return self.mode == self.INFO_MODE

    def help_mode(self):
        """Activate help mode."""
        if self.is_in_help_mode():
            return
        self.mode = self.HELP_MODE
        self.ui.show_help(self.configuration)
        self.redraw_screen()

    def is_in_help_mode(self):
        return self.mode == self.HELP_MODE

    # -- Timelines ------------------------------------------------------------

    @wrap_exceptions
    def append_timeline(self, 
                        name, 
                        update_function, 
                        update_args=None,
                        update_kwargs=None):
        """
        Given a name, function to update a timeline and optionally
        arguments to the update function, it creates the timeline and
        appends it to `timelines`.
        """
        timeline = Timeline(name=name,
                            update_function=update_function,
                            update_function_args=update_args,
                            update_function_kwargs=update_kwargs) 
        timeline.update()
        timeline.activate_first()
        self.timelines.append_timeline(timeline)
        if self.is_in_info_mode():
            self.timeline_mode()
        self.draw_timelines()

    @async
    def append_default_timelines(self):
        default_timelines = {
            HOME_TIMELINE:       self.append_home_timeline,
            MENTIONS_TIMELINE:   self.append_mentions_timeline,
            FAVORITES_TIMELINE:  self.append_favorites_timeline,
            MESSAGES_TIMELINE:   self.append_direct_messages_timeline,
            OWN_TWEETS_TIMELINE: self.append_own_tweets_timeline,
        }

        timelines = [
            HOME_TIMELINE,      
            MENTIONS_TIMELINE,  
            FAVORITES_TIMELINE, 
            MESSAGES_TIMELINE,  
            OWN_TWEETS_TIMELINE,
        ]

        is_any = any([self.configuration.default_timelines[timeline] 
                      for timeline in timelines])
                                                    
        if is_any:
            self.timeline_mode()
        else:
            self.info_message(_('You don\'t have any default timelines activated'))
            return 

        for timeline in timelines:
            append = default_timelines[timeline]
            if self.configuration.default_timelines[timeline]:
                append()
                self.draw_timelines()
        self.clear_status()

    def append_home_timeline(self):
        timeline_fetched = partial(self.info_message, 
                                    _('Home timeline fetched'))
        timeline_not_fetched = partial(self.error_message, 
                                        _('Failed to fetch home timeline'))

        self.append_timeline(name=_('tweets'),     
                             update_function=self.api.get_home_timeline,
                             on_error=timeline_not_fetched,
                             on_success=timeline_fetched,)
                              
    def append_user_timeline(self, username):
        timeline_fetched = partial(self.info_message, 
                                    _('@%s\'s tweets fetched' % username))
        timeline_not_fetched = partial(self.error_message, 
                                        _('Failed to fetch @%s\'s tweets' % username))

        self.append_timeline(name='@%s' % username,
                             update_function=self.api.get_user_timeline,
                             update_kwargs={'screen_name': username},
                             on_error=timeline_not_fetched,
                             on_success=timeline_fetched,)

    def append_own_tweets_timeline(self):
        timeline_fetched = partial(self.info_message, 
                                    _('Your tweets fetched'))
        timeline_not_fetched = partial(self.error_message, 
                                        _('Failed to fetch your tweets'))

        if not hasattr(self, 'user'):
            self.user = self.api.verify_credentials()
        self.append_timeline(name='@%s' % self.user.screen_name,     
                             update_function=self.api.get_own_timeline,
                             on_error=timeline_not_fetched,
                             on_success=timeline_fetched,)

    def append_mentions_timeline(self):
        timeline_fetched = partial(self.info_message, 
                                    _('Mentions fetched'))
        timeline_not_fetched = partial(self.error_message, 
                                        _('Failed to fetch mentions'))

        self.append_timeline(name=_('mentions'),     
                             update_function=self.api.get_mentions,
                             on_error=timeline_not_fetched,
                             on_success=timeline_fetched,)

    def append_favorites_timeline(self):
        timeline_fetched = partial(self.info_message, 
                                    _('Favorites fetched'))
        timeline_not_fetched = partial(self.error_message, 
                                        _('Failed to fetch favorites'))

        self.append_timeline(name=_('favorites'),     
                             update_function=self.api.get_favorites,
                             on_error=timeline_not_fetched,
                             on_success=timeline_fetched,)

    def append_direct_messages_timeline(self):
        timeline_fetched = partial(self.info_message, 
                                    _('Messages fetched'))
        timeline_not_fetched = partial(self.error_message, 
                                        _('Failed to fetch messages'))

        self.append_timeline(name=_('messages'),
                             update_function=self.api.get_direct_messages,
                             on_error=timeline_not_fetched,
                             on_success=timeline_fetched,)

    def append_thread_timeline(self):
        status = self.timelines.get_active_status()
        if status is None:
            return

        timeline_fetched = partial(self.info_message, 
                                   _('Thread fetched'))
        timeline_not_fetched = partial(self.error_message, 
                                       _('Failed to fetch thread'))

        if is_DM(status):
            self.error_message(_('Doesn\'t look like a public conversation'))
        else:
            participants = get_mentioned_usernames(status)
            author = get_authors_username(status)
            if author not in participants:
                participants.insert(0, author)

            self.append_timeline(name=_('thread: %s' % ', '.join(participants)),
                                 update_function=self.api.get_thread, 
                                 update_args=status,
                                 on_error=timeline_not_fetched,
                                 on_success=timeline_fetched)

    @async
    def update_all_timelines(self):
        for timeline in self.timelines:
            timeline.update()
            self.draw_timelines()
            self.info_message(_('%s updated' % timeline.name))
        self.clear_status()

    # -- Timeline mode --------------------------------------------------------

    def draw_timelines(self):
        self.update_header()
        self.draw_timeline_buffer()

    def update_header(self):
        # update tabs with buffer names and unread count
        timeline_names = self.timelines.get_timeline_names()
        unread_tweets = self.timelines.get_unread_counts()

        template = self.configuration.styles['tab_template']

        name_and_unread = zip(timeline_names, map(str, unread_tweets))

        tabs = [template.format(timeline_name=name, unread=unread) 
                for (name, unread) in name_and_unread]
        self.ui.set_tab_names(tabs)

        # highlight the active
        active_index = self.timelines.active_index
        self.ui.activate_tab(active_index)

        # colorize the visible tabs 
        visible_indexes = self.timelines.get_visible_indexes()
        self.ui.header.set_visible_tabs(visible_indexes)

    def draw_timeline_buffer(self):
        # draw visible timelines
        visible_timelines = self.timelines.get_visible_timelines()
        self.ui.draw_timelines(visible_timelines)
        # focus active timeline
        active_timeline = self.timelines.get_active_timeline()
        active_pos = self.timelines.get_visible_timeline_relative_index()
        # focus active status
        self.ui.focus_timeline(active_pos)
        self.ui.focus_status(active_timeline.active_index)

    def mark_all_as_read(self):
        """Mark all statuses in active timeline as read."""
        active_timeline = self.timelines.get_active_timeline()
        for tweet in active_timeline:
            tweet.read = True
        self.update_header()

    @async
    def update_active_timeline(self):
        """Updates the timeline and renders the active timeline."""
        if self.timelines.has_timelines():
            active_timeline = self.timelines.get_active_timeline()
            try:
                newest = active_timeline[0]
            except IndexError:
                return
            active_timeline.update_with_extra_kwargs(since_id=newest.id)
            if self.is_in_timeline_mode():
                self.draw_timelines()
            self.info_message('%s updated' % active_timeline.name)

    @async
    def update_active_timeline_with_newer_statuses(self):
        """
        Updates the active timeline with newer tweets than the active.
        """
        active_timeline = self.timelines.get_active_timeline()
        active_status = active_timeline.get_active()
        if active_status:
            active_timeline.update_with_extra_kwargs(since_id=active_status.id)

    @async
    def update_active_timeline_with_older_statuses(self):
        """
        Updates the active timeline with older tweets than the active.
        """
        active_timeline = self.timelines.get_active_timeline()
        active_status = active_timeline.get_active()
        if active_status:
            active_timeline.update_with_extra_kwargs(max_id=active_status.id)

    def previous_timeline(self):
        if self.timelines.has_timelines():
            self.timelines.activate_previous()
            self.draw_timelines()

    def next_timeline(self):
        if self.timelines.has_timelines():
            self.timelines.activate_next()
            self.draw_timelines()

    def shift_buffer_left(self):
        if self.timelines.has_timelines():
            self.timelines.shift_active_previous()
            self.draw_timelines()

    def shift_buffer_right(self):
        if self.timelines.has_timelines():
            self.timelines.shift_active_next()
            self.draw_timelines()

    def shift_buffer_beggining(self):
        if self.timelines.has_timelines():
            self.timelines.shift_active_beggining()
            self.draw_timelines()

    def shift_buffer_end(self):
        if self.timelines.has_timelines():
            self.timelines.shift_active_end()
            self.draw_timelines()

    def expand_buffer_left(self):
        if self.timelines.has_timelines():
            self.timelines.expand_visible_previous()
            self.draw_timelines()

    def expand_buffer_right(self):
        if self.timelines.has_timelines():
            self.timelines.expand_visible_next()
            self.draw_timelines()

    def shrink_buffer_left(self):
        if self.timelines.has_timelines():
            self.timelines.shrink_visible_beggining()
            self.draw_timelines()

    def shrink_buffer_right(self):
        if self.timelines.has_timelines():
            self.timelines.shrink_visible_end()
            self.draw_timelines()

    def activate_first_buffer(self):
        if self.timelines.has_timelines():
            self.timelines.activate_first()
            self.draw_timelines()

    def activate_last_buffer(self):
        if self.timelines.has_timelines():
            self.timelines.activate_last()
            self.draw_timelines()

    def delete_buffer(self):
        self.timelines.delete_active_timeline()
        if self.timelines.has_timelines():
            self.draw_timelines()
        else:
            self.info_mode()

    # -- Motion --------------------------------------------------------------- 

    def scroll_up(self):
        self.ui.focus_previous()
        if self.is_in_timeline_mode():
            active_timeline = self.timelines.get_active_timeline()
            # update with newer tweets when scrolling down being at the bottom
            if active_timeline.active_index == 0:
               self.update_active_timeline_with_newer_statuses()
            active_timeline.activate_previous()
            self.draw_timelines()

    def scroll_down(self):
        self.ui.focus_next()
        if self.is_in_timeline_mode():
            active_timeline = self.timelines.get_active_timeline()
            # update with older tweets when scrolling down being at the bottom
            if active_timeline.active_index == len(active_timeline) - 1:
               self.update_active_timeline_with_older_statuses()
            active_timeline.activate_next()
            self.draw_timelines()

    def scroll_top(self):
        self.ui.focus_first()
        if self.is_in_timeline_mode():
            active_timeline = self.timelines.get_active_timeline()
            active_timeline.activate_first()
            self.draw_timelines()

    def scroll_bottom(self):
        self.ui.focus_last()
        if self.is_in_timeline_mode():
            active_timeline = self.timelines.get_active_timeline()
            active_timeline.activate_last()
            self.draw_timelines()

    # -- Footer ---------------------------------------------------------------
        
    def error_message(self, message):
        self.ui.status_error_message(message)
        self.redraw_screen()

    def info_message(self, message):
        self.ui.status_info_message(message)
        self.redraw_screen()

    def clear_body(self):
        """Clear body."""
        self.ui.body.clear()

    def clear_status(self):
        """Clear the status bar."""
        self.ui.clear_status()
        self.redraw_screen()

    # -- UI -------------------------------------------------------------------

    def redraw_screen(self):
        raise NotImplementedError

    # -- Editor event handlers ------------------------------------------------

    def tweet_handler(self, text):
        """Handle the post as a tweet of the given `text`."""
        self.timeline_mode()
        self.ui.remove_editor(self.tweet_handler)
        self.ui.set_focus('body')

        self.info_message(_('Sending tweet'))

        if not is_valid_status_text(text):
            # <Esc> was pressed
            self.info_message(_('Tweet canceled'))
            return

        tweet_sent = partial(self.info_message, _('Tweet sent'))
        tweet_not_sent = partial(self.error_message, _('Tweet not sent'))

        # API call
        self.api.update(text=text, 
                        on_success=tweet_sent,
                        on_error=tweet_not_sent,)

    def direct_message_handler(self, username, text):
        """Handle the post as a DM of the given `text` to `username`."""
        self.timeline_mode()
        self.ui.remove_editor(self.direct_message_handler)
        self.ui.set_focus('body')

        self.info_message(_('Sending DM'))

        if not is_valid_status_text(text):
            # <Esc> was pressed
            self.info_message(_('DM canceled'))
            return

        dm_info = _('Direct Message to @%s sent' % username) 
        dm_sent = partial(self.info_message, dm_info)
        dm_error =_('Failed to send message to @%s' % username) 
        dm_not_sent = partial(self.error_message, dm_error) 

        self.api.direct_message(screen_name=username, 
                                text=text,
                                on_success=dm_sent,
                                on_error=dm_not_sent,)

    def search_handler(self, text):
        """
        Handles creating a timeline tracking the search term given in 
        `text`.
        """
        self.timeline_mode()
        self.ui.remove_editor(self.search_handler)
        self.ui.set_focus('body')

        if text is None:
            self.info_message(_('Search cancelled'))
            return

        text = text.strip()
        if not is_valid_search_text(text):
            self.error_message(_('Invalid search'))
            return
        else:
            self.info_message(_('Creating search timeline for "%s"' % text))

        timeline_created =  partial(self.info_message,
                                    _('Search timeline for "%s" created' % text))
        timeline_not_created =  partial(self.info_message,
                                        _('Error creating search timeline for "%s"' % text))

        self.append_timeline(name=_('Search: %s' % text),
                            update_function=self.api.get_search, 
                            update_args=text,
                            on_error=timeline_not_created,
                            on_success=timeline_created)

    def search_user_handler(self, username):
        """
        Handles creating a timeline tracking the searched user's tweets.
        """
        self.ui.remove_editor(self.search_user_handler)
        self.ui.set_focus('body')

        # TODO make sure that the user EXISTS and THEN fetch its tweets
        username = sanitize_username(username)
        if not is_username(username):
            self.info_message(_('Invalid username'))
            return
        else:
            self.info_message(_('Fetching latest tweets from @%s' % username))

        timeline_created = partial(self.info_message,
                                  _('@%s\'s timeline created' % username))
        timeline_not_created = partial(self.error_message,
                                       _('Unable to create @%s\'s timeline' % username))

        self.append_timeline(name='@%s' % username,
                             update_function=self.api.get_user_timeline, 
                             update_args=username,
                             on_success=timeline_created,
                             on_error=timeline_not_created)

    # -- Twitter -------------------------------------------------------------- 

    def search(self, text=None):
        text = '' if text is None else text
        self.ui.show_text_editor(prompt='Search', 
                                 content=text,
                                 done_signal_handler=self.search_handler)

    def search_user(self):
        self.ui.show_text_editor(prompt=_('Search user (no need to prepend it with "@")'),
                                 content='',
                                 done_signal_handler=self.search_user_handler)

    def search_hashtags(self):
        status = self.timelines.get_active_status()
        if status is None:
            return
        hashtags = ' '.join(get_hashtags(status))
        self.search_handler(text=hashtags)

    def focused_status_author_timeline(self):
        status = self.timelines.get_active_status()
        if status is None:
            return
        author = get_authors_username(status)
        self.append_user_timeline(author)

    def tweet(self, 
              prompt=_('Tweet'), 
              content=''):
        self.ui.show_tweet_editor(prompt=prompt,
                                  content=content,
                                  done_signal_handler=self.tweet_handler)

    def retweet(self):
        status = self.timelines.get_active_status()
        if status is None:
            return

        if is_DM(status):
            self.error_message(_('You can\'t retweet direct messages'))
            return

        retweet_posted = partial(self.info_message, 
                                 _('Retweet posted'))
        retweet_post_failed = partial(self.error_message, 
                                      _('Failed to post retweet'))
        self.api.retweet(on_error=retweet_post_failed,
                         on_success=retweet_posted,
                         status=status,)

    def manual_retweet(self):
        status = self.timelines.get_active_status()

        if status is None:
            return

        rt_text = 'RT ' + status.text
        if is_valid_status_text(' ' + rt_text):
            self.tweet(content=rt_text)
        else:
            self.error_message(_('Tweet too long for manual retweet'))

    def reply(self):
        status = self.timelines.get_active_status()
        if status is None:
            return
        if is_DM(status):
            self.direct_message()
            return

        author = get_authors_username(status)
        mentioned = get_mentioned_for_reply(status)
        try:
            mentioned.remove('@%s' % self.user.screen_name)
        except ValueError:
            pass

        self.ui.show_tweet_editor(prompt=_('Reply to %s' % author),
                                  content=' '.join(mentioned),
                                  done_signal_handler=self.tweet_handler)

    def direct_message(self):
        status = self.timelines.get_active_status()
        if status is None:
            return
        recipient = get_dm_recipients_username(self.user.screen_name, status)
        if recipient:
            self.ui.show_dm_editor(prompt=_('DM to %s' % recipient), 
                                   content='',
                                   recipient=recipient,
                                   done_signal_handler=self.direct_message_handler)
        else:
            self.error_message(_('What do you mean?'))

    def tweet_with_hashtags(self):
        status = self.timelines.get_active_status()
        if status is None:
            return
        hashtags = ' '.join(get_hashtags(status))
        if hashtags:
            # TODO cursor in the begginig
            self.ui.show_tweet_editor(prompt=_('%s' % hashtags),
                                      content=hashtags,
                                      done_signal_handler=self.tweet_handler)

    def delete_tweet(self):
        status = self.timelines.get_active_status()
        if status is None:
            return
        if is_DM(status): 
            self.delete_dm()
            return

        author = get_authors_username(status)
        if author != self.user.screen_name:
            self.error_message(_('You can only delete your own tweets'))
            return

        # TODO: check if DM and delete DM if is

        status_deleted = partial(self.info_message, 
                                 _('Tweet deleted'))
        status_not_deleted = partial(self.error_message, 
                                     _('Failed to delete tweet'))

        self.api.destroy_status(status=status, 
                                on_error=status_not_deleted,
                                on_success=status_deleted)

    def delete_dm(self):
        dm = self.timelines.get_active_status()
        if dm is None:
            return

        if dm.sender_screen_name != self.user.screen_name:
            self.error_message(_('You can only delete messages sent by you'))
            return

        dm_deleted = partial(self.info_message, 
                             _('Message deleted'))
        dm_not_deleted = partial(self.error_message, 
                                 _('Failed to delete message'))

        self.api.destroy_direct_message(status=dm, 
                                        on_error=dm_not_deleted,
                                        on_success=dm_deleted)

    def follow_selected(self):
        status = self.timelines.get_active_status()
        if status is None:
            return
        username = get_authors_username(status)
        if username == self.user.screen_name:
            self.error_message(_('You can\'t follow yourself'))
            return
        follow_done = partial(self.info_message, 
                              _('You are now following @%s' % username))
        follow_error = partial(self.error_message, 
                               _('We can not ensure that you are following @%s' % username))
        self.api.create_friendship(screen_name=username, 
                                   on_error=follow_error,
                                   on_success=follow_done)

    def unfollow_selected(self):
        status = self.timelines.get_active_status()
        if status is None:
            return
        username = get_authors_username(status)
        if username == self.user.screen_name:
            self.error_message(_('That doesn\'t make any sense'))
            return
        unfollow_done = partial(self.info_message, 
                                _('You are no longer following %s' % username))
        unfollow_error = partial(self.error_message, 
                               _('We can not ensure that you are not following %s' % username))
        self.api.destroy_friendship(screen_name=username, 
                                    on_error=unfollow_error,
                                    on_success=unfollow_done)

    def favorite(self):
        status = self.timelines.get_active_status()
        if status is None:
            return
        favorite_error = partial(self.error_message,
                                 _('Failed to mark tweet as favorite'))
        favorite_done = partial(self.info_message,
                                _('Tweet marked as favorite'))
        self.api.create_favorite(on_error=favorite_error,
                                 on_success=favorite_done,
                                 status=status,)

    def unfavorite(self):
        status = self.timelines.get_active_status()
        if status is None:
            return
        unfavorite_error = partial(self.error_message,
                                   _('Failed to remove tweet from favorites'))
        unfavorite_done = partial(self.info_message,
                                  _('Tweet removed from favorites'))
        self.api.destroy_favorite(on_error=unfavorite_error,
                                  on_success=unfavorite_done,
                                  status=status,)

    # -------------------------------------------------------------------------

    def open_urls(self):
        """
        Open the URLs contained on the focused tweets in a browser.
        """
        status = self.timelines.get_active_status()
        if status is None:
            return
        urls = get_urls(status.text)

        if not urls:
            self.info_message(_('No URLs found on this tweet'))
            return

        args = ' '.join(urls)

        command = self.configuration.browser
        if not command:
            self.error_message(_('You have to set the BROWSER environment variable to open URLs'))
            return

        try:
            spawn_process(command, args)
        except:
            self.error_message(_('Unable to launch the browser'))