Merge branch 'TheoPib-master'

This commit is contained in:
Michael Lazar
2015-09-20 23:35:34 -07:00
13 changed files with 363 additions and 134 deletions

View File

@@ -1,2 +1,3 @@
include version.py
include CHANGELOG.rst CONTRIBUTORS.rst LICENSE
include rtv/templates/*

View File

@@ -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 [#]_.
.. [#] `<https://github.com/reddit/reddit/wiki/OAuth2>`_
.. [#] `<https://www.reddit.com/r/redditdev/comments/2ujhkr/important_api_licensing_terms_clarified/>`_
-----------
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

View File

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

View File

@@ -1,5 +1,60 @@
"""
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__
__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
--------

View File

@@ -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.

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):
"""
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:

View File

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

View File

@@ -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,

View File

@@ -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)
add_line(win, text, row, 1)

View File

@@ -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=['praw>=3.1.0', 'six', 'requests', 'kitchen'],
install_requires=requirements,
entry_points={'console_scripts': ['rtv=rtv.__main__:main']},
classifiers=[
'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>