diff --git a/MANIFEST.in b/MANIFEST.in index 6ca17b7..cb6994a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,7 @@ include version.py include CHANGELOG.rst CONTRIBUTORS.rst LICENSE +<<<<<<< HEAD include rtv.1 +======= +include rtv/templates/* +>>>>>>> 28d17b28d0840f75386586686897e9316378150e diff --git a/README.rst b/README.rst index a4cdbfb..72616a6 100644 --- a/README.rst +++ b/README.rst @@ -73,11 +73,8 @@ Basic Commands Authenticated Commands ---------------------- -Some actions require that you be logged in to your reddit account. To log in you can either: - -1. provide your username as a command line argument ``-u`` (your password will be securely prompted), or -2. press ``u`` while inside of the program - +Some actions require that you be logged in to your reddit account. +You can log in by pressing ``u`` while inside of the program. Once you are logged in your username will appear in the top-right corner of the screen. :``a``/``z``: Upvote/downvote @@ -147,6 +144,22 @@ If you prefer to stay in the terminal, use ``$BROWSER`` to specify a console-bas $ export BROWSER=w3m +-------------- +Authentication +-------------- + +RTV use OAuth to facilitate logging into your reddit user account [#]_. The login process follows these steps: + +1. You initiate a login by pressing the ``u`` key. +2. You're redirected to a webbrowser where reddit will ask you to login and authorize RTV. +3. RTV uses the generated token to login on your behalf. +4. The token is stored on your computer at ``~/.config/rtv/refresh-token`` for future sessions. You can disable this behavior by setting ``persistent=False`` in your RTV config. + +Note that RTV no longer allows you to input your username/password directly. This method of cookie based authentication has been deprecated by reddit and will not be supported in future releases [#]_. + +.. [#] ``_ +.. [#] ``_ + ----------- Config File ----------- @@ -155,14 +168,13 @@ RTV will read a configuration placed at ``~/.config/rtv/rtv.cfg`` (or ``$XDG_CON Each line in the file 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. -Example config: +Example initial config: + +**rtv.cfg** .. code-block:: ini [rtv] - username=MyUsername - password=MySecretPassword - # Log file location log=/tmp/rtv.log @@ -176,6 +188,24 @@ Example config: # This may be necessary for compatibility with some terminal browsers # ascii=True + # Enable persistent storage of your authentication token + # This allows you to remain logged in when you restart the program + persistent=True + + +=== +FAQ +=== + +How do I run the code directly using python? + This project is structured to be run as a python *module*. This means that in order to resolve imports you need to launch using python's ``-m`` flag. This method works for all versions of python. Follow the example below, which assumes that you have cloned the repository into the directory **~/rtv_project**. + + .. code-block:: bash + + $ cd ~/rtv_project + $ python2 -m rtv + $ python3 -m rtv + ========= Changelog diff --git a/rtv/__main__.py b/rtv/__main__.py index 8d6e561..06d0e19 100644 --- a/rtv/__main__.py +++ b/rtv/__main__.py @@ -7,48 +7,25 @@ import logging import requests import praw import praw.errors -from six.moves import configparser +import tornado from . import config from .exceptions import SubmissionError, SubredditError, SubscriptionError, ProgramError -from .curses_helpers import curses_session +from .curses_helpers import curses_session, LoadScreen from .submission import SubmissionPage from .subreddit import SubredditPage from .docs import * +from .oauth import OAuthTool from .__version__ import __version__ __all__ = [] -def load_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() - - 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', 'rtv.cfg'), - os.path.join(HOME, '.rtv') - ] - - # read only the first existing config file - for config_path in config_paths: - if os.path.exists(config_path): - config.read(config_path) - break - - defaults = {} - if config.has_section('rtv'): - defaults = dict(config.items('rtv')) - - if 'ascii' in defaults: - defaults['ascii'] = config.getboolean('rtv', 'ascii') - - return defaults - +# Pycharm debugging note: +# You can use pycharm to debug a curses application by launching rtv in a +# console window (python -m rtv) and using pycharm to attach to the remote +# process. On Ubuntu, you may need to allow ptrace permissions by setting +# ptrace_scope to 0 in /etc/sysctl.d/10-ptrace.conf. +# http://blog.mellenthin.de/archives/2010/10/18/gdb-attach-fails def command_line(): @@ -57,71 +34,68 @@ def command_line(): epilog=CONTROLS + HELP, formatter_class=argparse.RawDescriptionHelpFormatter) - parser.add_argument('-s', dest='subreddit', help='subreddit name') - parser.add_argument('-l', dest='link', help='full link to a submission') - parser.add_argument('--ascii', action='store_true', - help='enable ascii-only mode') - parser.add_argument('--log', metavar='FILE', action='store', - help='Log HTTP requests') - - group = parser.add_argument_group('authentication (optional)', AUTH) - group.add_argument('-u', dest='username', help='reddit username') - group.add_argument('-p', dest='password', help='reddit password') - + parser.add_argument('-s', dest='subreddit', help='name of the subreddit that will be opened on start') + parser.add_argument('-l', dest='link', help='full URL of a submission that will be opened on start') + parser.add_argument('--ascii', action='store_true', help='enable ascii-only mode') + parser.add_argument('--log', metavar='FILE', action='store', help='log HTTP requests to a file') + parser.add_argument('--non-persistent', dest='persistent', action='store_false', help='Forget all authenticated users when the program exits') + parser.add_argument('--clear-auth', dest='clear_auth', action='store_true', help='Remove any saved OAuth tokens before starting') args = parser.parse_args() return args - def main(): "Main entry point" - # logging.basicConfig(level=logging.DEBUG, filename='rtv.log') + # Squelch SSL warnings + logging.captureWarnings(True) locale.setlocale(locale.LC_ALL, '') - args = command_line() - local_config = load_config() - - # set the terminal title + # Set the terminal title title = 'rtv {0}'.format(__version__) if os.name == 'nt': os.system('title {0}'.format(title)) else: sys.stdout.write("\x1b]2;{0}\x07".format(title)) - # Fill in empty arguments with config file values. Paramaters explicitly + # Fill in empty arguments with config file values. Parameters explicitly # typed on the command line will take priority over config file params. + args = command_line() + local_config = config.load_config() for key, val in local_config.items(): if getattr(args, key, None) is None: setattr(args, key, val) - config.unicode = (not args.ascii) - - # Squelch SSL warnings for Ubuntu - logging.captureWarnings(True) + if args.ascii: + config.unicode = False + if not args.persistent: + config.persistent = False if args.log: logging.basicConfig(level=logging.DEBUG, filename=args.log) + if args.clear_auth: + config.clear_refresh_token() try: print('Connecting...') reddit = praw.Reddit(user_agent=AGENT) reddit.config.decode_html_entities = False - if args.username: - # PRAW will prompt for password if it is None - reddit.login(args.username, args.password) with curses_session() as stdscr: + oauth = OAuthTool(reddit, stdscr, LoadScreen(stdscr)) + if oauth.refresh_token: + oauth.authorize() + if args.link: - page = SubmissionPage(stdscr, reddit, url=args.link) + page = SubmissionPage(stdscr, reddit, oauth, url=args.link) page.loop() subreddit = args.subreddit or 'front' - page = SubredditPage(stdscr, reddit, subreddit) + page = SubredditPage(stdscr, reddit, oauth, subreddit) page.loop() - except praw.errors.InvalidUserPass: - print('Invalid password for username: {}'.format(args.username)) - except requests.ConnectionError: - print('Connection timeout') - except requests.HTTPError: + except (praw.errors.OAuthAppRequired, praw.errors.OAuthInvalidToken): + print('Invalid OAuth data') + except praw.errors.NotFound: print('HTTP Error: 404 Not Found') + except praw.errors.HTTPException: + print('Connection timeout') except SubmissionError as e: print('Could not reach submission URL: {}'.format(e.url)) except SubredditError as e: @@ -134,5 +108,7 @@ def main(): finally: # Ensure sockets are closed to prevent a ResourceWarning reddit.handler.http.close() + # Explicitly close file descriptors opened by Tornado's IOLoop + tornado.ioloop.IOLoop.current().close(all_fds=True) sys.exit(main()) diff --git a/rtv/config.py b/rtv/config.py index c59a16d..999dfce 100644 --- a/rtv/config.py +++ b/rtv/config.py @@ -1,5 +1,60 @@ """ Global configuration settings """ +import os +from six.moves import configparser -unicode = True \ No newline at end of file +HOME = os.path.expanduser('~') +XDG_HOME = os.getenv('XDG_CONFIG_HOME', os.path.join(HOME, '.config')) +CONFIG = os.path.join(XDG_HOME, 'rtv', 'rtv.cfg') +TOKEN = os.path.join(XDG_HOME, 'rtv', 'refresh-token') + +unicode = True +persistent = True + +# https://github.com/reddit/reddit/wiki/OAuth2 +# Client ID is of type "installed app" and the secret should be left empty +oauth_client_id = 'E2oEtRQfdfAfNQ' +oauth_client_secret = 'praw_gapfill' +oauth_redirect_uri = 'http://127.0.0.1:65000/' +oauth_redirect_port = 65000 +oauth_scope = ['edit', 'history', 'identity', 'mysubreddits', 'privatemessages', + 'read', 'report', 'save', 'submit', 'subscribe', 'vote'] + +def load_config(): + """ + Attempt to load settings from the local config file. + """ + + config = configparser.ConfigParser() + if os.path.exists(CONFIG): + config.read(CONFIG) + + config_dict = {} + if config.has_section('rtv'): + config_dict = dict(config.items('rtv')) + + # Convert 'true'/'false' to boolean True/False + if 'ascii' in config_dict: + config_dict['ascii'] = config.getboolean('rtv', 'ascii') + if 'clear_auth' in config_dict: + config_dict['clear_auth'] = config.getboolean('rtv', 'clear_auth') + if 'persistent' in config_dict: + config_dict['persistent'] = config.getboolean('rtv', 'persistent') + + return config_dict + +def load_refresh_token(filename=TOKEN): + if os.path.exists(filename): + with open(filename) as fp: + return fp.read().strip() + else: + return None + +def save_refresh_token(token, filename=TOKEN): + with open(filename, 'w+') as fp: + fp.write(token) + +def clear_refresh_token(filename=TOKEN): + if os.path.exists(filename): + os.remove(filename) diff --git a/rtv/docs.py b/rtv/docs.py index e6d8722..66aa6c2 100644 --- a/rtv/docs.py +++ b/rtv/docs.py @@ -1,6 +1,6 @@ from .__version__ import __version__ -__all__ = ['AGENT', 'SUMMARY', 'AUTH', 'CONTROLS', 'HELP', 'COMMENT_FILE', +__all__ = ['AGENT', 'SUMMARY', 'CONTROLS', 'HELP', 'COMMENT_FILE', 'SUBMISSION_FILE', 'COMMENT_EDIT_FILE'] AGENT = """\ @@ -12,11 +12,6 @@ Reddit Terminal Viewer is a lightweight browser for www.reddit.com built into a terminal window. """ -AUTH = """\ -Authenticating is required to vote and leave comments. If only a username is -given, the program will display a secure prompt to enter a password. -""" - CONTROLS = """ Controls -------- diff --git a/rtv/helpers.py b/rtv/helpers.py index e07434e..e666cdc 100644 --- a/rtv/helpers.py +++ b/rtv/helpers.py @@ -14,7 +14,8 @@ from . import config from .exceptions import ProgramError __all__ = ['open_browser', 'clean', 'wrap_text', 'strip_textpad', - 'strip_subreddit_url', 'humanize_timestamp', 'open_editor'] + 'strip_subreddit_url', 'humanize_timestamp', 'open_editor', + 'check_browser_display'] def clean(string, n_cols=None): @@ -102,21 +103,7 @@ def open_browser(url): are not detected here. """ - console_browsers = ['www-browser', 'links', 'links2', 'elinks', 'lynx', 'w3m'] - - display = bool(os.environ.get("DISPLAY")) - - # Use the convention defined here to parse $BROWSER - # https://docs.python.org/2/library/webbrowser.html - if "BROWSER" in os.environ: - user_browser = os.environ["BROWSER"].split(os.pathsep)[0] - if user_browser in console_browsers: - display = False - - if webbrowser._tryorder and webbrowser._tryorder[0] in console_browsers: - display = False - - if display: + if check_browser_display(): command = "import webbrowser; webbrowser.open_new_tab('%s')" % url args = [sys.executable, '-c', command] with open(os.devnull, 'ab+', 0) as null: @@ -127,6 +114,28 @@ def open_browser(url): curses.doupdate() +def check_browser_display(): + """ + Use a number of methods to guess if the default webbrowser will open in + the background as opposed to opening directly in the terminal. + """ + + display = bool(os.environ.get("DISPLAY")) + + # Use the convention defined here to parse $BROWSER + # https://docs.python.org/2/library/webbrowser.html + console_browsers = ['www-browser', 'links', 'links2', 'elinks', 'lynx', 'w3m'] + if "BROWSER" in os.environ: + user_browser = os.environ["BROWSER"].split(os.pathsep)[0] + if user_browser in console_browsers: + display = False + + if webbrowser._tryorder and webbrowser._tryorder[0] in console_browsers: + display = False + + return display + + def wrap_text(text, width): """ Wrap text paragraphs to the given character width while preserving newlines. diff --git a/rtv/oauth.py b/rtv/oauth.py new file mode 100644 index 0000000..62aee86 --- /dev/null +++ b/rtv/oauth.py @@ -0,0 +1,121 @@ +import os +import time +import uuid + +import praw +from tornado import gen, ioloop, web, httpserver +from concurrent.futures import ThreadPoolExecutor + +from . import config +from .curses_helpers import show_notification, prompt_input +from .helpers import check_browser_display, open_browser + +__all__ = ['OAuthTool'] + +oauth_state = None +oauth_code = None +oauth_error = None + +class AuthHandler(web.RequestHandler): + + def get(self): + global oauth_state, oauth_code, oauth_error + + oauth_state = self.get_argument('state', default='placeholder') + oauth_code = self.get_argument('code', default='placeholder') + oauth_error = self.get_argument('error', default='placeholder') + + self.render('index.html', state=oauth_state, code=oauth_code, error=oauth_error) + + # Stop IOLoop if using a background browser such as firefox + if check_browser_display(): + ioloop.IOLoop.current().stop() + +class OAuthTool(object): + + def __init__(self, reddit, stdscr=None, loader=None): + + self.reddit = reddit + self.stdscr = stdscr + self.loader = loader + self.http_server = None + + self.refresh_token = config.load_refresh_token() + + # Initialize Tornado webapp + routes = [('/', AuthHandler)] + self.callback_app = web.Application(routes, template_path='templates') + + self.reddit.set_oauth_app_info(config.oauth_client_id, + config.oauth_client_secret, + config.oauth_redirect_uri) + + # Reddit's mobile website works better on terminal browsers + if not check_browser_display(): + if '.compact' not in self.reddit.config.API_PATHS['authorize']: + self.reddit.config.API_PATHS['authorize'] += '.compact' + + def authorize(self): + + # If we already have a token, request new access credentials + if self.refresh_token: + with self.loader(message='Logging in'): + self.reddit.refresh_access_information(self.refresh_token) + return + + # Start the authorization callback server + if self.http_server is None: + self.http_server = httpserver.HTTPServer(self.callback_app) + self.http_server.listen(config.oauth_redirect_port) + + hex_uuid = uuid.uuid4().hex + authorize_url = self.reddit.get_authorize_url( + hex_uuid, scope=config.oauth_scope, refreshable=True) + + # Open the browser and wait for the user to authorize the app + if check_browser_display(): + with self.loader(message='Waiting for authorization'): + open_browser(authorize_url) + ioloop.IOLoop.current().start() + else: + with self.loader(delay=0, message='Redirecting to reddit'): + # Provide user feedback + time.sleep(1) + ioloop.IOLoop.current().add_callback(self._open_authorize_url, + authorize_url) + ioloop.IOLoop.current().start() + + if oauth_error == 'access_denied': + show_notification(self.stdscr, ['Declined access']) + return + elif oauth_error != 'placeholder': + show_notification(self.stdscr, ['Authentication error']) + return + elif hex_uuid != oauth_state: + # Check if UUID matches obtained state. + # If not, authorization process is compromised. + show_notification(self.stdscr, ['UUID mismatch']) + return + + try: + with self.loader(message='Logging in'): + access_info = self.reddit.get_access_information(oauth_code) + self.refresh_token = access_info['refresh_token'] + if config.persistent: + config.save_refresh_token(access_info['refresh_token']) + except (praw.errors.OAuthAppRequired, praw.errors.OAuthInvalidToken): + show_notification(self.stdscr, ['Invalid OAuth data']) + else: + message = ['Welcome {}!'.format(self.reddit.user.name)] + show_notification(self.stdscr, message) + + def clear_oauth_data(self): + self.reddit.clear_authentication() + config.clear_refresh_token() + self.refresh_token = None + + @gen.coroutine + def _open_authorize_url(self, url): + with ThreadPoolExecutor(max_workers=1) as executor: + yield executor.submit(open_browser, url) + ioloop.IOLoop.current().stop() diff --git a/rtv/page.py b/rtv/page.py index ef075d6..b4a4dde 100644 --- a/rtv/page.py +++ b/rtv/page.py @@ -238,17 +238,18 @@ class BaseController(object): class BasePage(object): """ - Base terminal viewer incorperates a cursor to navigate content + Base terminal viewer incorporates a cursor to navigate content """ MIN_HEIGHT = 10 MIN_WIDTH = 20 - def __init__(self, stdscr, reddit, content, **kwargs): + def __init__(self, stdscr, reddit, content, oauth, **kwargs): self.stdscr = stdscr self.reddit = reddit self.content = content + self.oauth = oauth self.nav = Navigator(self.content.get, **kwargs) self._header_window = None @@ -348,23 +349,11 @@ class BasePage(object): account. """ - if self.reddit.is_logged_in(): - self.logout() - return - - username = prompt_input(self.stdscr, 'Enter username:') - password = prompt_input(self.stdscr, 'Enter password:', hide=True) - if not username or not password: - curses.flash() - return - - try: - with self.loader(message='Logging in'): - self.reddit.login(username, password) - except praw.errors.InvalidUserPass: - show_notification(self.stdscr, ['Invalid user/pass']) + if self.reddit.is_oauth_session(): + self.oauth.clear_oauth_data() + show_notification(self.stdscr, ['Logged out']) else: - show_notification(self.stdscr, ['Welcome {}'.format(username)]) + self.oauth.authorize() @BaseController.register('d') def delete(self): @@ -372,7 +361,7 @@ class BasePage(object): Delete a submission or comment. """ - if not self.reddit.is_logged_in(): + if not self.reddit.is_oauth_session(): show_notification(self.stdscr, ['Not logged in']) return @@ -400,7 +389,7 @@ class BasePage(object): Edit a submission or comment. """ - if not self.reddit.is_logged_in(): + if not self.reddit.is_oauth_session(): show_notification(self.stdscr, ['Not logged in']) return @@ -437,6 +426,7 @@ class BasePage(object): """ Checks the inbox for unread messages and displays a notification. """ + inbox = len(list(self.reddit.get_unread(limit=1))) try: if inbox > 0: diff --git a/rtv/submission.py b/rtv/submission.py index 8731f0a..9044bd5 100644 --- a/rtv/submission.py +++ b/rtv/submission.py @@ -20,7 +20,7 @@ class SubmissionController(BaseController): class SubmissionPage(BasePage): - def __init__(self, stdscr, reddit, url=None, submission=None): + def __init__(self, stdscr, reddit, oauth, url=None, submission=None): self.controller = SubmissionController(self) self.loader = LoadScreen(stdscr) @@ -31,8 +31,8 @@ class SubmissionPage(BasePage): else: raise ValueError('Must specify url or submission') - super(SubmissionPage, self).__init__(stdscr, reddit, - content, page_index=-1) + super(SubmissionPage, self).__init__(stdscr, reddit, content, oauth, + page_index=-1) def loop(self): "Main control loop" @@ -88,7 +88,7 @@ class SubmissionPage(BasePage): selected comment. """ - if not self.reddit.is_logged_in(): + if not self.reddit.is_oauth_session(): show_notification(self.stdscr, ['Not logged in']) return diff --git a/rtv/subreddit.py b/rtv/subreddit.py index a869a3b..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 @@ -33,13 +33,14 @@ class SubredditController(BaseController): class SubredditPage(BasePage): - def __init__(self, stdscr, reddit, name): + def __init__(self, stdscr, reddit, oauth, name): self.controller = SubredditController(self) self.loader = LoadScreen(stdscr) + self.oauth = oauth content = SubredditContent.from_name(reddit, name, self.loader) - super(SubredditPage, self).__init__(stdscr, reddit, content) + super(SubredditPage, self).__init__(stdscr, reddit, content, oauth) def loop(self): "Main control loop" @@ -104,7 +105,7 @@ class SubredditPage(BasePage): "Select the current submission to view posts" data = self.content.get(self.nav.absolute_index) - page = SubmissionPage(self.stdscr, self.reddit, url=data['permalink']) + page = SubmissionPage(self.stdscr, self.reddit, self.oauth, url=data['permalink']) page.loop() if data['url_type'] == 'selfpost': global history @@ -119,7 +120,7 @@ class SubredditPage(BasePage): global history history.add(url) if data['url_type'] in ['x-post', 'selfpost']: - page = SubmissionPage(self.stdscr, self.reddit, url=url) + page = SubmissionPage(self.stdscr, self.reddit, self.oauth, url=url) page.loop() else: open_browser(url) @@ -128,7 +129,7 @@ class SubredditPage(BasePage): def post_submission(self): "Post a new submission to the given subreddit" - if not self.reddit.is_logged_in(): + if not self.reddit.is_oauth_session(): show_notification(self.stdscr, ['Not logged in']) return @@ -161,7 +162,7 @@ class SubredditPage(BasePage): time.sleep(2.0) # Open the newly created post s.catch = False - page = SubmissionPage(self.stdscr, self.reddit, submission=post) + page = SubmissionPage(self.stdscr, self.reddit, self.oauth, submission=post) page.loop() self.refresh_content() @@ -169,12 +170,12 @@ class SubredditPage(BasePage): def open_subscriptions(self): "Open user subscriptions page" - if not self.reddit.is_logged_in(): + if not self.reddit.is_oauth_session(): show_notification(self.stdscr, ['Not logged in']) return # Open subscriptions page - page = SubscriptionPage(self.stdscr, self.reddit) + page = SubscriptionPage(self.stdscr, self.reddit, self.oauth) page.loop() # When user has chosen a subreddit in the subscriptions list, diff --git a/rtv/subscriptions.py b/rtv/subscription.py similarity index 92% rename from rtv/subscriptions.py rename to rtv/subscription.py index 8ef2094..bf7b97c 100644 --- a/rtv/subscriptions.py +++ b/rtv/subscription.py @@ -15,14 +15,14 @@ class SubscriptionController(BaseController): class SubscriptionPage(BasePage): - def __init__(self, stdscr, reddit): + def __init__(self, stdscr, reddit, oauth): self.controller = SubscriptionController(self) self.loader = LoadScreen(stdscr) self.selected_subreddit_data = None content = SubscriptionContent.from_user(reddit, self.loader) - super(SubscriptionPage, self).__init__(stdscr, reddit, content) + super(SubscriptionPage, self).__init__(stdscr, reddit, content, oauth) def loop(self): "Main control loop" @@ -37,7 +37,7 @@ class SubscriptionPage(BasePage): def refresh_content(self): "Re-download all subscriptions and reset the page index" - self.content = SubscriptionContent.get_list(self.reddit, self.loader) + self.content = SubscriptionContent.from_user(self.reddit, self.loader) self.nav = Navigator(self.content.get) @SubscriptionController.register(curses.KEY_ENTER, 10, curses.KEY_RIGHT) @@ -70,4 +70,4 @@ class SubscriptionPage(BasePage): row = offset + 1 for row, text in enumerate(data['split_title'], start=row): if row in valid_rows: - add_line(win, text, row, 1) \ No newline at end of file + add_line(win, text, row, 1) diff --git a/setup.py b/setup.py index f260e6e..dd14eb4 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,8 +21,12 @@ setup( keywords='reddit terminal praw curses', packages=['rtv'], include_package_data=True, +<<<<<<< HEAD data_files=[("share/man/man1", ["rtv.1"])], install_requires=['praw>=3.1.0', 'six', 'requests', 'kitchen'], +======= + install_requires=requirements, +>>>>>>> 28d17b28d0840f75386586686897e9316378150e entry_points={'console_scripts': ['rtv=rtv.__main__:main']}, classifiers=[ 'Intended Audience :: End Users/Desktop', diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..6a17b5f --- /dev/null +++ b/templates/index.html @@ -0,0 +1,43 @@ + + + + RTV OAuth2 Helper + + + + + {% if error == 'access_denied' %} +

Access Denied


+

Reddit Terminal Viewer was denied access and will continue to operate in unauthenticated mode, you can close this window. + {% elif error != 'placeholder' %} +

Error : {{ error }}

+ {% elif (state == 'placeholder' or code == 'placeholder') %} +

Wait...


+

This page is supposed to be a Reddit OAuth callback. You can't just come here hands in your pocket!

+ {% else %} +

Access Granted


+

Reddit Terminal Viewer will now log in, you can close this window.

+ {% end %} + + + +