import json from ipaddress import ip_address from urllib.parse import quote from markupsafe import Markup import flask from werkzeug.datastructures import CombinedMultiDict from sqlalchemy.orm import joinedload from nyaa import backend, forms, models, torrents from nyaa.extensions import db from nyaa.utils import cached_function app = flask.current_app bp = flask.Blueprint('torrents', __name__) @bp.route('/view/', endpoint='view', methods=['GET', 'POST']) def view_torrent(torrent_id): if flask.request.method == 'POST': torrent = models.Torrent.by_id(torrent_id) else: torrent = models.Torrent.query \ .options(joinedload(models.Torrent.filelist)) \ .filter_by(id=torrent_id) \ .first() if not torrent: flask.abort(404) # Only allow admins see deleted torrents if torrent.deleted and not (flask.g.user and flask.g.user.is_moderator): flask.abort(404) comment_form = None if flask.g.user and (not torrent.comment_locked or flask.g.user.is_moderator): comment_form = forms.CommentForm() if flask.request.method == 'POST': if not comment_form: flask.abort(403) if comment_form.validate(): comment_text = (comment_form.comment.data or '').strip() comment = models.Comment( torrent_id=torrent_id, user_id=flask.g.user.id, text=comment_text) db.session.add(comment) db.session.flush() torrent_count = torrent.update_comment_count() db.session.commit() flask.flash('Comment successfully posted.', 'success') return flask.redirect(flask.url_for('torrents.view', torrent_id=torrent_id, _anchor='com-' + str(torrent_count))) # Only allow owners and admins to edit torrents can_edit = flask.g.user and (flask.g.user is torrent.user or flask.g.user.is_moderator) files = None if torrent.filelist: files = json.loads(torrent.filelist.filelist_blob.decode('utf-8')) torrent_comments = models.Comment.query.filter_by( torrent_id=torrent_id ).order_by(models.Comment.id.asc()) report_form = forms.ReportForm() return flask.render_template('view.html', torrent=torrent, files=files, comment_form=comment_form, comments=torrent_comments, can_edit=can_edit, report_form=report_form) @bp.route('/view//edit', endpoint='edit', methods=['GET', 'POST']) def edit_torrent(torrent_id): torrent = models.Torrent.by_id(torrent_id) form = forms.EditForm(flask.request.form) form.category.choices = _create_upload_category_choices() delete_form = forms.DeleteForm() ban_form = None editor = flask.g.user if not torrent: flask.abort(404) # Only allow admins edit deleted torrents if torrent.deleted and not (editor and editor.is_moderator): flask.abort(404) # Only allow torrent owners or admins edit torrents if not editor or not (editor is torrent.user or editor.is_moderator): flask.abort(403) torrent_user_level = torrent.user and torrent.user.level if editor and editor.is_moderator and \ (torrent_user_level is None or editor.level > torrent_user_level): ban_form = forms.BanForm() if flask.request.method == 'POST' and form.submit.data and form.validate(): # Form has been sent, edit torrent with data. torrent.main_category_id, torrent.sub_category_id = \ form.category.parsed_data.get_category_ids() torrent.display_name = backend.sanitize_string((form.display_name.data or '').strip()) torrent.information = backend.sanitize_string((form.information.data or '').strip()) torrent.description = backend.sanitize_string((form.description.data or '').strip()) torrent.hidden = form.is_hidden.data torrent.remake = form.is_remake.data torrent.complete = form.is_complete.data torrent.anonymous = form.is_anonymous.data if editor.is_trusted: torrent.trusted = form.is_trusted.data if editor.is_moderator: locked_changed = torrent.comment_locked != form.is_comment_locked.data torrent.comment_locked = form.is_comment_locked.data url = flask.url_for('torrents.view', torrent_id=torrent.id) if editor.is_moderator and locked_changed: log = "Torrent [#{0}]({1}) marked as {2}".format( torrent.id, url, "comments locked" if torrent.comment_locked else "comments unlocked") adminlog = models.AdminLog(log=log, admin_id=editor.id) db.session.add(adminlog) db.session.commit() flask.flash(Markup( 'Torrent has been successfully edited! Changes might take a few minutes to show up.'), 'success') url = flask.url_for('torrents.view', torrent_id=torrent.id) return flask.redirect(url) elif flask.request.method == 'POST' and delete_form.validate() and \ (not ban_form or ban_form.validate()): return _delete_torrent(torrent, delete_form, ban_form) else: if flask.request.method != 'POST': # Fill form data only if the POST didn't fail form.category.data = torrent.sub_category.id_as_string form.display_name.data = torrent.display_name form.information.data = torrent.information form.description.data = torrent.description form.is_hidden.data = torrent.hidden form.is_remake.data = torrent.remake form.is_complete.data = torrent.complete form.is_anonymous.data = torrent.anonymous form.is_trusted.data = torrent.trusted form.is_comment_locked.data = torrent.comment_locked ipbanned = None if editor.is_moderator: torrent_ip_banned = True user_ip_banned = True # Archived torrents do not have a null uploader_ip if torrent.uploader_ip: torrent_ip_banned = models.Ban.banned(None, torrent.uploader_ip).first() if torrent.user: user_ip_banned = models.Ban.banned(None, torrent.user.last_login_ip).first() ipbanned = (torrent_ip_banned and user_ip_banned) return flask.render_template('edit.html', form=form, delete_form=delete_form, ban_form=ban_form, torrent=torrent, ipbanned=ipbanned) def _delete_torrent(torrent, form, banform): editor = flask.g.user uploader = torrent.user # Only allow admins edit deleted torrents if torrent.deleted and not (editor and editor.is_moderator): flask.abort(404) action = None url = flask.url_for('main.home') ban_torrent = form.ban.data if banform: ban_torrent = ban_torrent or banform.ban_user.data or banform.ban_userip.data if form.delete.data and not torrent.deleted: action = 'deleted' torrent.deleted = True db.session.add(torrent) elif ban_torrent and not torrent.banned and editor.is_moderator: action = 'banned' torrent.banned = True if not torrent.deleted: torrent.deleted = True action = 'deleted and banned' db.session.add(models.TrackerApi(torrent.info_hash, 'remove')) torrent.stats.seed_count = 0 torrent.stats.leech_count = 0 db.session.add(torrent) elif form.undelete.data and torrent.deleted: action = 'undeleted' torrent.deleted = False if torrent.banned: action = 'undeleted and unbanned' torrent.banned = False db.session.add(models.TrackerApi(torrent.info_hash, 'insert')) db.session.add(torrent) elif form.unban.data and torrent.banned: action = 'unbanned' torrent.banned = False db.session.add(models.TrackerApi(torrent.info_hash, 'insert')) db.session.add(torrent) if not action and not ban_torrent: flask.flash(Markup('What the fuck are you doing?'), 'danger') return flask.redirect(flask.url_for('torrents.edit', torrent_id=torrent.id)) if action and editor.is_moderator: url = flask.url_for('torrents.view', torrent_id=torrent.id) if editor is not uploader: log = "Torrent [#{0}]({1}) has been {2}".format(torrent.id, url, action) adminlog = models.AdminLog(log=log, admin_id=editor.id) db.session.add(adminlog) if action: db.session.commit() flask.flash(Markup('Torrent has been successfully {0}.'.format(action)), 'success') if not banform or not (banform.ban_user.data or banform.ban_userip.data): return flask.redirect(url) if banform.ban_userip.data: tbanned = models.Ban.banned(None, torrent.uploader_ip).first() ubanned = True if uploader: ubanned = models.Ban.banned(None, uploader.last_login_ip).first() ipbanned = (tbanned and ubanned) if (banform.ban_user.data and (not uploader or uploader.is_banned)) or \ (banform.ban_userip.data and ipbanned): flask.flash(Markup('What the fuck are you doing?'), 'danger') return flask.redirect(flask.url_for('torrents.edit', torrent_id=torrent.id)) flavor = "Nyaa" if app.config['SITE_FLAVOR'] == 'nyaa' else "Sukebei" eurl = flask.url_for('torrents.view', torrent_id=torrent.id, _external=True) reason = "[{0}#{1}]({2}) {3}".format(flavor, torrent.id, eurl, banform.reason.data) ban1 = models.Ban(admin_id=editor.id, reason=reason) ban2 = models.Ban(admin_id=editor.id, reason=reason) db.session.add(ban1) if uploader: uploader.status = models.UserStatusType.BANNED db.session.add(uploader) ban1.user_id = uploader.id ban2.user_id = uploader.id if banform.ban_userip.data: if not ubanned: ban1.user_ip = ip_address(uploader.last_login_ip) if not tbanned: uploader_ip = ip_address(torrent.uploader_ip) if ban1.user_ip != uploader_ip: ban2.user_ip = uploader_ip db.session.add(ban2) else: ban1.user_ip = ip_address(torrent.uploader_ip) uploader_str = "Anonymous" if uploader: uploader_url = flask.url_for('users.view_user', user_name=uploader.username) uploader_str = "[{0}]({1})".format(uploader.username, uploader_url) if ban1.user_ip: uploader_str += " IP({0})".format(ban1.user_ip) ban1.user_ip = ban1.user_ip.packed if ban2.user_ip: uploader_str += " IP({0})".format(ban2.user_ip) ban2.user_ip = ban2.user_ip.packed log = "Uploader {0} of torrent [#{1}]({2}) has been banned.".format( uploader_str, torrent.id, flask.url_for('torrents.view', torrent_id=torrent.id), action) adminlog = models.AdminLog(log=log, admin_id=editor.id) db.session.add(adminlog) db.session.commit() flask.flash(Markup('Uploader has been successfully banned.'), 'success') return flask.redirect(url) @bp.route('/view//magnet') def redirect_magnet(torrent_id): torrent = models.Torrent.by_id(torrent_id) if not torrent: flask.abort(404) return flask.redirect(torrents.create_magnet(torrent)) @bp.route('/view//torrent') @bp.route('/download/.torrent', endpoint='download') def download_torrent(torrent_id): torrent = models.Torrent.by_id(torrent_id) if not torrent or not torrent.has_torrent: flask.abort(404) if torrent.deleted and not (flask.g.user and flask.g.user.is_moderator): flask.abort(404) torrent_file, torrent_file_size = _make_torrent_file(torrent) disposition = 'inline; filename="{0}"; filename*=UTF-8\'\'{0}'.format( quote(torrent.torrent_name.encode('utf-8'))) resp = flask.Response(torrent_file) resp.headers['Content-Type'] = 'application/x-bittorrent' resp.headers['Content-Disposition'] = disposition resp.headers['Content-Length'] = torrent_file_size return resp @bp.route('/view//comment//edit', methods=['POST']) def edit_comment(torrent_id, comment_id): if not flask.g.user: flask.abort(403) torrent = models.Torrent.by_id(torrent_id) if not torrent: flask.abort(404) comment = models.Comment.query.get(comment_id) if not comment: flask.abort(404) if not comment.user.id == flask.g.user.id: flask.abort(403) if torrent.comment_locked and not flask.g.user.is_moderator: flask.abort(403) if comment.editing_limit_exceeded: flask.abort(flask.make_response(flask.jsonify( {'error': 'Editing time limit exceeded.'}), 400)) form = forms.CommentForm(flask.request.form) if not form.validate(): error_str = ' '.join(form.errors) flask.abort(flask.make_response(flask.jsonify({'error': error_str}), 400)) comment.text = form.comment.data db.session.commit() return flask.jsonify({'comment': comment.text}) @bp.route('/view//comment//delete', methods=['POST']) def delete_comment(torrent_id, comment_id): if not flask.g.user: flask.abort(403) torrent = models.Torrent.by_id(torrent_id) if not torrent: flask.abort(404) comment = models.Comment.query.filter_by(id=comment_id).first() if not comment: flask.abort(404) if not (comment.user.id == flask.g.user.id or flask.g.user.is_superadmin): flask.abort(403) if torrent_id != comment.torrent_id: flask.abort(400) if torrent.comment_locked and not flask.g.user.is_moderator: flask.abort(403) if comment.editing_limit_exceeded and not flask.g.user.is_superadmin: flask.abort(403) db.session.delete(comment) db.session.flush() torrent.update_comment_count() url = flask.url_for('torrents.view', torrent_id=torrent.id) if flask.g.user.is_moderator: log = "Comment deleted on torrent [#{}]({})".format(torrent.id, url) adminlog = models.AdminLog(log=log, admin_id=flask.g.user.id) db.session.add(adminlog) db.session.commit() flask.flash('Comment successfully deleted.', 'success') return flask.redirect(url) @bp.route('/view//submit_report', endpoint='report', methods=['POST']) def submit_report(torrent_id): if not flask.g.user or flask.g.user.age < app.config['RATELIMIT_ACCOUNT_AGE']: flask.abort(403) form = forms.ReportForm(flask.request.form) torrent = models.Torrent.by_id(torrent_id) if not torrent: flask.abort(404) if torrent.banned: flask.flash("The torrent you've tried to report is already banned.", 'danger') flask.abort(404) if flask.request.method == 'POST' and form.validate(): report_reason = form.reason.data current_user_id = flask.g.user.id report = models.Report( torrent_id=torrent_id, user_id=current_user_id, reason=report_reason) db.session.add(report) db.session.commit() flask.flash('Successfully reported torrent!', 'success') elif len(form.reason.data) == 0: flask.flash('Please give a report reason!', 'danger') return flask.redirect(flask.url_for('torrents.view', torrent_id=torrent_id)) @bp.route('/upload', methods=['GET', 'POST']) def upload(): upload_form = forms.UploadForm(CombinedMultiDict((flask.request.files, flask.request.form))) upload_form.category.choices = _create_upload_category_choices() show_ratelimit = False next_upload_time = None ratelimit_count = 0 # Anonymous uploaders and non-trusted uploaders no_or_new_account = (not flask.g.user or (flask.g.user.age < app.config['RATELIMIT_ACCOUNT_AGE'] and not flask.g.user.is_trusted)) if app.config['RATELIMIT_UPLOADS'] and no_or_new_account: now, ratelimit_count, next_upload_time = backend.check_uploader_ratelimit(flask.g.user) show_ratelimit = ratelimit_count >= app.config['MAX_UPLOAD_BURST'] next_upload_time = next_upload_time if next_upload_time > now else None if flask.request.method == 'POST' and upload_form.validate(): try: torrent = backend.handle_torrent_upload(upload_form, flask.g.user) return flask.redirect(flask.url_for('torrents.view', torrent_id=torrent.id)) except backend.TorrentExtraValidationException: pass # If we get here with a POST, it means the form data was invalid: return a non-okay status status_code = 400 if flask.request.method == 'POST' else 200 return flask.render_template('upload.html', upload_form=upload_form, show_ratelimit=show_ratelimit, ratelimit_count=ratelimit_count, next_upload_time=next_upload_time), status_code @cached_function def _create_upload_category_choices(): ''' Turns categories in the database into a list of (id, name)s ''' choices = [('', '[Select a category]')] id_map = backend.get_category_id_map() for key in sorted(id_map.keys()): cat_names = id_map[key] is_main_cat = key.endswith('_0') # cat_name = is_main_cat and cat_names[0] or (' - ' + cat_names[1]) cat_name = ' - '.join(cat_names) choices.append((key, cat_name, is_main_cat)) return choices def _make_torrent_file(torrent): with open(torrent.info_dict_path, 'rb') as in_file: bencoded_info = in_file.read() bencoded_torrent_data = torrents.create_bencoded_torrent(torrent, bencoded_info) return bencoded_torrent_data, len(bencoded_torrent_data)