From b6bffd660b189ed8521e3c6d82174eff2ccaad53 Mon Sep 17 00:00:00 2001 From: Michael Lazar Date: Thu, 27 Aug 2015 00:08:16 -0700 Subject: [PATCH 01/20] Upping version. --- CHANGELOG.rst | 14 ++++++++++++++ README.rst | 6 ++++-- rtv/__version__.py | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7164f19..71e46f7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,7 @@ RTV Changelog ============= +.. _1.5: http://github.com/michael-lazar/rtv/releases/tag/v1.5 .. _1.4.2: http://github.com/michael-lazar/rtv/releases/tag/v1.4.2 .. _1.4.1: http://github.com/michael-lazar/rtv/releases/tag/v1.4.1 .. _1.4: http://github.com/michael-lazar/rtv/releases/tag/v1.4 @@ -10,6 +11,19 @@ RTV Changelog .. _1.2.1: http://github.com/michael-lazar/rtv/releases/tag/v1.2.1 .. _1.2: http://github.com/michael-lazar/rtv/releases/tag/v1.2 +----------------- +1.5_ (2015-08-26) +----------------- +Features + +* New page to view and open subscribed subreddits with `s`. +* Sorting method can now be toggled with the `1` - `5` keys. +* Links to x-posts are now opened inside of RTV. + +Bugfixes + +* Added */r/* to subreddit names in the subreddit view. + ------------------- 1.4.2_ (2015-08-01) ------------------- diff --git a/README.rst b/README.rst index 59c68bc..036af33 100644 --- a/README.rst +++ b/README.rst @@ -63,7 +63,7 @@ Basic Commands :``j``/``k`` or ``▲``/``▼``: Move the cursor up/down :``m``/``n`` or ``PgUp``/``PgDn``: Jump to the previous/next page -:``o`` or ``ENTER``: Open the selected item as a webpage +:``1-5``: Toggle post order (*hot*, *top*, *rising*, *new*, *controversial*) :``r`` or ``F5``: Refresh page content :``u``: Log in or switch accounts :``?``: Show the help screen @@ -84,7 +84,7 @@ Once you are logged in your username will appear in the top-right corner of the :``c``: Compose a new post or comment :``e``: Edit an existing post or comment :``d``: Delete an existing post or comment -:``s``: Open/close subscribed subreddits list +:``s``: View a list of subscribed subreddits -------------- Subreddit Mode @@ -93,6 +93,7 @@ Subreddit Mode In subreddit mode you can browse through the top submissions on either the front page or a specific subreddit. :``l`` or ``►``: Enter the selected submission +:``o`` or ``ENTER``: Open the submission link with your web browser :``/``: Open a prompt to switch subreddits :``f``: Open a prompt to search the current subreddit @@ -111,6 +112,7 @@ Submission Mode In submission mode you can view the self text for a submission and browse comments. :``h`` or ``◄``: Return to the subreddit +:``o`` or ``ENTER``: Open the comment permalink with your web browser :``SPACE``: Fold the selected comment, or load additional comments ============= diff --git a/rtv/__version__.py b/rtv/__version__.py index 98d186b..fcb6b5d 100644 --- a/rtv/__version__.py +++ b/rtv/__version__.py @@ -1 +1 @@ -__version__ = '1.4.2' +__version__ = '1.5' From 210eba35fc4473e626fc58a8e4ea3cdbb6abdc28 Mon Sep 17 00:00:00 2001 From: Johnathan Jenkins Date: Thu, 27 Aug 2015 13:07:14 -0700 Subject: [PATCH 02/20] add undocumented function to display new messages. --- rtv/docs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rtv/docs.py b/rtv/docs.py index 8775728..0a912c8 100644 --- a/rtv/docs.py +++ b/rtv/docs.py @@ -34,6 +34,7 @@ Basic Commands `o` or `ENTER` : Open the selected item as a webpage `r` or `F5` : Refresh page content `u` : Log in or switch accounts + `i` : Display new messages prompt `?` : Show the help screen `q` : Quit From b82d4c02263530a5a349cfed6e06a3a32ec28d00 Mon Sep 17 00:00:00 2001 From: Johnathan Jenkins Date: Thu, 27 Aug 2015 13:08:42 -0700 Subject: [PATCH 03/20] add undocumented function to display new messages. --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index 036af33..1a6e066 100644 --- a/README.rst +++ b/README.rst @@ -66,6 +66,7 @@ Basic Commands :``1-5``: Toggle post order (*hot*, *top*, *rising*, *new*, *controversial*) :``r`` or ``F5``: Refresh page content :``u``: Log in or switch accounts +:``i``: Display new messages prompt :``?``: Show the help screen :``q``: Quit From 817e6e432e683bfcf976290a1ccda6a8f624579a Mon Sep 17 00:00:00 2001 From: Johnathan Jenkins Date: Fri, 28 Aug 2015 10:04:39 -0700 Subject: [PATCH 04/20] add undocumented function to display new messages. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 1a6e066..a4cdbfb 100644 --- a/README.rst +++ b/README.rst @@ -66,7 +66,6 @@ Basic Commands :``1-5``: Toggle post order (*hot*, *top*, *rising*, *new*, *controversial*) :``r`` or ``F5``: Refresh page content :``u``: Log in or switch accounts -:``i``: Display new messages prompt :``?``: Show the help screen :``q``: Quit @@ -85,6 +84,7 @@ Once you are logged in your username will appear in the top-right corner of the :``c``: Compose a new post or comment :``e``: Edit an existing post or comment :``d``: Delete an existing post or comment +:``i``: Display new messages prompt :``s``: View a list of subscribed subreddits -------------- From ccadcfe891871032ea5e4c1974db67ed1b69a0e8 Mon Sep 17 00:00:00 2001 From: Johnathan Jenkins Date: Fri, 28 Aug 2015 10:05:09 -0700 Subject: [PATCH 05/20] add undocumented function to display new messages. --- rtv/docs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtv/docs.py b/rtv/docs.py index 0a912c8..e6d8722 100644 --- a/rtv/docs.py +++ b/rtv/docs.py @@ -34,7 +34,6 @@ Basic Commands `o` or `ENTER` : Open the selected item as a webpage `r` or `F5` : Refresh page content `u` : Log in or switch accounts - `i` : Display new messages prompt `?` : Show the help screen `q` : Quit @@ -43,6 +42,7 @@ Authenticated Commands `c` : Compose a new post or comment `e` : Edit an existing post or comment `d` : Delete an existing post or comment + `i` : Display new messages prompt `s` : Open/close subscribed subreddits list Subreddit Mode From d2822ccf8522a2dab9f451d801536a177d9fd38a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Piboub=C3=A8s?= Date: Fri, 28 Aug 2015 20:28:58 +0200 Subject: [PATCH 06/20] Make OAuth compatible with Python 2 --- rtv/__main__.py | 5 +++-- rtv/oauth.py | 18 +++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/rtv/__main__.py b/rtv/__main__.py index 881feff..9970e4a 100644 --- a/rtv/__main__.py +++ b/rtv/__main__.py @@ -79,7 +79,8 @@ def load_oauth_config(): defaults = dict(config.items('oauth')) else: # Populate OAuth section - config['oauth'] = {'auto_login': False} + config.add_section('oauth') + config.set('oauth', 'auto_login', 'false') with open(get_config_fp(), 'w') as cfg: config.write(cfg) defaults = dict(config.items('oauth')) @@ -154,7 +155,7 @@ def main(): reddit.config.decode_html_entities = False with curses_session() as stdscr: oauth = OAuthTool(reddit, stdscr, LoadScreen(stdscr)) - if args.auto_login == 'True': # Ew! + if args.auto_login == 'true': # Ew! oauth.authorize() if args.link: page = SubmissionPage(stdscr, reddit, oauth, url=args.link) diff --git a/rtv/oauth.py b/rtv/oauth.py index 0d3029c..f7d64cf 100644 --- a/rtv/oauth.py +++ b/rtv/oauth.py @@ -1,4 +1,3 @@ -from six.moves import configparser import curses import logging import os @@ -7,6 +6,7 @@ import uuid import webbrowser import praw +from six.moves import configparser from . import config from .curses_helpers import show_notification, prompt_input @@ -111,7 +111,7 @@ class OAuthTool(object): try: with self.loader(message='Refreshing token'): new_access_info = self.reddit.refresh_access_information( - self.config['oauth']['refresh_token']) + self.config.get('oauth', 'refresh_token')) self.access_info = new_access_info self.reddit.set_access_credentials(scope=set(self.access_info['scope']), access_token=self.access_info['access_token'], @@ -121,8 +121,8 @@ class OAuthTool(object): praw.errors.HTTPException) as e: show_notification(self.stdscr, ['Invalid OAuth data']) else: - self.config['oauth']['access_token'] = self.access_info['access_token'] - self.config['oauth']['refresh_token'] = self.access_info['refresh_token'] + self.config.set('oauth', 'access_token', self.access_info['access_token']) + self.config.set('oauth', 'refresh_token', self.access_info['refresh_token']) self.save_config() def authorize(self): @@ -132,7 +132,7 @@ class OAuthTool(object): self.open_config(update=True) # If no previous OAuth data found, starting from scratch - if 'oauth' not in self.config or 'access_token' not in self.config['oauth']: + if not self.config.has_section('oauth') or not self.config.has_option('oauth', 'access_token'): # Generate a random UUID hex_uuid = uuid.uuid4().hex @@ -178,11 +178,11 @@ class OAuthTool(object): except (praw.errors.OAuthAppRequired, praw.errors.OAuthInvalidToken) as e: show_notification(self.stdscr, ['Invalid OAuth data']) else: - if 'oauth' not in self.config: - self.config['oauth'] = {} + if not self.config.has_section('oauth'): + self.config.add_section('oauth') - self.config['oauth']['access_token'] = self.access_info['access_token'] - self.config['oauth']['refresh_token'] = self.access_info['refresh_token'] + self.config.set('oauth', 'access_token', self.access_info['access_token']) + self.config.set('oauth', 'refresh_token', self.access_info['refresh_token']) self.save_config() # Otherwise, fetch new access token else: From c579aa928dad7cefc07027668b005a4c9eb35ed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Piboub=C3=A8s?= Date: Fri, 28 Aug 2015 20:40:40 +0200 Subject: [PATCH 07/20] Additional Python 2 instructions --- README.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.rst b/README.rst index f2394a7..fd14260 100644 --- a/README.rst +++ b/README.rst @@ -46,6 +46,13 @@ The installation will place a script in the system path $ rtv $ rtv --help +If you're having issues running RTV with Python 2, run RTV as module : + +.. code-block:: bash + + $ cd /path/to/rtv + $ python2 -m rtv + ===== Usage ===== From d24c81bce6ef2353e7a408377c4a5362d8799d2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Piboub=C3=A8s?= Date: Fri, 28 Aug 2015 21:13:57 +0200 Subject: [PATCH 08/20] External OAuth configuration file --- README.rst | 19 +++++++++++++------ rtv/__main__.py | 50 ++++++++++++++++++++++++------------------------- rtv/oauth.py | 4 ++-- 3 files changed, 40 insertions(+), 33 deletions(-) diff --git a/README.rst b/README.rst index fd14260..a32e6b1 100644 --- a/README.rst +++ b/README.rst @@ -155,20 +155,21 @@ If you prefer to stay in the terminal, use ``$BROWSER`` to specify a console-bas Config File ----------- -RTV will read a configuration placed at ``~/.config/rtv/rtv.cfg`` (or ``$XDG_CONFIG_HOME``). -Each line in the file will replace the corresponding default argument in the launch script. +RTV will read two configuration files: +* ``~/.config/rtv/rtv.cfg`` (or ``$XDG_CONFIG_HOME/.rtv``) +* ``~/.config/rtv/oauth.cfg`` (or ``$XDG_CONFIG_HOME/.rtv-oauth``) +Each line in the files will replace the corresponding default argument in the launch script. This can be used to avoid having to re-enter login credentials every time the program is launched. -The OAuth section contains a boolean to trigger auto-login (defaults to False). +The OAuth section contains a boolean to trigger auto-login (defaults to false). When authenticated, two additional fields are written : **access_token** and **refresh_token**. Those are basically like username and password : they are used to authenticate you on Reddit servers. Example initial config: -.. code-block:: ini +**rtv.cfg** - [oauth] - auto_login=False +.. code-block:: ini [rtv] # Log file location @@ -184,6 +185,12 @@ Example initial config: # This may be necessary for compatibility with some terminal browsers # ascii=True +**oauth.cfg** + +.. code-block:: ini + + [oauth] + auto_login=false ========= Changelog diff --git a/rtv/__main__.py b/rtv/__main__.py index 9970e4a..8d3c1f8 100644 --- a/rtv/__main__.py +++ b/rtv/__main__.py @@ -22,7 +22,13 @@ from tornado import ioloop __all__ = [] -def get_config_fp(): +def load_rtv_config(): + """ + Attempt to load saved settings for things like the username and password. + """ + + config = configparser.ConfigParser() + HOME = os.path.expanduser('~') XDG_CONFIG_HOME = os.getenv('XDG_CONFIG_HOME', os.path.join(HOME, '.config')) @@ -35,30 +41,9 @@ def get_config_fp(): # get the first existing config file for config_path in config_paths: if os.path.exists(config_path): + config.read(config_path) break - return config_path - -def open_config(): - """ - Search for a configuration file at the location ~/.rtv and attempt to load - saved settings for things like the username and password. - """ - - config = configparser.ConfigParser() - - config_path = get_config_fp() - config.read(config_path) - - return config - -def load_rtv_config(): - """ - Attempt to load saved settings for things like the username and password. - """ - - config = open_config() - defaults = {} if config.has_section('rtv'): defaults = dict(config.items('rtv')) @@ -73,7 +58,22 @@ def load_oauth_config(): Attempt to load saved OAuth settings """ - config = open_config() + config = configparser.ConfigParser() + + HOME = os.path.expanduser('~') + XDG_CONFIG_HOME = os.getenv('XDG_CONFIG_HOME', + os.path.join(HOME, '.config')) + + config_paths = [ + os.path.join(XDG_CONFIG_HOME, 'rtv', 'oauth.cfg'), + os.path.join(HOME, '.rtv-oauth') + ] + + # get the first existing config file + for config_path in config_paths: + if os.path.exists(config_path): + config.read(config_path) + break if config.has_section('oauth'): defaults = dict(config.items('oauth')) @@ -81,7 +81,7 @@ def load_oauth_config(): # Populate OAuth section config.add_section('oauth') config.set('oauth', 'auto_login', 'false') - with open(get_config_fp(), 'w') as cfg: + with open(config_path, 'w') as cfg: config.write(cfg) defaults = dict(config.items('oauth')) diff --git a/rtv/oauth.py b/rtv/oauth.py index f7d64cf..8fcd118 100644 --- a/rtv/oauth.py +++ b/rtv/oauth.py @@ -77,8 +77,8 @@ class OAuthTool(object): os.path.join(HOME, '.config')) config_paths = [ - os.path.join(XDG_CONFIG_HOME, 'rtv', 'rtv.cfg'), - os.path.join(HOME, '.rtv') + os.path.join(XDG_CONFIG_HOME, 'rtv', 'oauth.cfg'), + os.path.join(HOME, '.rtv-oauth') ] # get the first existing config file From 3fb4dc66bb1efa92acc416852268848d263a5acb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Piboub=C3=A8s?= Date: Fri, 28 Aug 2015 21:15:49 +0200 Subject: [PATCH 09/20] README --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index a32e6b1..17628a3 100644 --- a/README.rst +++ b/README.rst @@ -156,8 +156,10 @@ Config File ----------- RTV will read two configuration files: + * ``~/.config/rtv/rtv.cfg`` (or ``$XDG_CONFIG_HOME/.rtv``) * ``~/.config/rtv/oauth.cfg`` (or ``$XDG_CONFIG_HOME/.rtv-oauth``) + Each line in the files will replace the corresponding default argument in the launch script. This can be used to avoid having to re-enter login credentials every time the program is launched. From 314d2dbf2643bf51d7ec5110531ea69a5d8374df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Piboub=C3=A8s?= Date: Mon, 31 Aug 2015 20:37:02 +0200 Subject: [PATCH 10/20] OAuth config file improvements --- rtv/__main__.py | 14 +++++--------- rtv/oauth.py | 15 +++++---------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/rtv/__main__.py b/rtv/__main__.py index 8d3c1f8..ebd8d51 100644 --- a/rtv/__main__.py +++ b/rtv/__main__.py @@ -64,16 +64,12 @@ def load_oauth_config(): XDG_CONFIG_HOME = os.getenv('XDG_CONFIG_HOME', os.path.join(HOME, '.config')) - config_paths = [ - os.path.join(XDG_CONFIG_HOME, 'rtv', 'oauth.cfg'), - os.path.join(HOME, '.rtv-oauth') - ] + if os.path.exists(os.path.join(XDG_CONFIG_HOME, 'rtv')): + config_path = os.path.join(XDG_CONFIG_HOME, 'rtv', 'oauth.cfg') + else: + config_path = os.path.join(HOME, '.rtv-oauth') - # get the first existing config file - for config_path in config_paths: - if os.path.exists(config_path): - config.read(config_path) - break + config.read(config_path) if config.has_section('oauth'): defaults = dict(config.items('oauth')) diff --git a/rtv/oauth.py b/rtv/oauth.py index 8fcd118..8203de6 100644 --- a/rtv/oauth.py +++ b/rtv/oauth.py @@ -76,17 +76,12 @@ class OAuthTool(object): XDG_CONFIG_HOME = os.getenv('XDG_CONFIG_HOME', os.path.join(HOME, '.config')) - config_paths = [ - os.path.join(XDG_CONFIG_HOME, 'rtv', 'oauth.cfg'), - os.path.join(HOME, '.rtv-oauth') - ] + if os.path.exists(os.path.join(XDG_CONFIG_HOME, 'rtv')): + file_path = os.path.join(XDG_CONFIG_HOME, 'rtv', 'oauth.cfg') + else: + file_path = os.path.join(HOME, '.rtv-oauth') - # get the first existing config file - for config_path in config_paths: - if os.path.exists(config_path): - break - - return config_path + return file_path def open_config(self, update=False): if self.config_fp is None: From f6546aaf75592d05d85a14e62f270781f3113818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Piboub=C3=A8s?= Date: Tue, 1 Sep 2015 22:32:56 +0200 Subject: [PATCH 11/20] Let PRAW manage authentication --- rtv/__main__.py | 1 - rtv/oauth.py | 41 ++--------------------- rtv/page.py | 15 --------- rtv/submission.py | 6 ---- rtv/subreddit.py | 11 +----- rtv/{subscriptions.py => subscription.py} | 3 -- 6 files changed, 4 insertions(+), 73 deletions(-) rename rtv/{subscriptions.py => subscription.py} (97%) diff --git a/rtv/__main__.py b/rtv/__main__.py index ebd8d51..0096031 100644 --- a/rtv/__main__.py +++ b/rtv/__main__.py @@ -103,7 +103,6 @@ def command_line(): oauth_group = parser.add_argument_group('OAuth data (optional)', OAUTH) oauth_group.add_argument('--auto-login', dest='auto_login', help='OAuth auto-login setting') - oauth_group.add_argument('--auth-token', dest='access_token', help='OAuth authorization token') oauth_group.add_argument('--refresh-token', dest='refresh_token', help='OAuth refresh token') args = parser.parse_args() diff --git a/rtv/oauth.py b/rtv/oauth.py index 8203de6..e6f0168 100644 --- a/rtv/oauth.py +++ b/rtv/oauth.py @@ -16,8 +16,6 @@ from tornado import ioloop, web __all__ = ['token_validity', 'OAuthTool'] _logger = logging.getLogger(__name__) -token_validity = 3540 - oauth_state = None oauth_code = None oauth_error = None @@ -62,8 +60,6 @@ class OAuthTool(object): self.access_info = {} - self.token_expiration = 0 - # Initialize Tornado webapp and listen on port 65000 self.callback_app = web.Application([ (r'/', HomeHandler), @@ -95,31 +91,6 @@ class OAuthTool(object): with open(self.config_fp, 'w') as cfg: self.config.write(cfg) - def set_token_expiration(self): - self.token_expiration = time.time() + token_validity - - def token_expired(self): - return time.time() > self.token_expiration - - def refresh(self, force=False): - if self.token_expired() or force: - try: - with self.loader(message='Refreshing token'): - new_access_info = self.reddit.refresh_access_information( - self.config.get('oauth', 'refresh_token')) - self.access_info = new_access_info - self.reddit.set_access_credentials(scope=set(self.access_info['scope']), - access_token=self.access_info['access_token'], - refresh_token=self.access_info['refresh_token']) - self.set_token_expiration() - except (praw.errors.OAuthAppRequired, praw.errors.OAuthInvalidToken, - praw.errors.HTTPException) as e: - show_notification(self.stdscr, ['Invalid OAuth data']) - else: - self.config.set('oauth', 'access_token', self.access_info['access_token']) - self.config.set('oauth', 'refresh_token', self.access_info['refresh_token']) - self.save_config() - def authorize(self): self.reddit.set_oauth_app_info(self.client_id, self.client_secret, @@ -127,7 +98,7 @@ class OAuthTool(object): self.open_config(update=True) # If no previous OAuth data found, starting from scratch - if not self.config.has_section('oauth') or not self.config.has_option('oauth', 'access_token'): + if not self.config.has_section('oauth') or not self.config.has_option('oauth', 'refresh_token'): # Generate a random UUID hex_uuid = uuid.uuid4().hex @@ -164,21 +135,15 @@ class OAuthTool(object): with self.loader(message='Logging in'): # Get access information (tokens and scopes) self.access_info = self.reddit.get_access_information(self.final_code) - - self.reddit.set_access_credentials( - scope=set(self.access_info['scope']), - access_token=self.access_info['access_token'], - refresh_token=self.access_info['refresh_token']) - self.set_token_expiration() except (praw.errors.OAuthAppRequired, praw.errors.OAuthInvalidToken) as e: show_notification(self.stdscr, ['Invalid OAuth data']) else: if not self.config.has_section('oauth'): self.config.add_section('oauth') - self.config.set('oauth', 'access_token', self.access_info['access_token']) self.config.set('oauth', 'refresh_token', self.access_info['refresh_token']) self.save_config() # Otherwise, fetch new access token else: - self.refresh(force=True) + with self.loader(message='Logging in'): + self.reddit.refresh_access_information(self.config.get('oauth', 'refresh_token')) diff --git a/rtv/page.py b/rtv/page.py index 8f13565..9eb9025 100644 --- a/rtv/page.py +++ b/rtv/page.py @@ -314,9 +314,6 @@ class BasePage(object): @BaseController.register('a') def upvote(self): - # Refresh access token if expired - self.oauth.refresh() - data = self.content.get(self.nav.absolute_index) try: if 'likes' not in data: @@ -332,9 +329,6 @@ class BasePage(object): @BaseController.register('z') def downvote(self): - # Refresh access token if expired - self.oauth.refresh() - data = self.content.get(self.nav.absolute_index) try: if 'likes' not in data: @@ -372,9 +366,6 @@ class BasePage(object): show_notification(self.stdscr, ['Not logged in']) return - # Refresh access token if expired - self.oauth.refresh() - data = self.content.get(self.nav.absolute_index) if data.get('author') != self.reddit.user.name: curses.flash() @@ -403,9 +394,6 @@ class BasePage(object): show_notification(self.stdscr, ['Not logged in']) return - # Refresh access token if expired - self.oauth.refresh() - data = self.content.get(self.nav.absolute_index) if data.get('author') != self.reddit.user.name: curses.flash() @@ -440,9 +428,6 @@ class BasePage(object): Checks the inbox for unread messages and displays a notification. """ - # Refresh access token if expired - self.oauth.refresh() - inbox = len(list(self.reddit.get_unread(limit=1))) try: if inbox > 0: diff --git a/rtv/submission.py b/rtv/submission.py index 1086ce6..f5aba23 100644 --- a/rtv/submission.py +++ b/rtv/submission.py @@ -93,9 +93,6 @@ class SubmissionPage(BasePage): show_notification(self.stdscr, ['Not logged in']) return - # Refresh access token if expired - self.oauth.refresh() - data = self.content.get(self.nav.absolute_index) if data['type'] == 'Submission': content = data['text'] @@ -131,9 +128,6 @@ class SubmissionPage(BasePage): def delete_comment(self): "Delete a comment as long as it is not the current submission" - # Refresh access token if expired - self.oauth.refresh() - if self.nav.absolute_index != -1: self.delete() else: diff --git a/rtv/subreddit.py b/rtv/subreddit.py index fcd1f07..9359d1e 100644 --- a/rtv/subreddit.py +++ b/rtv/subreddit.py @@ -8,7 +8,7 @@ import requests from .exceptions import SubredditError, AccountError from .page import BasePage, Navigator, BaseController from .submission import SubmissionPage -from .subscriptions import SubscriptionPage +from .subscription import SubscriptionPage from .content import SubredditContent from .helpers import open_browser, open_editor, strip_subreddit_url from .docs import SUBMISSION_FILE @@ -54,9 +54,6 @@ class SubredditPage(BasePage): def refresh_content(self, name=None, order=None): "Re-download all submissions and reset the page index" - # Refresh access token if expired - self.oauth.refresh() - name = name or self.content.name order = order or self.content.order @@ -136,9 +133,6 @@ class SubredditPage(BasePage): show_notification(self.stdscr, ['Not logged in']) return - # Refresh access token if expired - self.oauth.refresh() - # Strips the subreddit to just the name # Make sure it is a valid subreddit for submission subreddit = self.reddit.get_subreddit(self.content.name) @@ -180,9 +174,6 @@ class SubredditPage(BasePage): show_notification(self.stdscr, ['Not logged in']) return - # Refresh access token if expired - self.oauth.refresh() - # Open subscriptions page page = SubscriptionPage(self.stdscr, self.reddit, self.oauth) page.loop() diff --git a/rtv/subscriptions.py b/rtv/subscription.py similarity index 97% rename from rtv/subscriptions.py rename to rtv/subscription.py index 64e3a2a..0361427 100644 --- a/rtv/subscriptions.py +++ b/rtv/subscription.py @@ -38,9 +38,6 @@ class SubscriptionPage(BasePage): def refresh_content(self): "Re-download all subscriptions and reset the page index" - # Refresh access token if expired - self.oauth.refresh() - self.content = SubscriptionContent.from_user(self.reddit, self.loader) self.nav = Navigator(self.content.get) From 6933b35240586732f7160897aa03091922c35bc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Piboub=C3=A8s?= Date: Fri, 4 Sep 2015 18:18:31 +0200 Subject: [PATCH 12/20] Avoid infinite loop if server crashes --- rtv/oauth.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/rtv/oauth.py b/rtv/oauth.py index e6f0168..b81ddd1 100644 --- a/rtv/oauth.py +++ b/rtv/oauth.py @@ -28,17 +28,18 @@ class HomeHandler(web.RequestHandler): class AuthHandler(web.RequestHandler): def get(self): - global oauth_state - global oauth_code - global oauth_error + try: + global oauth_state + global oauth_code + global oauth_error - oauth_state = self.get_argument('state', default='state_placeholder') - oauth_code = self.get_argument('code', default='code_placeholder') - oauth_error = self.get_argument('error', default='error_placeholder') + oauth_state = self.get_argument('state', default='state_placeholder') + oauth_code = self.get_argument('code', default='code_placeholder') + oauth_error = self.get_argument('error', default='error_placeholder') - self.render('auth.html', state=oauth_state, code=oauth_code, error=oauth_error) - - ioloop.IOLoop.current().stop() + self.render('auth.html', state=oauth_state, code=oauth_code, error=oauth_error) + finally: + ioloop.IOLoop.current().stop() class OAuthTool(object): From eaed5142080736a561051c656c4bdc3673c3abfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Piboub=C3=A8s?= Date: Fri, 4 Sep 2015 18:22:01 +0200 Subject: [PATCH 13/20] OAuth flow terminal web browser support --- rtv/oauth.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/rtv/oauth.py b/rtv/oauth.py index b81ddd1..afa0c32 100644 --- a/rtv/oauth.py +++ b/rtv/oauth.py @@ -58,9 +58,11 @@ class OAuthTool(object): self.redirect_uri = redirect_uri or config.oauth_redirect_uri self.scope = scope or config.oauth_scope.split('-') - self.access_info = {} + # Terminal web browser + self.compact = os.environ.get('BROWSER') in ['w3m', 'links', 'elinks', 'lynx'] + # Initialize Tornado webapp and listen on port 65000 self.callback_app = web.Application([ (r'/', HomeHandler), @@ -93,6 +95,9 @@ class OAuthTool(object): self.config.write(cfg) def authorize(self): + if self.compact and not '.compact' in self.reddit.config.API_PATHS['authorize']: + self.reddit.config.API_PATHS['authorize'] += '.compact' + self.reddit.set_oauth_app_info(self.client_id, self.client_secret, self.redirect_uri) @@ -106,9 +111,16 @@ class OAuthTool(object): permission_ask_page_link = self.reddit.get_authorize_url(str(hex_uuid), scope=self.scope, refreshable=True) - with self.loader(message='Waiting for authorization'): - webbrowser.open(permission_ask_page_link) + if self.compact: + show_notification(self.stdscr, ['Opening ' + os.environ.get('BROWSER')]) + curses.endwin() + webbrowser.open_new_tab(permission_ask_page_link) ioloop.IOLoop.current().start() + curses.doupdate() + else: + with self.loader(message='Waiting for authorization'): + webbrowser.open(permission_ask_page_link) + ioloop.IOLoop.current().start() global oauth_state global oauth_code From b643ce955909d248be6ee87665ee1f6d7e1799dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Piboub=C3=A8s?= Date: Fri, 4 Sep 2015 18:23:10 +0200 Subject: [PATCH 14/20] Complete logout --- rtv/oauth.py | 6 ++++++ rtv/page.py | 1 + 2 files changed, 7 insertions(+) diff --git a/rtv/oauth.py b/rtv/oauth.py index afa0c32..7edf7b5 100644 --- a/rtv/oauth.py +++ b/rtv/oauth.py @@ -94,6 +94,12 @@ class OAuthTool(object): with open(self.config_fp, 'w') as cfg: self.config.write(cfg) + def clear_oauth_data(self): + self.open_config(update=True) + if self.config.has_section('oauth') and self.config.has_option('oauth', 'refresh_token'): + self.config.remove_option('oauth', 'refresh_token') + self.save_config() + def authorize(self): if self.compact and not '.compact' in self.reddit.config.API_PATHS['authorize']: self.reddit.config.API_PATHS['authorize'] += '.compact' diff --git a/rtv/page.py b/rtv/page.py index 9eb9025..34a5467 100644 --- a/rtv/page.py +++ b/rtv/page.py @@ -352,6 +352,7 @@ class BasePage(object): if self.reddit.is_oauth_session(): self.reddit.clear_authentication() + self.oauth.clear_oauth_data() return self.oauth.authorize() From 1f0ca4d59264bd1e924198d1cfe1ecececa8ef4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Piboub=C3=A8s?= Date: Fri, 4 Sep 2015 18:39:22 +0200 Subject: [PATCH 15/20] Update README.rst and docs --- README.rst | 24 ++++++++++++++---------- rtv/docs.py | 4 ++-- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index 17628a3..7de1152 100644 --- a/README.rst +++ b/README.rst @@ -155,18 +155,10 @@ If you prefer to stay in the terminal, use ``$BROWSER`` to specify a console-bas Config File ----------- -RTV will read two configuration files: - -* ``~/.config/rtv/rtv.cfg`` (or ``$XDG_CONFIG_HOME/.rtv``) -* ``~/.config/rtv/oauth.cfg`` (or ``$XDG_CONFIG_HOME/.rtv-oauth``) - +RTV will read a configuration placed at ``~/.config/rtv/rtv.cfg`` (or ``$XDG_CONFIG_HOME``). Each line in the files will replace the corresponding default argument in the launch script. This can be used to avoid having to re-enter login credentials every time the program is launched. -The OAuth section contains a boolean to trigger auto-login (defaults to false). -When authenticated, two additional fields are written : **access_token** and **refresh_token**. -Those are basically like username and password : they are used to authenticate you on Reddit servers. - Example initial config: **rtv.cfg** @@ -187,7 +179,19 @@ Example initial config: # This may be necessary for compatibility with some terminal browsers # ascii=True -**oauth.cfg** +----- +OAuth +----- + +OAuth is an authentication standard, that replaces authentication with login and password. + +RTV implements OAuth. It stores OAuth configuration at ``~/.config/rtv/oauth.cfg``(or ``$XDG_CONFIG_HOME``). +**The OAuth configuration file must be writable, and is created automatically if it doesn't exist.** +It contains a boolean to trigger auto-login (defaults to false). +When authenticated, an additional field is written : **refresh_token**. +This acts as a replacement to username and password : it is used to authenticate you on Reddit servers. + +Example **oauth.cfg**: .. code-block:: ini diff --git a/rtv/docs.py b/rtv/docs.py index cefd7b0..4f427c8 100644 --- a/rtv/docs.py +++ b/rtv/docs.py @@ -18,8 +18,8 @@ given, the program will display a secure prompt to enter a password. """ OAUTH = """\ -Authentication is now done by OAuth, since PRAW will stop supporting login with -username and password soon. +Authentication is now done by OAuth, since PRAW will drop +password authentication soon. """ CONTROLS = """ From 0134e157d083e18f7e1dcbbe663fdd8eb22fdc98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Piboub=C3=A8s?= Date: Fri, 4 Sep 2015 20:09:58 +0200 Subject: [PATCH 16/20] Listen on port only when web server is needed --- rtv/oauth.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rtv/oauth.py b/rtv/oauth.py index 7edf7b5..307cf4a 100644 --- a/rtv/oauth.py +++ b/rtv/oauth.py @@ -63,12 +63,11 @@ class OAuthTool(object): # Terminal web browser self.compact = os.environ.get('BROWSER') in ['w3m', 'links', 'elinks', 'lynx'] - # Initialize Tornado webapp and listen on port 65000 + # Initialize Tornado webapp self.callback_app = web.Application([ (r'/', HomeHandler), (r'/auth', AuthHandler), ], template_path='rtv/templates') - self.callback_app.listen(65000) def get_config_fp(self): HOME = os.path.expanduser('~') @@ -111,6 +110,9 @@ class OAuthTool(object): self.open_config(update=True) # If no previous OAuth data found, starting from scratch if not self.config.has_section('oauth') or not self.config.has_option('oauth', 'refresh_token'): + # Start HTTP server and listen on port 65000 + self.callback_app.listen(65000) + # Generate a random UUID hex_uuid = uuid.uuid4().hex From 8e6758a38990ca7ea1ac6309c486c6121c2df78f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Piboub=C3=A8s?= Date: Sat, 5 Sep 2015 16:25:34 +0200 Subject: [PATCH 17/20] Open terminal web browser asynchronously --- rtv/oauth.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/rtv/oauth.py b/rtv/oauth.py index 307cf4a..f696918 100644 --- a/rtv/oauth.py +++ b/rtv/oauth.py @@ -11,7 +11,8 @@ from six.moves import configparser from . import config from .curses_helpers import show_notification, prompt_input -from tornado import ioloop, web +from tornado import gen, ioloop, web +from concurrent.futures import ThreadPoolExecutor __all__ = ['token_validity', 'OAuthTool'] _logger = logging.getLogger(__name__) @@ -28,18 +29,15 @@ class HomeHandler(web.RequestHandler): class AuthHandler(web.RequestHandler): def get(self): - try: - global oauth_state - global oauth_code - global oauth_error + global oauth_state + global oauth_code + global oauth_error - oauth_state = self.get_argument('state', default='state_placeholder') - oauth_code = self.get_argument('code', default='code_placeholder') - oauth_error = self.get_argument('error', default='error_placeholder') + oauth_state = self.get_argument('state', default='state_placeholder') + oauth_code = self.get_argument('code', default='code_placeholder') + oauth_error = self.get_argument('error', default='error_placeholder') - self.render('auth.html', state=oauth_state, code=oauth_code, error=oauth_error) - finally: - ioloop.IOLoop.current().stop() + self.render('auth.html', state=oauth_state, code=oauth_code, error=oauth_error) class OAuthTool(object): @@ -99,6 +97,13 @@ class OAuthTool(object): self.config.remove_option('oauth', 'refresh_token') self.save_config() + @gen.coroutine + def open_terminal_browser(self, url): + with ThreadPoolExecutor(max_workers=1) as executor: + yield executor.submit(webbrowser.open_new_tab, url) + + ioloop.IOLoop.current().stop() + def authorize(self): if self.compact and not '.compact' in self.reddit.config.API_PATHS['authorize']: self.reddit.config.API_PATHS['authorize'] += '.compact' @@ -112,7 +117,7 @@ class OAuthTool(object): if not self.config.has_section('oauth') or not self.config.has_option('oauth', 'refresh_token'): # Start HTTP server and listen on port 65000 self.callback_app.listen(65000) - + # Generate a random UUID hex_uuid = uuid.uuid4().hex @@ -122,7 +127,7 @@ class OAuthTool(object): if self.compact: show_notification(self.stdscr, ['Opening ' + os.environ.get('BROWSER')]) curses.endwin() - webbrowser.open_new_tab(permission_ask_page_link) + ioloop.IOLoop.current().add_callback(self.open_terminal_browser, permission_ask_page_link) ioloop.IOLoop.current().start() curses.doupdate() else: From e90dcc6e5cd478e5f27a97520ccd4a0299bb9ce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Piboub=C3=A8s?= Date: Sat, 5 Sep 2015 16:29:31 +0200 Subject: [PATCH 18/20] Start HTTP server only once --- rtv/oauth.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/rtv/oauth.py b/rtv/oauth.py index f696918..1dfcc67 100644 --- a/rtv/oauth.py +++ b/rtv/oauth.py @@ -11,7 +11,7 @@ from six.moves import configparser from . import config from .curses_helpers import show_notification, prompt_input -from tornado import gen, ioloop, web +from tornado import gen, ioloop, web, httpserver from concurrent.futures import ThreadPoolExecutor __all__ = ['token_validity', 'OAuthTool'] @@ -67,6 +67,8 @@ class OAuthTool(object): (r'/auth', AuthHandler), ], template_path='rtv/templates') + self.http_server = None + def get_config_fp(self): HOME = os.path.expanduser('~') XDG_CONFIG_HOME = os.getenv('XDG_CONFIG_HOME', @@ -115,8 +117,9 @@ class OAuthTool(object): self.open_config(update=True) # If no previous OAuth data found, starting from scratch if not self.config.has_section('oauth') or not self.config.has_option('oauth', 'refresh_token'): - # Start HTTP server and listen on port 65000 - self.callback_app.listen(65000) + if self.http_server is None: + self.http_server = httpserver.HTTPServer(self.callback_app) + self.http_server.listen(65000) # Generate a random UUID hex_uuid = uuid.uuid4().hex From a84ac2f61e4ec6e81bcce926618eb01bcccf4eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Piboub=C3=A8s?= Date: Sat, 5 Sep 2015 16:36:12 +0200 Subject: [PATCH 19/20] Stop IOLoop when callback page reached with GUI browser --- rtv/oauth.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/rtv/oauth.py b/rtv/oauth.py index 1dfcc67..6a51ada 100644 --- a/rtv/oauth.py +++ b/rtv/oauth.py @@ -28,6 +28,9 @@ class HomeHandler(web.RequestHandler): class AuthHandler(web.RequestHandler): + def initialize(self): + self.compact = os.environ.get('BROWSER') in ['w3m', 'links', 'elinks', 'lynx'] + def get(self): global oauth_state global oauth_code @@ -39,6 +42,10 @@ class AuthHandler(web.RequestHandler): self.render('auth.html', state=oauth_state, code=oauth_code, error=oauth_error) + # Stop IOLoop if using BackgroundBrowser (or GUI browser) + if not self.compact: + ioloop.IOLoop.current().stop() + class OAuthTool(object): def __init__(self, reddit, stdscr=None, loader=None, From c8a2b480421f279f9df6aaf50967b60fb8fa09ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Piboub=C3=A8s?= Date: Sun, 6 Sep 2015 02:39:41 +0200 Subject: [PATCH 20/20] Python 2 concurrent.futures backport requirement --- setup.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 38d0d08..f00c25d 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,14 @@ from setuptools import setup from version import __version__ as version +import sys + +requirements = ['tornado', 'praw>=3.1.0', 'six', 'requests', 'kitchen'] + +# Python 2: add required concurrent.futures backport from Python 3.2 +if sys.version_info.major <= 2: + requirements.append('futures') + setup( name='rtv', version=version, @@ -13,7 +21,7 @@ setup( keywords='reddit terminal praw curses', packages=['rtv'], include_package_data=True, - install_requires=['tornado', 'praw>=3.1.0', 'six', 'requests', 'kitchen'], + install_requires=requirements, entry_points={'console_scripts': ['rtv=rtv.__main__:main']}, classifiers=[ 'Intended Audience :: End Users/Desktop',