class SessionTest(unittest.TestCase): def setUp(self): self.session = Session(mock_api) def test_clean_timeline_list_string(self): self.assertEqual(clean_timeline_list_string(''), []) self.assertEqual(clean_timeline_list_string(' '), []) self.assertEqual(clean_timeline_list_string('home'), ['home']) self.assertEqual(clean_timeline_list_string(' home, '), ['home']) self.assertEqual(clean_timeline_list_string('home, mentions'), ['home', 'mentions']) self.assertEqual(clean_timeline_list_string(' home,mentions '), ['home', 'mentions']) self.assertEqual( clean_timeline_list_string( 'mentions, favorites, messages, own_tweets'), ['mentions', 'favorites', 'messages', 'own_tweets']) def test_custom_session(self): """ Test that, when defining a custom session, the timelines are created correctly. """ timeline_list = TimelineList() visible_string = 'home, mentions, search:turses' self.session.append_visible_timelines(visible_string, timeline_list) # check that the visible timelines are appended correctly self.assertTrue(len(timeline_list), 3) self.assertTrue(is_home_timeline(timeline_list[0])) self.assertTrue(is_mentions_timeline(timeline_list[1])) self.assertTrue(is_search_timeline(timeline_list[2])) self.assertEqual( timeline_list.visible_timelines, [timeline_list[0], timeline_list[1], timeline_list[2]]) # now let's append the buffers in the background buffers_string = 'messages' self.session.append_background_timelines(buffers_string, timeline_list) self.assertTrue(len(timeline_list), 4) self.assertTrue(is_messages_timeline(timeline_list[3]))
class SessionTest(unittest.TestCase): def setUp(self): self.session = Session(mock_api) def test_clean_timeline_list_string(self): self.assertEqual(clean_timeline_list_string(''), []) self.assertEqual(clean_timeline_list_string(' '), []) self.assertEqual(clean_timeline_list_string('home'), ['home']) self.assertEqual(clean_timeline_list_string(' home, '), ['home']) self.assertEqual(clean_timeline_list_string('home, mentions'), ['home', 'mentions']) self.assertEqual(clean_timeline_list_string(' home,mentions '), ['home', 'mentions']) self.assertEqual(clean_timeline_list_string( 'mentions, favorites, messages, own_tweets'), ['mentions', 'favorites', 'messages', 'own_tweets']) def test_custom_session(self): """ Test that, when defining a custom session, the timelines are created correctly. """ timeline_list = TimelineList() visible_string = 'home, mentions, search:turses' self.session.append_visible_timelines(visible_string, timeline_list) # check that the visible timelines are appended correctly self.assertTrue(len(timeline_list), 3) self.assertTrue(is_home_timeline(timeline_list[0])) self.assertTrue(is_mentions_timeline(timeline_list[1])) self.assertTrue(is_search_timeline(timeline_list[2])) self.assertEqual(timeline_list.visible_timelines, [timeline_list[0], timeline_list[1], timeline_list[2]]) # now let's append the buffers in the background buffers_string = 'messages' self.session.append_background_timelines(buffers_string, timeline_list) self.assertTrue(len(timeline_list), 4) self.assertTrue(is_messages_timeline(timeline_list[3]))
def __init__(self, ui, api, timelines): # View self.ui = ui self.api = api self.timelines = timelines # Load session self.session = Session(self.api) self.session.populate(self.timelines) self.editor = None # Default Mode self.mode = self.INFO_MODE # Subscribe to model updates self.timelines.subscribe(self)
class Controller(Observer): """ The :class:`Controller`. """ # Modes INFO_MODE = 0 TIMELINE_MODE = 1 HELP_MODE = 2 EDITOR_MODE = 3 USER_INFO_MODE = 4 # -- Initialization ------------------------------------------------------- def __init__(self, ui, api, timelines): # View self.ui = ui self.api = api self.timelines = timelines # Load session self.session = Session(self.api) self.session.populate(self.timelines) self.editor = None # Default Mode self.mode = self.INFO_MODE # Subscribe to model updates self.timelines.subscribe(self) signal.signal(signal.SIGCONT, self.handle_sigcont) def start(self): self.main_loop() def handle_sigcont(self, signum, stack_frame): self.loop.screen.stop() self.loop.screen.start() self.redraw_screen() def authenticate_api(self): self.info_message(_('Authenticating API')) self.api.init_api(on_error=self.api_init_error, on_success=self.init_timelines,) @async def init_timelines(self): # API has to be authenticated while (not self.api.is_authenticated): pass # fetch the authenticated user self.user = self.api.verify_credentials() # initialize the timelines self.info_message(_('Fetching timelines')) for timeline in self.timelines: timeline.update() timeline.activate_first() self.timeline_mode() self.clear_status() # Main loop has to be running while not getattr(self, 'loop'): pass # update alarm seconds = configuration.twitter['update_frequency'] self.loop.set_alarm_in(seconds, self.update_alarm) def main_loop(self): """ Launch the main loop of the program. """ if not hasattr(self, 'loop'): # Creating the main loop for the first time self.input_handler = InputHandler(self) self.loop = urwid.MainLoop( self.ui, configuration.palette, handle_mouse=True, unhandled_input=self.input_handler.handle, input_filter=self.input_handler.filter_input) # Authenticate API just before starting main loop self.authenticate_api() try: self.loop.run() except TweepError, message: logging.exception(message) self.error_message(_('API error: %s' % message)) # recover from API errors self.main_loop() except KeyboardInterrupt: # treat Ctrl-C as Escape self.input_handler.handle('esc') self.main_loop()
class Controller(Observer): """ The :class:`Controller`. """ # Modes INFO_MODE = 0 TIMELINE_MODE = 1 HELP_MODE = 2 EDITOR_MODE = 3 USER_INFO_MODE = 4 # -- Initialization ------------------------------------------------------- def __init__(self, ui, api, timelines): # View self.ui = ui self.api = api self.timelines = timelines # Load session self.session = Session(self.api) self.session.populate(self.timelines) self.editor = None # Default Mode self.mode = self.INFO_MODE # Subscribe to model updates self.timelines.subscribe(self) signal.signal(signal.SIGCONT, self.handle_sigcont) def start(self): self.main_loop() def handle_sigcont(self, signum, stack_frame): self.loop.screen.stop() self.loop.screen.start() self.redraw_screen() def authenticate_api(self): self.info_message(_('Authenticating API')) self.api.init_api(on_error=self.api_init_error, on_success=self.init_timelines,) @async def init_timelines(self): # API has to be authenticated while (not self.api.is_authenticated): pass # fetch the authenticated user self.user = self.api.verify_credentials() # initialize the timelines self.info_message(_('Fetching timelines')) for timeline in self.timelines: timeline.update() timeline.activate_first() self.timeline_mode() self.clear_status() # Main loop has to be running while not getattr(self, 'loop'): pass # update alarm seconds = configuration.twitter['update_frequency'] self.loop.set_alarm_in(seconds, self.update_alarm) def main_loop(self): """ Launch the main loop of the program. """ if not hasattr(self, 'loop'): # Creating the main loop for the first time self.input_handler = InputHandler(self) self.loop = urwid.MainLoop( self.ui, configuration.palette, handle_mouse=True, unhandled_input=self.input_handler.handle, input_filter=self.input_handler.filter_input) # Authenticate API just before starting main loop self.authenticate_api() try: self.loop.run() except TweepError as message: logging.exception(message) self.error_message(_('API error: %s' % message)) # recover from API errors self.main_loop() except KeyboardInterrupt: # treat Ctrl-C as Escape self.input_handler.handle('esc') self.main_loop() def exit(self): """Exit the program.""" raise urwid.ExitMainLoop() # -- Observer ------------------------------------------------------------- def update(self): """ From :class:`~turses.meta.Observer`, gets called when the observed subjects change. """ if self.is_in_info_mode(): self.timeline_mode() self.draw_timelines() # -- Callbacks ------------------------------------------------------------ def api_init_error(self): # TODO retry self.error_message(_('Couldn\'t initialize API')) def update_alarm(self, *args, **kwargs): self.update_all_timelines() seconds = configuration.twitter['update_frequency'] 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_user_info_mode(): self.ui.hide_user_info() if self.is_in_timeline_mode(): return if self.is_in_help_mode(): self.clear_status() if self.timelines.has_timelines(): self.mode = self.TIMELINE_MODE self.draw_timelines() else: self.mode = self.INFO_MODE self.ui.show_info() 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.redraw_screen() def is_in_help_mode(self): return self.mode == self.HELP_MODE def editor_mode(self, editor): """Activate editor mode.""" self.editor = editor self.mode = self.EDITOR_MODE def is_in_editor_mode(self): return self.mode == self.EDITOR_MODE def user_info_mode(self, user): """Activate user info mode.""" self._user_info = user self.mode = self.USER_INFO_MODE def is_in_user_info_mode(self): return self.mode == self.USER_INFO_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) 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): success_message = _('@%s\'s tweets fetched' % username) timeline_fetched = partial(self.info_message, success_message) error_message = _('Failed to fetch @%s\'s tweets' % username) timeline_not_fetched = partial(self.error_message, error_message) 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,) @has_active_status def append_thread_timeline(self): status = self.timelines.active_status timeline_fetched = partial(self.info_message, _('Thread fetched')) timeline_not_fetched = partial(self.error_message, _('Failed to fetch thread')) if is_DM(status): participants = [status.sender_screen_name, status.recipient_screen_name] name = _('DM thread: %s' % ', '.join(participants)) update_function = self.api.get_message_thread else: participants = status.mentioned_usernames author = status.authors_username if author not in participants: participants.insert(0, author) name = _('thread: %s' % ', '.join(participants)) update_function = self.api.get_thread self.append_timeline(name=name, update_function=update_function, update_args=status, on_error=timeline_not_fetched, on_success=timeline_fetched) @async def append_search_timeline(self, query): text = query.strip() if not is_valid_search_text(text): self.error_message(_('Invalid search')) return else: self.info_message(_('Creating search timeline for "%s"' % text)) success_message = _('Search timeline for "%s" created' % text) timeline_created = partial(self.info_message, success_message) error_message = _('Error creating search timeline for "%s"' % text) timeline_not_created = partial(self.info_message, error_message) self.append_timeline(name=_('Search: %s' % text), update_function=self.api.search, update_args=text, on_error=timeline_not_created, on_success=timeline_created) @async def append_retweets_of_me_timeline(self): success_message = _('Your retweeted tweet timeline created') timeline_created = partial(self.info_message, success_message) error_message = _('Error creating timeline for your retweeted tweets') timeline_not_created = partial(self.info_message, error_message) self.append_timeline(name=_('Retweets of %s' % self.user.screen_name), update_function=self.api.get_retweets_of_me, on_error=timeline_not_created, on_success=timeline_created) @async def update_all_timelines(self): for timeline in self.timelines: timeline.update() self.draw_timelines() self.info_message(_('%s updated' % timeline.name)) self.redraw_screen() self.clear_status() # -- Timeline mode -------------------------------------------------------- def draw_timelines(self): if not self.is_in_timeline_mode(): return if self.timelines.has_timelines(): self.update_header() # draw visible timelines visible_timelines = self.timelines.visible_timelines self.ui.draw_timelines(visible_timelines) # focus active timeline active_timeline = self.timelines.active active_pos = self.timelines.active_index_relative_to_visible # focus active status (if any) if active_timeline.active_index >= 0: self.ui.focus_timeline(active_pos) self.ui.focus_status(active_timeline.active_index) else: self.ui.clear_header() def update_header(self): template = configuration.styles['tab_template'] name_and_unread = [(tl.name, tl.unread_count) for tl in self.timelines] 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.visible self.ui.highlight_tabs(visible_indexes) def mark_all_as_read(self): """Mark all statuses in active timeline as read.""" active_timeline = self.timelines.active for tweet in active_timeline: tweet.read = True self.update_header() @async def update_active_timeline(self): """Update the active timeline and draw the timeline buffers.""" if self.timelines.has_timelines(): active_timeline = self.timelines.active try: newest = active_timeline[0] except IndexError: return active_timeline.update(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.active active_status = active_timeline.active if active_status: active_timeline.update(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.active active_status = active_timeline.active if active_status: active_timeline.update(max_id=active_status.id) # Center focus in order to make the fetched tweets visible self.draw_timelines() self.ui.center_focus() self.redraw_screen() @has_timelines def previous_timeline(self): self.timelines.activate_previous() @has_timelines def next_timeline(self): self.timelines.activate_next() @has_timelines def shift_buffer_left(self): self.timelines.shift_active_previous() @has_timelines def shift_buffer_right(self): self.timelines.shift_active_next() @has_timelines def shift_buffer_beggining(self): self.timelines.shift_active_beggining() @has_timelines def shift_buffer_end(self): self.timelines.shift_active_end() @has_timelines def expand_buffer_left(self): self.timelines.expand_visible_previous() @has_timelines def expand_buffer_right(self): self.timelines.expand_visible_next() @has_timelines def shrink_buffer_left(self): self.timelines.shrink_visible_beggining() @has_timelines def shrink_buffer_right(self): self.timelines.shrink_visible_end() @has_timelines def activate_first_buffer(self): self.timelines.activate_first() @has_timelines def activate_last_buffer(self): self.timelines.activate_last() def delete_buffer(self): self.timelines.delete_active_timeline() if not self.timelines.has_timelines(): self.info_mode() # -- Motion --------------------------------------------------------------- def scroll_up(self): self.ui.focus_previous() if self.is_in_timeline_mode(): active_timeline = self.timelines.active # 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.active # 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.active 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.active 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_status(self): """Clear the status bar.""" self.ui.clear_status() self.redraw_screen() # -- UI ------------------------------------------------------------------- def redraw_screen(self): if hasattr(self, "loop"): try: self.loop.draw_screen() except AssertionError as message: logging.critical(message) # -- Editor --------------------------------------------------------------- def forward_to_editor(self, key): if self.editor: # FIXME: `keypress` function needs a `size` parameter size = 20, self.editor.keypress(size, key) @text_from_editor def tweet_handler(self, text): """Handle the post as a tweet of the given `text`.""" self.info_message(_('Sending tweet')) if not is_valid_status_text(text): # tweet was explicitly cancelled or empty text 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,) @text_from_editor @has_active_status def reply_handler(self, text): """Handle the post as a tweet of the given `text` replying to the current status.""" status = self.timelines.active_status self.info_message(_('Sending reply')) if not is_valid_status_text(text): # reply was explicitly cancelled or empty text self.info_message(_('Reply canceled')) return reply_sent = partial(self.info_message, _('Reply sent')) reply_not_sent = partial(self.error_message, _('Reply not sent')) # API call self.api.reply(status=status, text=text, on_success=reply_sent, on_error=reply_not_sent,) @text_from_editor def direct_message_handler(self, username, text): """Handle the post as a DM of the given `text` to `username`.""" 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,) @text_from_editor def follow_user_handler(self, username): """ Handles following the user given in `username`. """ if username is None: self.info_message(_('Search cancelled')) return username = sanitize_username(username) if username == self.user.screen_name: self.error_message(_('You can\'t follow yourself')) return # TODO make sure that the user EXISTS and THEN follow if not is_username(username): self.info_message(_('Invalid username')) return else: self.info_message(_('Following @%s' % username)) success_message = _('You are now following @%s' % username) follow_done = partial(self.info_message, success_message) error_template = _('We can not ensure that you are following @%s') error_message = error_template % username follow_error = partial(self.error_message, error_message) self.api.create_friendship(screen_name=username, on_error=follow_error, on_success=follow_done) @text_from_editor def unfollow_user_handler(self, username): """ Handles unfollowing the user given in `username`. """ if username is None: self.info_message(_('Search cancelled')) return username = sanitize_username(username) if username == self.user.screen_name: self.error_message(_('That doesn\'t make any sense')) return # TODO make sure that the user EXISTS and THEN follow if not is_username(username): self.info_message(_('Invalid username')) return else: self.info_message(_('Unfollowing @%s' % username)) success_message = _('You are no longer following %s' % username) unfollow_done = partial(self.info_message, success_message) error_template = _('We can not ensure that you are not following %s') error_message = error_template % username unfollow_error = partial(self.error_message, error_message) self.api.destroy_friendship(screen_name=username, on_error=unfollow_error, on_success=unfollow_done) @text_from_editor def search_handler(self, text): """ Handles creating a timeline tracking the search term given in `text`. """ if text is None: self.info_message(_('Search cancelled')) return self.append_search_timeline(text) @text_from_editor def search_user_handler(self, username): """ Handles creating a timeline tracking the searched user's tweets. """ if username is None: self.info_message(_('Search cancelled')) return # 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)) success_message = _('@%s\'s timeline created' % username) timeline_created = partial(self.info_message, success_message) error_message = _('Unable to create @%s\'s timeline' % username) timeline_not_created = partial(self.error_message, error_message) 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 handler = self.search_handler editor = self.ui.show_text_editor(prompt='Search', content=text, done_signal_handler=handler) self.editor_mode(editor) def search_user(self): prompt = _('Search user (no need to prepend it with "@"') handler = self.search_user_handler editor = self.ui.show_text_editor(prompt=prompt, content='', done_signal_handler=handler) self.editor_mode(editor) @has_active_status def search_hashtags(self): status = self.timelines.active_status hashtags = ' '.join(status.hashtags) self.search_handler(text=hashtags) @has_active_status def focused_status_author_timeline(self): status = self.timelines.active_status author = status.authors_username self.append_user_timeline(author) def tweet(self, prompt=_('Tweet'), content='', cursor_position=None): handler = self.tweet_handler editor = self.ui.show_tweet_editor(prompt=prompt, content=content, done_signal_handler=handler, cursor_position=cursor_position) self.editor_mode(editor) @has_active_status def retweet(self): status = self.timelines.active_status if is_DM(status): self.error_message(_('You can\'t retweet direct messages')) return self._retweet(status) def _retweet(self, status): 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,) @has_active_status def manual_retweet(self): status = self.timelines.active_status rt_text = ''.join([' RT @%s: ' % status.authors_username, status.text]) if is_valid_status_text(rt_text): self.tweet(content=rt_text, cursor_position=0) else: self.error_message(_('Tweet too long for manual retweet')) @has_active_status def retweet_and_favorite(self): status = self.timelines.active_status if is_DM(status): self.error_message( _('You can\'t retweet or favorite direct messages')) return self._retweet(status) self._favorite(status) @has_active_status def reply(self): status = self.timelines.active_status if is_DM(status): self.direct_message() return author = status.authors_username mentioned = status.mentioned_for_reply try: mentioned.remove('@%s' % self.user.screen_name) except ValueError: pass handler = self.reply_handler editor = self.ui.show_tweet_editor(prompt=_('Reply to %s' % author), content=' '.join(mentioned), done_signal_handler=handler) self.editor_mode(editor) @has_active_status def direct_message(self): status = self.timelines.active_status recipient = status.dm_recipients_username(self.user.screen_name) if recipient: handler = self.direct_message_handler editor = self.ui.show_dm_editor(prompt=_('DM to %s' % recipient), content='', recipient=recipient, done_signal_handler=handler) self.editor_mode(editor) else: self.error_message(_('What do you mean?')) @has_active_status def tweet_with_hashtags(self): status = self.timelines.active_status hashtags = ' '.join(status.hashtags) if hashtags: handler = self.tweet_handler content = ''.join([' ', hashtags]) editor = self.ui.show_tweet_editor(prompt=_('%s' % hashtags), content=content, done_signal_handler=handler, cursor_position=0) self.editor_mode(editor) @has_active_status def delete_tweet(self): status = self.timelines.active_status if is_DM(status): self.delete_dm() return author = status.authors_username if (author != self.user.screen_name and status.user != self.user.screen_name): self.error_message(_('You can only delete your own tweets')) return 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.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) @has_active_status def follow_selected(self): status = self.timelines.active_status username = status.authors_username if username == self.user.screen_name: self.error_message(_('You can\'t follow yourself')) return success_message = _('You are now following @%s' % username) follow_done = partial(self.info_message, success_message) error_template = _('We can not ensure that you are following @%s') error_message = error_template % username follow_error = partial(self.error_message, error_message) self.api.create_friendship(screen_name=username, on_error=follow_error, on_success=follow_done) def follow_user(self, prompt=_('Follow user (no need to prepend it with "@"'), content='', cursor_position=None): handler = self.follow_user_handler editor = self.ui.show_text_editor(prompt=prompt, content=content, done_signal_handler=handler, cursor_position=cursor_position) self.editor_mode(editor) def unfollow_user(self, prompt=_('Unfollow user (no need to prepend it with' ' "@"'), content='', cursor_position=None): handler = self.unfollow_user_handler editor = self.ui.show_text_editor(prompt=prompt, content=content, done_signal_handler=handler, cursor_position=cursor_position) self.editor_mode(editor) @has_active_status def unfollow_selected(self): status = self.timelines.active_status username = status.authors_username if username == self.user.screen_name: self.error_message(_('That doesn\'t make any sense')) return success_message = _('You are no longer following %s' % username) unfollow_done = partial(self.info_message, success_message) error_template = _('We can not ensure that you are not following %s') error_message = error_template % username unfollow_error = partial(self.error_message, error_message) self.api.destroy_friendship(screen_name=username, on_error=unfollow_error, on_success=unfollow_done) @has_active_status def favorite(self): status = self.timelines.active_status self._favorite(status) def _favorite(self, status): 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,) @has_active_status def unfavorite(self): status = self.timelines.active_status 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,) @has_active_status def user_info(self): status = self.timelines.active_status username = status.authors_username user = self.api.get_user(username) last_statuses = self.api.get_user_timeline(username) self.ui.show_user_info(user, last_statuses) self.user_info_mode(user) # - Configuration --------------------------------------------------------- def reload_configuration(self): configuration.reload() self.redraw_screen() self.info_message(_('Configuration reloaded')) # - Browser --------------------------------------------------------------- @has_active_status def open_urls(self): """ Open the URLs contained on the focused tweets in a browser. """ status = self.timelines.active_status urls = get_urls(status.text) if not urls: self.info_message(_('No URLs found on this tweet')) return self.open_urls_in_browser(urls) @has_active_status def open_status_url(self): """ Open the focused tweet in a browser. """ status = self.timelines.active_status if is_DM(status): message = _('You only can open regular statuses in a browser') self.info_message(message) return self.open_urls_in_browser([status.url]) def open_urls_in_browser(self, urls): """ Open `urls` in $BROWSER if the environment variable is set. """ # The webbrowser module respects the BROWSER environment variable, # so if that's set, it'll use it, otherwise it will try to find # something sensible savout = os.dup(1) os.close(1) os.open(os.devnull, os.O_RDWR) try: # Firefox, w3m, etc can't handle multiple URLs at command line, so # split the URLs up for them for url in urls: webbrowser.open(url) except Exception as message: logging.exception(message) self.error_message(_('Unable to launch the browser')) finally: os.dup2(savout, 1) os.close(savout)
class Controller(Observer): """ The :class:`Controller`. """ # Modes INFO_MODE = 0 TIMELINE_MODE = 1 HELP_MODE = 2 EDITOR_MODE = 3 USER_INFO_MODE = 4 # -- Initialization ------------------------------------------------------- def __init__(self, ui, api, timelines): # View self.ui = ui self.api = api self.timelines = timelines # Load session self.session = Session(self.api) self.session.populate(self.timelines) self.editor = None # Default Mode self.mode = self.INFO_MODE # Subscribe to model updates self.timelines.subscribe(self) signal.signal(signal.SIGCONT, self.handle_sigcont) def start(self): self.main_loop() def handle_sigcont(self, signum, stack_frame): self.loop.screen.stop() self.loop.screen.start() self.redraw_screen() def authenticate_api(self): self.info_message(_('Authenticating API')) self.api.init_api( on_error=self.api_init_error, on_success=self.init_timelines, ) @async_thread def init_timelines(self): # API has to be authenticated while (not self.api.is_authenticated): pass # fetch the authenticated user self.user = self.api.verify_credentials() # initialize the timelines self.info_message(_('Fetching timelines')) for timeline in self.timelines: timeline.update() timeline.activate_first() self.timeline_mode() self.clear_status() # Main loop has to be running while not getattr(self, 'loop'): pass # update alarm seconds = configuration.twitter['update_frequency'] self.loop.set_alarm_in(seconds, self.update_alarm) def main_loop(self): """ Launch the main loop of the program. """ if not hasattr(self, 'loop'): # Creating the main loop for the first time self.input_handler = InputHandler(self) self.loop = urwid.MainLoop( self.ui, configuration.palette, handle_mouse=True, unhandled_input=self.input_handler.handle, input_filter=self.input_handler.filter_input) # Authenticate API just before starting main loop self.authenticate_api() try: self.loop.run() except TweepError as message: logging.exception(message) self.error_message(_('API error: %s' % message)) # recover from API errors self.main_loop() except KeyboardInterrupt: # treat Ctrl-C as Escape self.input_handler.handle('esc') self.main_loop() def exit(self): """Exit the program.""" raise urwid.ExitMainLoop() # -- Observer ------------------------------------------------------------- def update(self): """ From :class:`~turses.meta.Observer`, gets called when the observed subjects change. """ if self.is_in_info_mode(): self.timeline_mode() self.draw_timelines() # -- Callbacks ------------------------------------------------------------ def api_init_error(self): # TODO retry self.error_message(_('Couldn\'t initialize API')) def update_alarm(self, *args, **kwargs): self.update_all_timelines() seconds = configuration.twitter['update_frequency'] 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_user_info_mode(): self.ui.hide_user_info() if self.is_in_timeline_mode(): return if self.is_in_help_mode(): self.clear_status() if self.timelines.has_timelines(): self.mode = self.TIMELINE_MODE self.draw_timelines() else: self.mode = self.INFO_MODE self.ui.show_info() 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.redraw_screen() def is_in_help_mode(self): return self.mode == self.HELP_MODE def editor_mode(self, editor): """Activate editor mode.""" self.editor = editor self.mode = self.EDITOR_MODE def is_in_editor_mode(self): return self.mode == self.EDITOR_MODE def user_info_mode(self, user): """Activate user info mode.""" self._user_info = user self.mode = self.USER_INFO_MODE def is_in_user_info_mode(self): return self.mode == self.USER_INFO_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) 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): success_message = _('@%s\'s tweets fetched' % username) timeline_fetched = partial(self.info_message, success_message) error_message = _('Failed to fetch @%s\'s tweets' % username) timeline_not_fetched = partial(self.error_message, error_message) 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, ) @has_active_status def append_thread_timeline(self): status = self.timelines.active_status timeline_fetched = partial(self.info_message, _('Thread fetched')) timeline_not_fetched = partial(self.error_message, _('Failed to fetch thread')) if is_DM(status): participants = [ status.sender_screen_name, status.recipient_screen_name ] name = _('DM thread: %s' % ', '.join(participants)) update_function = self.api.get_message_thread else: participants = status.mentioned_usernames author = status.authors_username if author not in participants: participants.insert(0, author) name = _('thread: %s' % ', '.join(participants)) update_function = self.api.get_thread self.append_timeline(name=name, update_function=update_function, update_args=status, on_error=timeline_not_fetched, on_success=timeline_fetched) @async_thread def append_search_timeline(self, query): text = query.strip() if not is_valid_search_text(text): self.error_message(_('Invalid search')) return else: self.info_message(_('Creating search timeline for "%s"' % text)) success_message = _('Search timeline for "%s" created' % text) timeline_created = partial(self.info_message, success_message) error_message = _('Error creating search timeline for "%s"' % text) timeline_not_created = partial(self.info_message, error_message) self.append_timeline(name=_('Search: %s' % text), update_function=self.api.search, update_args=text, on_error=timeline_not_created, on_success=timeline_created) @async_thread def append_retweets_of_me_timeline(self): success_message = _('Your retweeted tweet timeline created') timeline_created = partial(self.info_message, success_message) error_message = _('Error creating timeline for your retweeted tweets') timeline_not_created = partial(self.info_message, error_message) self.append_timeline(name=_('Retweets of %s' % self.user.screen_name), update_function=self.api.get_retweets_of_me, on_error=timeline_not_created, on_success=timeline_created) @async_thread def update_all_timelines(self): for timeline in self.timelines: timeline.update() self.draw_timelines() self.info_message(_('%s updated' % timeline.name)) self.redraw_screen() self.clear_status() # -- Timeline mode -------------------------------------------------------- def draw_timelines(self): if not self.is_in_timeline_mode(): return if self.timelines.has_timelines(): self.update_header() # draw visible timelines visible_timelines = self.timelines.visible_timelines self.ui.draw_timelines(visible_timelines) # focus active timeline active_timeline = self.timelines.active active_pos = self.timelines.active_index_relative_to_visible # focus active status (if any) if active_timeline.active_index >= 0: self.ui.focus_timeline(active_pos) self.ui.focus_status(active_timeline.active_index) else: self.ui.clear_header() def update_header(self): template = configuration.styles['tab_template'] name_and_unread = [(tl.name, tl.unread_count) for tl in self.timelines] 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.visible self.ui.highlight_tabs(visible_indexes) def mark_all_as_read(self): """Mark all statuses in active timeline as read.""" active_timeline = self.timelines.active for tweet in active_timeline: tweet.read = True self.update_header() @async_thread def update_active_timeline(self): """Update the active timeline and draw the timeline buffers.""" if self.timelines.has_timelines(): active_timeline = self.timelines.active try: newest = active_timeline[0] except IndexError: return active_timeline.update(since_id=newest.id) if self.is_in_timeline_mode(): self.draw_timelines() self.info_message('%s updated' % active_timeline.name) @async_thread def update_active_timeline_with_newer_statuses(self): """ Updates the active timeline with newer tweets than the active. """ active_timeline = self.timelines.active active_status = active_timeline.active if active_status: active_timeline.update(since_id=active_status.id) @async_thread def update_active_timeline_with_older_statuses(self): """ Updates the active timeline with older tweets than the active. """ active_timeline = self.timelines.active active_status = active_timeline.active if active_status: active_timeline.update(max_id=active_status.id) # Center focus in order to make the fetched tweets visible self.draw_timelines() self.ui.center_focus() self.redraw_screen() @has_timelines def previous_timeline(self): self.timelines.activate_previous() @has_timelines def next_timeline(self): self.timelines.activate_next() @has_timelines def shift_buffer_left(self): self.timelines.shift_active_previous() @has_timelines def shift_buffer_right(self): self.timelines.shift_active_next() @has_timelines def shift_buffer_beggining(self): self.timelines.shift_active_beggining() @has_timelines def shift_buffer_end(self): self.timelines.shift_active_end() @has_timelines def expand_buffer_left(self): self.timelines.expand_visible_previous() @has_timelines def expand_buffer_right(self): self.timelines.expand_visible_next() @has_timelines def shrink_buffer_left(self): self.timelines.shrink_visible_beggining() @has_timelines def shrink_buffer_right(self): self.timelines.shrink_visible_end() @has_timelines def activate_first_buffer(self): self.timelines.activate_first() @has_timelines def activate_last_buffer(self): self.timelines.activate_last() def delete_buffer(self): self.timelines.delete_active_timeline() if not self.timelines.has_timelines(): self.info_mode() # -- Motion --------------------------------------------------------------- def scroll_up(self): self.ui.focus_previous() if self.is_in_timeline_mode(): active_timeline = self.timelines.active # 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.active # 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.active 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.active 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_status(self): """Clear the status bar.""" self.ui.clear_status() self.redraw_screen() # -- UI ------------------------------------------------------------------- def redraw_screen(self): if hasattr(self, "loop"): try: self.loop.draw_screen() except AssertionError as message: logging.critical(message) # -- Editor --------------------------------------------------------------- def forward_to_editor(self, key): if self.editor: # FIXME: `keypress` function needs a `size` parameter size = 20, self.editor.keypress(size, key) @text_from_editor def tweet_handler(self, text): """Handle the post as a tweet of the given `text`.""" self.info_message(_('Sending tweet')) if not is_valid_status_text(text): # tweet was explicitly cancelled or empty text 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, ) @text_from_editor @has_active_status def reply_handler(self, text): """Handle the post as a tweet of the given `text` replying to the current status.""" status = self.timelines.active_status self.info_message(_('Sending reply')) if not is_valid_status_text(text): # reply was explicitly cancelled or empty text self.info_message(_('Reply canceled')) return reply_sent = partial(self.info_message, _('Reply sent')) reply_not_sent = partial(self.error_message, _('Reply not sent')) # API call self.api.reply( status=status, text=text, on_success=reply_sent, on_error=reply_not_sent, ) @text_from_editor def direct_message_handler(self, username, text): """Handle the post as a DM of the given `text` to `username`.""" 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, ) @text_from_editor def follow_user_handler(self, username): """ Handles following the user given in `username`. """ if username is None: self.info_message(_('Search cancelled')) return username = sanitize_username(username) if username == self.user.screen_name: self.error_message(_('You can\'t follow yourself')) return # TODO make sure that the user EXISTS and THEN follow if not is_username(username): self.info_message(_('Invalid username')) return else: self.info_message(_('Following @%s' % username)) success_message = _('You are now following @%s' % username) follow_done = partial(self.info_message, success_message) error_template = _('We can not ensure that you are following @%s') error_message = error_template % username follow_error = partial(self.error_message, error_message) self.api.create_friendship(screen_name=username, on_error=follow_error, on_success=follow_done) @text_from_editor def unfollow_user_handler(self, username): """ Handles unfollowing the user given in `username`. """ if username is None: self.info_message(_('Search cancelled')) return username = sanitize_username(username) if username == self.user.screen_name: self.error_message(_('That doesn\'t make any sense')) return # TODO make sure that the user EXISTS and THEN follow if not is_username(username): self.info_message(_('Invalid username')) return else: self.info_message(_('Unfollowing @%s' % username)) success_message = _('You are no longer following %s' % username) unfollow_done = partial(self.info_message, success_message) error_template = _('We can not ensure that you are not following %s') error_message = error_template % username unfollow_error = partial(self.error_message, error_message) self.api.destroy_friendship(screen_name=username, on_error=unfollow_error, on_success=unfollow_done) @text_from_editor def search_handler(self, text): """ Handles creating a timeline tracking the search term given in `text`. """ if text is None: self.info_message(_('Search cancelled')) return self.append_search_timeline(text) @text_from_editor def search_user_handler(self, username): """ Handles creating a timeline tracking the searched user's tweets. """ if username is None: self.info_message(_('Search cancelled')) return # 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)) success_message = _('@%s\'s timeline created' % username) timeline_created = partial(self.info_message, success_message) error_message = _('Unable to create @%s\'s timeline' % username) timeline_not_created = partial(self.error_message, error_message) 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 handler = self.search_handler editor = self.ui.show_text_editor(prompt='Search', content=text, done_signal_handler=handler) self.editor_mode(editor) def search_user(self): prompt = _('Search user (no need to prepend it with "@"') handler = self.search_user_handler editor = self.ui.show_text_editor(prompt=prompt, content='', done_signal_handler=handler) self.editor_mode(editor) @has_active_status def search_hashtags(self): status = self.timelines.active_status hashtags = ' '.join(status.hashtags) self.search_handler(text=hashtags) @has_active_status def focused_status_author_timeline(self): status = self.timelines.active_status author = status.authors_username self.append_user_timeline(author) def tweet(self, prompt=_('Tweet'), content='', cursor_position=None): handler = self.tweet_handler editor = self.ui.show_tweet_editor(prompt=prompt, content=content, done_signal_handler=handler, cursor_position=cursor_position) self.editor_mode(editor) @has_active_status def retweet(self): status = self.timelines.active_status if is_DM(status): self.error_message(_('You can\'t retweet direct messages')) return self._retweet(status) def _retweet(self, status): 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, ) @has_active_status def manual_retweet(self): status = self.timelines.active_status rt_text = ''.join([' RT @%s: ' % status.authors_username, status.text]) if is_valid_status_text(rt_text): self.tweet(content=rt_text, cursor_position=0) else: self.error_message(_('Tweet too long for manual retweet')) @has_active_status def retweet_and_favorite(self): status = self.timelines.active_status if is_DM(status): self.error_message( _('You can\'t retweet or favorite direct messages')) return self._retweet(status) self._favorite(status) @has_active_status def reply(self): status = self.timelines.active_status if is_DM(status): self.direct_message() return author = status.authors_username mentioned = status.mentioned_for_reply try: mentioned.remove('@%s' % self.user.screen_name) except ValueError: pass handler = self.reply_handler editor = self.ui.show_tweet_editor(prompt=_('Reply to %s' % author), content=' '.join(mentioned), done_signal_handler=handler) self.editor_mode(editor) @has_active_status def direct_message(self): status = self.timelines.active_status recipient = status.dm_recipients_username(self.user.screen_name) if recipient: handler = self.direct_message_handler editor = self.ui.show_dm_editor(prompt=_('DM to %s' % recipient), content='', recipient=recipient, done_signal_handler=handler) self.editor_mode(editor) else: self.error_message(_('What do you mean?')) @has_active_status def tweet_with_hashtags(self): status = self.timelines.active_status hashtags = ' '.join(status.hashtags) if hashtags: handler = self.tweet_handler content = ''.join([' ', hashtags]) editor = self.ui.show_tweet_editor(prompt=_('%s' % hashtags), content=content, done_signal_handler=handler, cursor_position=0) self.editor_mode(editor) @has_active_status def delete_tweet(self): status = self.timelines.active_status if is_DM(status): self.delete_dm() return author = status.authors_username if (author != self.user.screen_name and status.user != self.user.screen_name): self.error_message(_('You can only delete your own tweets')) return 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.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) @has_active_status def follow_selected(self): status = self.timelines.active_status username = status.authors_username if username == self.user.screen_name: self.error_message(_('You can\'t follow yourself')) return success_message = _('You are now following @%s' % username) follow_done = partial(self.info_message, success_message) error_template = _('We can not ensure that you are following @%s') error_message = error_template % username follow_error = partial(self.error_message, error_message) self.api.create_friendship(screen_name=username, on_error=follow_error, on_success=follow_done) def follow_user(self, prompt=_('Follow user (no need to prepend it with "@"'), content='', cursor_position=None): handler = self.follow_user_handler editor = self.ui.show_text_editor(prompt=prompt, content=content, done_signal_handler=handler, cursor_position=cursor_position) self.editor_mode(editor) def unfollow_user(self, prompt=_('Unfollow user (no need to prepend it with' ' "@"'), content='', cursor_position=None): handler = self.unfollow_user_handler editor = self.ui.show_text_editor(prompt=prompt, content=content, done_signal_handler=handler, cursor_position=cursor_position) self.editor_mode(editor) @has_active_status def unfollow_selected(self): status = self.timelines.active_status username = status.authors_username if username == self.user.screen_name: self.error_message(_('That doesn\'t make any sense')) return success_message = _('You are no longer following %s' % username) unfollow_done = partial(self.info_message, success_message) error_template = _('We can not ensure that you are not following %s') error_message = error_template % username unfollow_error = partial(self.error_message, error_message) self.api.destroy_friendship(screen_name=username, on_error=unfollow_error, on_success=unfollow_done) @has_active_status def favorite(self): status = self.timelines.active_status self._favorite(status) def _favorite(self, status): 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, ) @has_active_status def unfavorite(self): status = self.timelines.active_status 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, ) @has_active_status def user_info(self): status = self.timelines.active_status username = status.authors_username user = self.api.get_user(username) last_statuses = self.api.get_user_timeline(username) self.ui.show_user_info(user, last_statuses) self.user_info_mode(user) # - Configuration --------------------------------------------------------- def reload_configuration(self): configuration.reload() self.redraw_screen() self.info_message(_('Configuration reloaded')) # - Browser --------------------------------------------------------------- @has_active_status def open_urls(self): """ Open the URLs contained on the focused tweets in a browser. """ status = self.timelines.active_status urls = get_urls(status.text) if not urls: self.info_message(_('No URLs found on this tweet')) return self.open_urls_in_browser(urls) @has_active_status def open_status_url(self): """ Open the focused tweet in a browser. """ status = self.timelines.active_status if is_DM(status): message = _('You only can open regular statuses in a browser') self.info_message(message) return self.open_urls_in_browser([status.url]) def open_urls_in_browser(self, urls): """ Open `urls` in $BROWSER if the environment variable is set. """ # The webbrowser module respects the BROWSER environment variable, # so if that's set, it'll use it, otherwise it will try to find # something sensible try: # Firefox, w3m, etc can't handle multiple URLs at command line, so # split the URLs up for them for url in urls: webbrowser.open(url) except Exception as message: logging.exception(message) self.error_message(_('Unable to launch the browser'))
def setUp(self): self.session = Session(mock_api)
class Controller(Observer): """ The :class:`Controller`. """ # Modes INFO_MODE = 0 TIMELINE_MODE = 1 HELP_MODE = 2 EDITOR_MODE = 3 USER_INFO_MODE = 4 # -- Initialization ------------------------------------------------------- def __init__(self, ui, api, timelines): # View self.ui = ui self.api = api self.timelines = timelines # Load session self.session = Session(self.api) self.session.populate(self.timelines) self.editor = None # Default Mode self.mode = self.INFO_MODE # Subscribe to model updates self.timelines.subscribe(self) def start(self): self.main_loop() def authenticate_api(self): self.info_message(_('Authenticating API')) self.api.init_api( on_error=self.api_init_error, on_success=self.init_timelines, ) @async def init_timelines(self): # API has to be authenticated while (not self.api.is_authenticated): pass # fetch the authenticated user self.user = self.api.verify_credentials() # initialize the timelines self.info_message(_('Initializing timelines')) for timeline in self.timelines: timeline.update() timeline.activate_first() self.timeline_mode() self.clear_status() # Main loop has to be running while not getattr(self, 'loop'): pass # update alarm seconds = configuration.twitter['update_frequency'] self.loop.set_alarm_in(seconds, self.update_alarm) def main_loop(self): """ Launch the main loop of the program. """ if not hasattr(self, 'loop'): # Creating the main loop for the first time self.key_handler = KeyHandler(self) handler = self.key_handler.handle self.loop = urwid.MainLoop(self.ui, configuration.palette, handle_mouse=False, unhandled_input=handler) # Authenticate API just before starting main loop self.authenticate_api() try: self.loop.run() except TweepError, message: logging.exception(message) self.error_message(_('API error: %s' % message)) # recover from API errors self.main_loop()