nyaa/nyaa/api_handler.py

189 lines
6.3 KiB
Python
Raw Normal View History

2017-05-12 18:51:49 +00:00
import flask
2017-05-18 03:56:36 +00:00
from werkzeug.datastructures import ImmutableMultiDict, CombinedMultiDict
2017-05-12 18:51:49 +00:00
from nyaa import app, db
from nyaa import models, forms
2017-05-18 03:56:36 +00:00
from nyaa import bencode, backend, utils
2017-05-12 18:51:49 +00:00
from nyaa import torrents
2017-05-18 12:30:03 +00:00
import functools
2017-05-12 18:51:49 +00:00
import json
import os.path
2017-05-18 12:30:03 +00:00
api_blueprint = flask.Blueprint('api', __name__)
2017-05-12 18:51:49 +00:00
2017-05-18 12:30:03 +00:00
# #################################### API HELPERS ####################################
2017-05-19 12:36:33 +00:00
2017-05-18 12:30:03 +00:00
def basic_auth_user(f):
''' A decorator that will try to validate the user into g.user from basic auth.
Note: this does not set user to None on failure, so users can also authorize
themselves with the cookie (handled in routes.before_request). '''
@functools.wraps(f)
def decorator(*args, **kwargs):
auth = flask.request.authorization
if auth:
user = models.User.by_username_or_email(auth.get('username'))
if user and user.validate_authorization(auth.get('password')):
flask.g.user = user
return f(*args, **kwargs)
return decorator
2017-05-19 12:36:33 +00:00
2017-05-18 12:30:03 +00:00
def api_require_user(f):
''' Returns an error message if flask.g.user is None.
Remember to put after basic_auth_user. '''
@functools.wraps(f)
def decorator(*args, **kwargs):
if flask.g.user is None:
2017-05-19 12:36:33 +00:00
return flask.jsonify({'errors': ['Bad authorization']}), 403
2017-05-18 12:30:03 +00:00
return f(*args, **kwargs)
return decorator
2017-05-12 18:51:49 +00:00
2017-05-19 12:36:33 +00:00
2017-05-18 03:56:36 +00:00
def validate_user(upload_request):
auth_info = None
2017-05-12 18:51:49 +00:00
try:
2017-05-18 03:56:36 +00:00
if 'auth_info' in upload_request.files:
auth_info = json.loads(upload_request.files['auth_info'].read().decode('utf-8'))
if 'username' not in auth_info.keys() or 'password' not in auth_info.keys():
return False, None, None
2017-05-12 18:51:49 +00:00
2017-05-18 03:56:36 +00:00
username = auth_info['username']
password = auth_info['password']
2017-05-12 18:51:49 +00:00
user = models.User.by_username(username)
if not user:
user = models.User.by_email(username)
2017-05-19 12:36:33 +00:00
if not user or password != user.password_hash or \
user.status == models.UserStatusType.INACTIVE:
2017-05-18 03:56:36 +00:00
return False, None, None
2017-05-12 18:51:49 +00:00
2017-05-18 03:56:36 +00:00
return True, user, None
2017-05-18 04:31:28 +00:00
else:
return False, None, None
2017-05-12 18:51:49 +00:00
2017-05-18 03:56:36 +00:00
except Exception as e:
return False, None, e
2017-05-12 18:51:49 +00:00
2017-05-18 03:56:36 +00:00
def _create_upload_category_choices():
''' Turns categories in the database into a list of (id, name)s '''
choices = [('', '[Select a category]')]
for main_cat in models.MainCategory.query.order_by(models.MainCategory.id):
choices.append((main_cat.id_as_string, main_cat.name, True))
for sub_cat in main_cat.sub_categories:
choices.append((sub_cat.id_as_string, ' - ' + sub_cat.name))
return choices
2017-05-12 18:51:49 +00:00
2017-05-18 03:56:36 +00:00
# #################################### API ROUTES ####################################
def api_upload(upload_request, user):
form_info = None
try:
form_info = json.loads(upload_request.files['torrent_info'].read().decode('utf-8'))
2017-05-12 18:51:49 +00:00
2017-05-18 03:56:36 +00:00
form_info_as_dict = []
for k, v in form_info.items():
if k in ['is_anonymous', 'is_hidden', 'is_remake', 'is_complete']:
2017-05-19 12:36:33 +00:00
if v:
2017-05-18 03:56:36 +00:00
form_info_as_dict.append((k, v))
2017-05-12 18:51:49 +00:00
else:
2017-05-18 03:56:36 +00:00
form_info_as_dict.append((k, v))
form_info = ImmutableMultiDict(form_info_as_dict)
except Exception as e:
2017-05-19 12:36:33 +00:00
return flask.make_response(flask.jsonify(
{'Failure': ['Invalid data. See HELP in api_uploader.py']}), 400)
2017-05-12 18:51:49 +00:00
2017-05-18 03:56:36 +00:00
try:
torrent_file = upload_request.files['torrent_file']
torrent_file = ImmutableMultiDict([('torrent_file', torrent_file)])
except Exception as e:
2017-05-19 12:36:33 +00:00
return flask.make_response(flask.jsonify(
{'Failure': ['No torrent file was attached.']}), 400)
2017-05-12 18:51:49 +00:00
2017-05-18 03:56:36 +00:00
form = forms.UploadForm(CombinedMultiDict((torrent_file, form_info)))
form.category.choices = _create_upload_category_choices()
2017-05-12 18:51:49 +00:00
2017-05-18 03:56:36 +00:00
if upload_request.method == 'POST' and form.validate():
torrent = backend.handle_torrent_upload(form, user, True)
2017-05-12 18:51:49 +00:00
2017-05-18 07:27:27 +00:00
return flask.make_response(flask.jsonify({'Success': int('{0}'.format(torrent.id))}), 200)
2017-05-12 18:51:49 +00:00
else:
2017-05-18 03:56:36 +00:00
return_error_messages = []
for error_name, error_messages in form.errors.items():
return_error_messages.extend(error_messages)
2017-05-18 07:27:27 +00:00
return flask.make_response(flask.jsonify({'Failure': return_error_messages}), 400)
2017-05-18 12:30:03 +00:00
# V2 below
2017-05-19 12:36:33 +00:00
2017-05-18 12:30:03 +00:00
# Map UploadForm fields to API keys
UPLOAD_API_FORM_KEYMAP = {
2017-05-19 12:36:33 +00:00
'torrent_file': 'torrent',
2017-05-18 12:30:03 +00:00
2017-05-19 12:36:33 +00:00
'display_name': 'name',
2017-05-18 12:30:03 +00:00
2017-05-19 12:36:33 +00:00
'is_anonymous': 'anonymous',
'is_hidden': 'hidden',
'is_complete': 'complete',
'is_remake': 'remake',
'is_trusted': 'trusted'
2017-05-18 12:30:03 +00:00
}
2017-05-19 12:36:33 +00:00
UPLOAD_API_FORM_KEYMAP_REVERSE = {v: k for k, v in UPLOAD_API_FORM_KEYMAP.items()}
2017-05-18 12:30:03 +00:00
UPLOAD_API_KEYS = [
'name',
'category',
'anonymous',
'hidden',
'complete',
'remake',
'trusted',
2017-05-18 12:30:03 +00:00
'information',
'description'
]
2017-05-19 12:36:33 +00:00
2017-05-18 12:30:03 +00:00
@api_blueprint.route('/v2/upload', methods=['POST'])
@basic_auth_user
@api_require_user
def v2_api_upload():
mapped_dict = {
2017-05-19 12:36:33 +00:00
'torrent_file': flask.request.files.get('torrent')
2017-05-18 12:30:03 +00:00
}
request_data_field = flask.request.form.get('torrent_data')
if request_data_field is None:
2017-05-19 12:36:33 +00:00
return flask.jsonify({'errors': ['missing torrent_data field']}), 400
2017-05-18 12:30:03 +00:00
request_data = json.loads(request_data_field)
# Map api keys to upload form fields
for key in UPLOAD_API_KEYS:
mapped_key = UPLOAD_API_FORM_KEYMAP_REVERSE.get(key, key)
mapped_dict[mapped_key] = request_data.get(key) or ''
2017-05-18 12:30:03 +00:00
# Flask-WTF (very helpfully!!) automatically grabs the request form, so force a None formdata
upload_form = forms.UploadForm(None, data=mapped_dict)
upload_form.category.choices = _create_upload_category_choices()
if upload_form.validate():
torrent = backend.handle_torrent_upload(upload_form, flask.g.user)
# Create a response dict with relevant data
torrent_metadata = {
2017-05-19 12:36:33 +00:00
'url': flask.url_for('view_torrent', torrent_id=torrent.id, _external=True),
'id': torrent.id,
'name': torrent.display_name,
'hash': torrent.info_hash.hex(),
'magnet': torrent.magnet_uri
2017-05-18 12:30:03 +00:00
}
return flask.jsonify(torrent_metadata)
else:
# Map errors back from form fields into the api keys
2017-05-19 12:36:33 +00:00
mapped_errors = {UPLOAD_API_FORM_KEYMAP.get(k, k): v for k, v in upload_form.errors.items()}
return flask.jsonify({'errors': mapped_errors}), 400