Tweaking a few things oauth things.
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
include version.py
|
||||
include CHANGELOG.rst CONTRIBUTORS.rst LICENSE
|
||||
include rtv/templates/*.html
|
||||
include rtv/templates/*
|
||||
|
||||
135
rtv/__main__.py
135
rtv/__main__.py
@@ -7,7 +7,7 @@ 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
|
||||
@@ -18,70 +18,14 @@ from .docs import *
|
||||
from .oauth import OAuthTool
|
||||
from .__version__ import __version__
|
||||
|
||||
from tornado import ioloop
|
||||
|
||||
__all__ = []
|
||||
|
||||
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'))
|
||||
|
||||
config_paths = [
|
||||
os.path.join(XDG_CONFIG_HOME, 'rtv', 'rtv.cfg'),
|
||||
os.path.join(HOME, '.rtv')
|
||||
]
|
||||
|
||||
# get 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 load_oauth_config():
|
||||
"""
|
||||
Attempt to load saved OAuth settings
|
||||
"""
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
HOME = os.path.expanduser('~')
|
||||
XDG_CONFIG_HOME = os.getenv('XDG_CONFIG_HOME',
|
||||
os.path.join(HOME, '.config'))
|
||||
|
||||
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')
|
||||
|
||||
config.read(config_path)
|
||||
|
||||
if config.has_section('oauth'):
|
||||
defaults = dict(config.items('oauth'))
|
||||
else:
|
||||
# Populate OAuth section
|
||||
config.add_section('oauth')
|
||||
config.set('oauth', 'auto_login', 'false')
|
||||
with open(config_path, 'w') as cfg:
|
||||
config.write(cfg)
|
||||
defaults = dict(config.items('oauth'))
|
||||
|
||||
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():
|
||||
|
||||
@@ -90,59 +34,46 @@ 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')
|
||||
|
||||
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('--refresh-token', dest='refresh_token', help='OAuth refresh token')
|
||||
|
||||
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('--refresh-token', dest='refresh_token', help='OAuth refresh token')
|
||||
parser.add_argument('--clear-session', dest='clear_session', 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_rtv_config = load_rtv_config()
|
||||
local_oauth_config = load_oauth_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.
|
||||
for key, val in local_rtv_config.items():
|
||||
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)
|
||||
|
||||
for k, v in local_oauth_config.items():
|
||||
if getattr(args, k, None) is None:
|
||||
setattr(args, k, v)
|
||||
|
||||
config.unicode = (not args.ascii)
|
||||
|
||||
# Squelch SSL warnings for Ubuntu
|
||||
logging.captureWarnings(True)
|
||||
if args.ascii:
|
||||
config.unicode = False
|
||||
if args.log:
|
||||
logging.basicConfig(level=logging.DEBUG, filename=args.log)
|
||||
if args.clear_session:
|
||||
config.clear_refresh_token()
|
||||
if args.refresh_token:
|
||||
config.save_refresh_token(args.refresh_token)
|
||||
|
||||
try:
|
||||
print('Connecting...')
|
||||
@@ -150,21 +81,21 @@ 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 oauth.refresh_token:
|
||||
oauth.authorize()
|
||||
|
||||
if args.link:
|
||||
page = SubmissionPage(stdscr, reddit, oauth, url=args.link)
|
||||
page.loop()
|
||||
subreddit = args.subreddit or 'front'
|
||||
page = SubredditPage(stdscr, reddit, oauth, subreddit)
|
||||
page.loop()
|
||||
except (praw.errors.OAuthAppRequired, praw.errors.OAuthInvalidToken,
|
||||
praw.errors.HTTPException) as e:
|
||||
except (praw.errors.OAuthAppRequired, praw.errors.OAuthInvalidToken):
|
||||
print('Invalid OAuth data')
|
||||
except requests.ConnectionError:
|
||||
print('Connection timeout')
|
||||
except requests.HTTPError:
|
||||
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:
|
||||
@@ -178,6 +109,6 @@ def main():
|
||||
# Ensure sockets are closed to prevent a ResourceWarning
|
||||
reddit.handler.http.close()
|
||||
# Explicitly close file descriptors opened by Tornado's IOLoop
|
||||
ioloop.IOLoop.current().close(all_fds=True)
|
||||
tornado.ioloop.IOLoop.current().close(all_fds=True)
|
||||
|
||||
sys.exit(main())
|
||||
@@ -1,14 +1,58 @@
|
||||
"""
|
||||
Global configuration settings
|
||||
"""
|
||||
import os
|
||||
from six.moves import configparser
|
||||
|
||||
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
|
||||
|
||||
"""
|
||||
OAuth settings
|
||||
"""
|
||||
|
||||
oauth_client_id = 'nxoobnwO7mCP5A'
|
||||
# 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/auth'
|
||||
oauth_scope = 'edit-history-identity-mysubreddits-privatemessages-read-report-save-submit-subscribe-vote'
|
||||
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_session' in config_dict:
|
||||
config_dict['clear_session'] = config.getboolean('rtv', 'clear_session')
|
||||
if 'oauth_scope' in config_dict:
|
||||
config_dict['oauth_scope'] = config.oauth_scope.split('-')
|
||||
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)
|
||||
12
rtv/docs.py
12
rtv/docs.py
@@ -1,6 +1,6 @@
|
||||
from .__version__ import __version__
|
||||
|
||||
__all__ = ['AGENT', 'SUMMARY', 'AUTH', 'OAUTH', 'CONTROLS', 'HELP', 'COMMENT_FILE',
|
||||
__all__ = ['AGENT', 'SUMMARY', 'CONTROLS', 'HELP', 'COMMENT_FILE',
|
||||
'SUBMISSION_FILE', 'COMMENT_EDIT_FILE']
|
||||
|
||||
AGENT = """\
|
||||
@@ -12,16 +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.
|
||||
"""
|
||||
|
||||
OAUTH = """\
|
||||
Authentication is now done by OAuth, since PRAW will drop
|
||||
password authentication soon.
|
||||
"""
|
||||
|
||||
CONTROLS = """
|
||||
Controls
|
||||
--------
|
||||
|
||||
@@ -102,21 +102,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 +113,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.
|
||||
|
||||
235
rtv/oauth.py
235
rtv/oauth.py
@@ -1,185 +1,120 @@
|
||||
import curses
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
import webbrowser
|
||||
|
||||
import praw
|
||||
from six.moves import configparser
|
||||
|
||||
from . import config
|
||||
from .curses_helpers import show_notification, prompt_input
|
||||
|
||||
from tornado import gen, ioloop, web, httpserver
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
__all__ = ['token_validity', 'OAuthTool']
|
||||
_logger = logging.getLogger(__name__)
|
||||
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 HomeHandler(web.RequestHandler):
|
||||
|
||||
def get(self):
|
||||
self.render('home.html')
|
||||
|
||||
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
|
||||
global oauth_error
|
||||
global oauth_state, oauth_code, 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='placeholder')
|
||||
oauth_code = self.get_argument('code', default='placeholder')
|
||||
oauth_error = self.get_argument('error', default='placeholder')
|
||||
|
||||
self.render('auth.html', state=oauth_state, code=oauth_code, error=oauth_error)
|
||||
self.render('index.html', state=oauth_state, code=oauth_code, error=oauth_error)
|
||||
|
||||
# Stop IOLoop if using BackgroundBrowser (or GUI browser)
|
||||
if not self.compact:
|
||||
# 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,
|
||||
client_id=None, redirect_uri=None, scope=None):
|
||||
def __init__(self, reddit, stdscr=None, loader=None):
|
||||
|
||||
self.reddit = reddit
|
||||
self.stdscr = stdscr
|
||||
self.loader = loader
|
||||
|
||||
self.config = configparser.ConfigParser()
|
||||
self.config_fp = None
|
||||
|
||||
self.client_id = client_id or config.oauth_client_id
|
||||
# Comply with PRAW's desperate need for client secret
|
||||
self.client_secret = config.oauth_client_secret
|
||||
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
|
||||
self.callback_app = web.Application([
|
||||
(r'/', HomeHandler),
|
||||
(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',
|
||||
os.path.join(HOME, '.config'))
|
||||
self.refresh_token = config.load_refresh_token()
|
||||
|
||||
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')
|
||||
# Initialize Tornado webapp
|
||||
routes = [('/', AuthHandler)]
|
||||
self.callback_app = web.Application(routes, template_path='templates')
|
||||
|
||||
return file_path
|
||||
self.reddit.set_oauth_app_info(config.oauth_client_id,
|
||||
config.oauth_client_secret,
|
||||
config.oauth_redirect_uri)
|
||||
|
||||
def open_config(self, update=False):
|
||||
if self.config_fp is None:
|
||||
self.config_fp = self.get_config_fp()
|
||||
|
||||
if update:
|
||||
self.config.read(self.config_fp)
|
||||
|
||||
def save_config(self):
|
||||
self.open_config()
|
||||
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()
|
||||
|
||||
@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()
|
||||
# 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 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)
|
||||
|
||||
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'):
|
||||
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
|
||||
|
||||
permission_ask_page_link = self.reddit.get_authorize_url(str(hex_uuid),
|
||||
scope=self.scope, refreshable=True)
|
||||
|
||||
if self.compact:
|
||||
show_notification(self.stdscr, ['Opening ' + os.environ.get('BROWSER')])
|
||||
curses.endwin()
|
||||
ioloop.IOLoop.current().add_callback(self.open_terminal_browser, 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
|
||||
global oauth_error
|
||||
|
||||
self.final_state = oauth_state
|
||||
self.final_code = oauth_code
|
||||
self.final_error = oauth_error
|
||||
|
||||
# Check if access was denied
|
||||
if self.final_error == 'access_denied':
|
||||
show_notification(self.stdscr, ['Declined access'])
|
||||
return
|
||||
elif self.final_error != 'error_placeholder':
|
||||
show_notification(self.stdscr, ['Authentication error'])
|
||||
return
|
||||
|
||||
# Check if UUID matches obtained state
|
||||
# (if not, authorization process is compromised, and I'm giving up)
|
||||
if hex_uuid != self.final_state:
|
||||
show_notification(self.stdscr, ['UUID mismatch, stopping.'])
|
||||
return
|
||||
|
||||
try:
|
||||
with self.loader(message='Logging in'):
|
||||
# Get access information (tokens and scopes)
|
||||
self.access_info = self.reddit.get_access_information(self.final_code)
|
||||
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', 'refresh_token', self.access_info['refresh_token'])
|
||||
self.save_config()
|
||||
# Otherwise, fetch new access token
|
||||
else:
|
||||
# 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.config.get('oauth', 'refresh_token'))
|
||||
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)
|
||||
config.save_refresh_token(access_info['refresh_token'])
|
||||
self.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()
|
||||
|
||||
@@ -239,7 +239,7 @@ 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
|
||||
@@ -351,11 +351,10 @@ class BasePage(object):
|
||||
"""
|
||||
|
||||
if self.reddit.is_oauth_session():
|
||||
self.reddit.clear_authentication()
|
||||
self.oauth.clear_oauth_data()
|
||||
return
|
||||
|
||||
self.oauth.authorize()
|
||||
show_notification(self.stdscr, ['Logged out'])
|
||||
else:
|
||||
self.oauth.authorize()
|
||||
|
||||
@BaseController.register('d')
|
||||
def delete(self):
|
||||
|
||||
@@ -24,7 +24,6 @@ class SubmissionPage(BasePage):
|
||||
|
||||
self.controller = SubmissionController(self)
|
||||
self.loader = LoadScreen(stdscr)
|
||||
self.oauth = oauth
|
||||
if url:
|
||||
content = SubmissionContent.from_url(reddit, url, self.loader)
|
||||
elif submission:
|
||||
@@ -32,8 +31,8 @@ class SubmissionPage(BasePage):
|
||||
else:
|
||||
raise ValueError('Must specify url or submission')
|
||||
|
||||
super(SubmissionPage, self).__init__(stdscr, reddit,
|
||||
content, oauth, page_index=-1)
|
||||
super(SubmissionPage, self).__init__(stdscr, reddit, content, oauth,
|
||||
page_index=-1)
|
||||
|
||||
def loop(self):
|
||||
"Main control loop"
|
||||
|
||||
@@ -19,7 +19,6 @@ class SubscriptionPage(BasePage):
|
||||
|
||||
self.controller = SubscriptionController(self)
|
||||
self.loader = LoadScreen(stdscr)
|
||||
self.oauth = oauth
|
||||
self.selected_subreddit_data = None
|
||||
|
||||
content = SubscriptionContent.from_user(reddit, self.loader)
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<title>RTV OAuth</title>
|
||||
{% if error == 'access_denied' %}
|
||||
<h3 style="color: red">Declined rtv access</h3>
|
||||
<p>You chose to stop <span style="font-weight: bold">Reddit Terminal Viewer</span> from accessing your account, it will continue in unauthenticated mode.<br>
|
||||
You can close this page.</p>
|
||||
{% elif error != 'error_placeholder' %}
|
||||
<h3 style="color: red">Error : {{ error }}</h3>
|
||||
{% elif (state == 'state_placeholder' or code == 'code_placeholder') and error == 'error_placeholder' %}
|
||||
<h3>Wait...</h3>
|
||||
<p>This page is supposed to be a Reddit OAuth callback. You can't just come here hands in the pocket!</p>
|
||||
{% else %}
|
||||
<h3 style="color: green">Allowed rtv access</h3>
|
||||
<p><span style="font-weight: bold">Reddit Terminal Viewer</span> will now log in. You can close this page.</p>
|
||||
{% end %}
|
||||
@@ -1,3 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<title>OAuth helper</title>
|
||||
<h1>Reddit Terminal Viewer OAuth helper</h1>
|
||||
43
templates/index.html
Normal file
43
templates/index.html
Normal 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>
|
||||
Reference in New Issue
Block a user