This commit is contained in:
John ShaggyTwoDope Jenkins
2015-09-23 05:54:57 -07:00
13 changed files with 369 additions and 133 deletions

View File

@@ -1,3 +1,7 @@
include version.py include version.py
include CHANGELOG.rst CONTRIBUTORS.rst LICENSE include CHANGELOG.rst CONTRIBUTORS.rst LICENSE
<<<<<<< HEAD
include rtv.1 include rtv.1
=======
include rtv/templates/*
>>>>>>> 28d17b28d0840f75386586686897e9316378150e

View File

@@ -73,11 +73,8 @@ Basic Commands
Authenticated Commands Authenticated Commands
---------------------- ----------------------
Some actions require that you be logged in to your reddit account. To log in you can either: Some actions require that you be logged in to your reddit account.
You can log in by pressing ``u`` while inside of the program.
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
Once you are logged in your username will appear in the top-right corner of the screen. Once you are logged in your username will appear in the top-right corner of the screen.
:``a``/``z``: Upvote/downvote :``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 $ 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 [#]_.
.. [#] `<https://github.com/reddit/reddit/wiki/OAuth2>`_
.. [#] `<https://www.reddit.com/r/redditdev/comments/2ujhkr/important_api_licensing_terms_clarified/>`_
----------- -----------
Config File 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. 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. 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 .. code-block:: ini
[rtv] [rtv]
username=MyUsername
password=MySecretPassword
# Log file location # Log file location
log=/tmp/rtv.log log=/tmp/rtv.log
@@ -176,6 +188,24 @@ Example config:
# This may be necessary for compatibility with some terminal browsers # This may be necessary for compatibility with some terminal browsers
# ascii=True # 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 Changelog

View File

@@ -7,48 +7,25 @@ import logging
import requests import requests
import praw import praw
import praw.errors import praw.errors
from six.moves import configparser import tornado
from . import config from . import config
from .exceptions import SubmissionError, SubredditError, SubscriptionError, ProgramError 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 .submission import SubmissionPage
from .subreddit import SubredditPage from .subreddit import SubredditPage
from .docs import * from .docs import *
from .oauth import OAuthTool
from .__version__ import __version__ from .__version__ import __version__
__all__ = [] __all__ = []
def load_config(): # Pycharm debugging note:
""" # You can use pycharm to debug a curses application by launching rtv in a
Search for a configuration file at the location ~/.rtv and attempt to load # console window (python -m rtv) and using pycharm to attach to the remote
saved settings for things like the username and password. # 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
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
def command_line(): def command_line():
@@ -57,71 +34,68 @@ def command_line():
epilog=CONTROLS + HELP, epilog=CONTROLS + HELP,
formatter_class=argparse.RawDescriptionHelpFormatter) formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('-s', dest='subreddit', help='subreddit name') 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 link to a submission') 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', parser.add_argument('--ascii', action='store_true', help='enable ascii-only mode')
help='enable ascii-only mode') parser.add_argument('--log', metavar='FILE', action='store', help='log HTTP requests to a file')
parser.add_argument('--log', metavar='FILE', action='store', parser.add_argument('--non-persistent', dest='persistent', action='store_false', help='Forget all authenticated users when the program exits')
help='Log HTTP requests') parser.add_argument('--clear-auth', dest='clear_auth', action='store_true', help='Remove any saved OAuth tokens before starting')
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')
args = parser.parse_args() args = parser.parse_args()
return args return args
def main(): def main():
"Main entry point" "Main entry point"
# logging.basicConfig(level=logging.DEBUG, filename='rtv.log') # Squelch SSL warnings
logging.captureWarnings(True)
locale.setlocale(locale.LC_ALL, '') locale.setlocale(locale.LC_ALL, '')
args = command_line() # Set the terminal title
local_config = load_config()
# set the terminal title
title = 'rtv {0}'.format(__version__) title = 'rtv {0}'.format(__version__)
if os.name == 'nt': if os.name == 'nt':
os.system('title {0}'.format(title)) os.system('title {0}'.format(title))
else: else:
sys.stdout.write("\x1b]2;{0}\x07".format(title)) 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. # 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(): for key, val in local_config.items():
if getattr(args, key, None) is None: if getattr(args, key, None) is None:
setattr(args, key, val) setattr(args, key, val)
config.unicode = (not args.ascii) if args.ascii:
config.unicode = False
# Squelch SSL warnings for Ubuntu if not args.persistent:
logging.captureWarnings(True) config.persistent = False
if args.log: if args.log:
logging.basicConfig(level=logging.DEBUG, filename=args.log) logging.basicConfig(level=logging.DEBUG, filename=args.log)
if args.clear_auth:
config.clear_refresh_token()
try: try:
print('Connecting...') print('Connecting...')
reddit = praw.Reddit(user_agent=AGENT) reddit = praw.Reddit(user_agent=AGENT)
reddit.config.decode_html_entities = False 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: with curses_session() as stdscr:
oauth = OAuthTool(reddit, stdscr, LoadScreen(stdscr))
if oauth.refresh_token:
oauth.authorize()
if args.link: if args.link:
page = SubmissionPage(stdscr, reddit, url=args.link) page = SubmissionPage(stdscr, reddit, oauth, url=args.link)
page.loop() page.loop()
subreddit = args.subreddit or 'front' subreddit = args.subreddit or 'front'
page = SubredditPage(stdscr, reddit, subreddit) page = SubredditPage(stdscr, reddit, oauth, subreddit)
page.loop() page.loop()
except praw.errors.InvalidUserPass: except (praw.errors.OAuthAppRequired, praw.errors.OAuthInvalidToken):
print('Invalid password for username: {}'.format(args.username)) print('Invalid OAuth data')
except requests.ConnectionError: except praw.errors.NotFound:
print('Connection timeout')
except requests.HTTPError:
print('HTTP Error: 404 Not Found') print('HTTP Error: 404 Not Found')
except praw.errors.HTTPException:
print('Connection timeout')
except SubmissionError as e: except SubmissionError as e:
print('Could not reach submission URL: {}'.format(e.url)) print('Could not reach submission URL: {}'.format(e.url))
except SubredditError as e: except SubredditError as e:
@@ -134,5 +108,7 @@ def main():
finally: finally:
# Ensure sockets are closed to prevent a ResourceWarning # Ensure sockets are closed to prevent a ResourceWarning
reddit.handler.http.close() reddit.handler.http.close()
# Explicitly close file descriptors opened by Tornado's IOLoop
tornado.ioloop.IOLoop.current().close(all_fds=True)
sys.exit(main()) sys.exit(main())

View File

@@ -1,5 +1,60 @@
""" """
Global configuration settings Global configuration settings
""" """
import os
from six.moves import configparser
unicode = True 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)

View File

@@ -1,6 +1,6 @@
from .__version__ import __version__ from .__version__ import __version__
__all__ = ['AGENT', 'SUMMARY', 'AUTH', 'CONTROLS', 'HELP', 'COMMENT_FILE', __all__ = ['AGENT', 'SUMMARY', 'CONTROLS', 'HELP', 'COMMENT_FILE',
'SUBMISSION_FILE', 'COMMENT_EDIT_FILE'] 'SUBMISSION_FILE', 'COMMENT_EDIT_FILE']
AGENT = """\ AGENT = """\
@@ -12,11 +12,6 @@ Reddit Terminal Viewer is a lightweight browser for www.reddit.com built into a
terminal window. 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 = """
Controls Controls
-------- --------

View File

@@ -14,7 +14,8 @@ from . import config
from .exceptions import ProgramError from .exceptions import ProgramError
__all__ = ['open_browser', 'clean', 'wrap_text', 'strip_textpad', __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): def clean(string, n_cols=None):
@@ -102,21 +103,7 @@ def open_browser(url):
are not detected here. are not detected here.
""" """
console_browsers = ['www-browser', 'links', 'links2', 'elinks', 'lynx', 'w3m'] if check_browser_display():
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:
command = "import webbrowser; webbrowser.open_new_tab('%s')" % url command = "import webbrowser; webbrowser.open_new_tab('%s')" % url
args = [sys.executable, '-c', command] args = [sys.executable, '-c', command]
with open(os.devnull, 'ab+', 0) as null: with open(os.devnull, 'ab+', 0) as null:
@@ -127,6 +114,28 @@ def open_browser(url):
curses.doupdate() 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): def wrap_text(text, width):
""" """
Wrap text paragraphs to the given character width while preserving newlines. Wrap text paragraphs to the given character width while preserving newlines.

121
rtv/oauth.py Normal file
View File

@@ -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()

View File

@@ -238,17 +238,18 @@ class BaseController(object):
class BasePage(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_HEIGHT = 10
MIN_WIDTH = 20 MIN_WIDTH = 20
def __init__(self, stdscr, reddit, content, **kwargs): def __init__(self, stdscr, reddit, content, oauth, **kwargs):
self.stdscr = stdscr self.stdscr = stdscr
self.reddit = reddit self.reddit = reddit
self.content = content self.content = content
self.oauth = oauth
self.nav = Navigator(self.content.get, **kwargs) self.nav = Navigator(self.content.get, **kwargs)
self._header_window = None self._header_window = None
@@ -348,23 +349,11 @@ class BasePage(object):
account. account.
""" """
if self.reddit.is_logged_in(): if self.reddit.is_oauth_session():
self.logout() self.oauth.clear_oauth_data()
return show_notification(self.stdscr, ['Logged out'])
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'])
else: else:
show_notification(self.stdscr, ['Welcome {}'.format(username)]) self.oauth.authorize()
@BaseController.register('d') @BaseController.register('d')
def delete(self): def delete(self):
@@ -372,7 +361,7 @@ class BasePage(object):
Delete a submission or comment. 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']) show_notification(self.stdscr, ['Not logged in'])
return return
@@ -400,7 +389,7 @@ class BasePage(object):
Edit a submission or comment. 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']) show_notification(self.stdscr, ['Not logged in'])
return return
@@ -437,6 +426,7 @@ class BasePage(object):
""" """
Checks the inbox for unread messages and displays a notification. Checks the inbox for unread messages and displays a notification.
""" """
inbox = len(list(self.reddit.get_unread(limit=1))) inbox = len(list(self.reddit.get_unread(limit=1)))
try: try:
if inbox > 0: if inbox > 0:

View File

@@ -20,7 +20,7 @@ class SubmissionController(BaseController):
class SubmissionPage(BasePage): 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.controller = SubmissionController(self)
self.loader = LoadScreen(stdscr) self.loader = LoadScreen(stdscr)
@@ -31,8 +31,8 @@ class SubmissionPage(BasePage):
else: else:
raise ValueError('Must specify url or submission') raise ValueError('Must specify url or submission')
super(SubmissionPage, self).__init__(stdscr, reddit, super(SubmissionPage, self).__init__(stdscr, reddit, content, oauth,
content, page_index=-1) page_index=-1)
def loop(self): def loop(self):
"Main control loop" "Main control loop"
@@ -88,7 +88,7 @@ class SubmissionPage(BasePage):
selected comment. selected comment.
""" """
if not self.reddit.is_logged_in(): if not self.reddit.is_oauth_session():
show_notification(self.stdscr, ['Not logged in']) show_notification(self.stdscr, ['Not logged in'])
return return

View File

@@ -8,7 +8,7 @@ import requests
from .exceptions import SubredditError, AccountError from .exceptions import SubredditError, AccountError
from .page import BasePage, Navigator, BaseController from .page import BasePage, Navigator, BaseController
from .submission import SubmissionPage from .submission import SubmissionPage
from .subscriptions import SubscriptionPage from .subscription import SubscriptionPage
from .content import SubredditContent from .content import SubredditContent
from .helpers import open_browser, open_editor, strip_subreddit_url from .helpers import open_browser, open_editor, strip_subreddit_url
from .docs import SUBMISSION_FILE from .docs import SUBMISSION_FILE
@@ -33,13 +33,14 @@ class SubredditController(BaseController):
class SubredditPage(BasePage): class SubredditPage(BasePage):
def __init__(self, stdscr, reddit, name): def __init__(self, stdscr, reddit, oauth, name):
self.controller = SubredditController(self) self.controller = SubredditController(self)
self.loader = LoadScreen(stdscr) self.loader = LoadScreen(stdscr)
self.oauth = oauth
content = SubredditContent.from_name(reddit, name, self.loader) 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): def loop(self):
"Main control loop" "Main control loop"
@@ -104,7 +105,7 @@ class SubredditPage(BasePage):
"Select the current submission to view posts" "Select the current submission to view posts"
data = self.content.get(self.nav.absolute_index) 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() page.loop()
if data['url_type'] == 'selfpost': if data['url_type'] == 'selfpost':
global history global history
@@ -119,7 +120,7 @@ class SubredditPage(BasePage):
global history global history
history.add(url) history.add(url)
if data['url_type'] in ['x-post', 'selfpost']: 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() page.loop()
else: else:
open_browser(url) open_browser(url)
@@ -128,7 +129,7 @@ class SubredditPage(BasePage):
def post_submission(self): def post_submission(self):
"Post a new submission to the given subreddit" "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']) show_notification(self.stdscr, ['Not logged in'])
return return
@@ -161,7 +162,7 @@ class SubredditPage(BasePage):
time.sleep(2.0) time.sleep(2.0)
# Open the newly created post # Open the newly created post
s.catch = False s.catch = False
page = SubmissionPage(self.stdscr, self.reddit, submission=post) page = SubmissionPage(self.stdscr, self.reddit, self.oauth, submission=post)
page.loop() page.loop()
self.refresh_content() self.refresh_content()
@@ -169,12 +170,12 @@ class SubredditPage(BasePage):
def open_subscriptions(self): def open_subscriptions(self):
"Open user subscriptions page" "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']) show_notification(self.stdscr, ['Not logged in'])
return return
# Open subscriptions page # Open subscriptions page
page = SubscriptionPage(self.stdscr, self.reddit) page = SubscriptionPage(self.stdscr, self.reddit, self.oauth)
page.loop() page.loop()
# When user has chosen a subreddit in the subscriptions list, # When user has chosen a subreddit in the subscriptions list,

View File

@@ -15,14 +15,14 @@ class SubscriptionController(BaseController):
class SubscriptionPage(BasePage): class SubscriptionPage(BasePage):
def __init__(self, stdscr, reddit): def __init__(self, stdscr, reddit, oauth):
self.controller = SubscriptionController(self) self.controller = SubscriptionController(self)
self.loader = LoadScreen(stdscr) self.loader = LoadScreen(stdscr)
self.selected_subreddit_data = None self.selected_subreddit_data = None
content = SubscriptionContent.from_user(reddit, self.loader) 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): def loop(self):
"Main control loop" "Main control loop"
@@ -37,7 +37,7 @@ class SubscriptionPage(BasePage):
def refresh_content(self): def refresh_content(self):
"Re-download all subscriptions and reset the page index" "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) self.nav = Navigator(self.content.get)
@SubscriptionController.register(curses.KEY_ENTER, 10, curses.KEY_RIGHT) @SubscriptionController.register(curses.KEY_ENTER, 10, curses.KEY_RIGHT)
@@ -70,4 +70,4 @@ class SubscriptionPage(BasePage):
row = offset + 1 row = offset + 1
for row, text in enumerate(data['split_title'], start=row): for row, text in enumerate(data['split_title'], start=row):
if row in valid_rows: if row in valid_rows:
add_line(win, text, row, 1) add_line(win, text, row, 1)

View File

@@ -1,6 +1,14 @@
from setuptools import setup from setuptools import setup
from version import __version__ as version 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( setup(
name='rtv', name='rtv',
version=version, version=version,
@@ -13,8 +21,12 @@ setup(
keywords='reddit terminal praw curses', keywords='reddit terminal praw curses',
packages=['rtv'], packages=['rtv'],
include_package_data=True, include_package_data=True,
<<<<<<< HEAD
data_files=[("share/man/man1", ["rtv.1"])], data_files=[("share/man/man1", ["rtv.1"])],
install_requires=['praw>=3.1.0', 'six', 'requests', 'kitchen'], install_requires=['praw>=3.1.0', 'six', 'requests', 'kitchen'],
=======
install_requires=requirements,
>>>>>>> 28d17b28d0840f75386586686897e9316378150e
entry_points={'console_scripts': ['rtv=rtv.__main__:main']}, entry_points={'console_scripts': ['rtv=rtv.__main__:main']},
classifiers=[ classifiers=[
'Intended Audience :: End Users/Desktop', 'Intended Audience :: End Users/Desktop',

43
templates/index.html Normal file
View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head>
<title>RTV OAuth2 Helper</title>
<!-- style borrowed from http://bettermotherfuckingwebsite.com/ -->
<style type="text/css">
body {
margin:40px auto;
max-width:650px;
line-height:1.6;
font-size:18px;
font-family:Arial, Helvetica, sans-serif;
color:#444;
padding:0 10px;
}
h1, h2, h3 {
line-height:1.2
}
#footer {
position: absolute;
bottom: 0px;
width: 100%;
font-size:14px;
}
</style>
</head>
<body>
{% if error == 'access_denied' %}
<h1 style="color: red">Access Denied</h1><hr>
<p><span style="font-weight: bold">Reddit Terminal Viewer</span> was denied access and will continue to operate in unauthenticated mode, you can close this window.
{% elif error != 'placeholder' %}
<h1 style="color: red">Error : {{ error }}</h1>
{% elif (state == 'placeholder' or code == 'placeholder') %}
<h1>Wait...</h1><hr>
<p>This page is supposed to be a Reddit OAuth callback. You can't just come here hands in your pocket!</p>
{% else %}
<h1 style="color: green">Access Granted</h1><hr>
<p><span style="font-weight: bold">Reddit Terminal Viewer</span> will now log in, you can close this window.</p>
{% end %}
<div id="footer">View the <a href="http://www.github.com/michael-lazar/rtv">Documentation</a></div>
</body>
</html>