Implement torrent nuking ability for mods (#377)

* Implement torrent nuking ability for mods

This deletes all torrents of a specific user.
A current caveat is that it will delete both sukebei and nyaa torrents,
but will only leave a log entry in the current flavour's log.

Also did some bootstrap untangling on the user view page.

* Per-flavour logging

Hopefully this works. Maybe.

* Tracker API: chunk into 100-element sublists

* isort

* Restrict nuking to superadmins

Also do a lint.sh.
This commit is contained in:
Nicolas F 2017-10-17 03:17:12 +02:00 committed by Arylide
parent de1fd2f1bc
commit 4019343d50
4 changed files with 120 additions and 51 deletions

View File

@ -333,25 +333,32 @@ def handle_torrent_upload(upload_form, uploading_user=None, fromAPI=False):
def tracker_api(info_hashes, method): def tracker_api(info_hashes, method):
url = app.config.get('TRACKER_API_URL') api_url = app.config.get('TRACKER_API_URL')
if not url: if not api_url:
return False return False
qs = [] # Split list into at most 100 elements
qs.append(('auth', app.config.get('TRACKER_API_AUTH'))) chunk_size = 100
qs.append(('method', method)) chunk_range = range(0, len(info_hashes), chunk_size)
chunked_info_hashes = (info_hashes[i:i + chunk_size] for i in chunk_range)
for infohash in info_hashes: for info_hashes_chunk in chunked_info_hashes:
qs.append(('info_hash', infohash)) qs = [
('auth', app.config.get('TRACKER_API_AUTH')),
('method', method)
]
qs = urlencode(qs) qs.extend(('info_hash', info_hash) for info_hash in info_hashes_chunk)
url += '?' + qs
try:
req = urlopen(url)
except:
return False
return req.status == 200 api_url += '?' + urlencode(qs)
try:
req = urlopen(api_url)
except:
return False
if req.status != 200:
return False
return True
def _delete_cached_torrent_file(torrent_id): def _delete_cached_torrent_file(torrent_id):

View File

@ -265,6 +265,7 @@ class DeleteForm(FlaskForm):
class BanForm(FlaskForm): class BanForm(FlaskForm):
ban_user = SubmitField("Delete & Ban and Ban User") ban_user = SubmitField("Delete & Ban and Ban User")
ban_userip = SubmitField("Delete & Ban and Ban User+IP") ban_userip = SubmitField("Delete & Ban and Ban User+IP")
nuke = SubmitField("Delete & Ban all torrents")
unban = SubmitField("Unban") unban = SubmitField("Unban")
_validator = DataRequired() _validator = DataRequired()

View File

@ -55,47 +55,70 @@
</div> </div>
<div class="panel-body"> <div class="panel-body">
<form method="POST"> <form method="POST">
{{ ban_form.csrf_token }} <div class="row">
{% if user.is_banned %} <div class="col-md-12">
This user is <strong>banned</strong>.<br> {{ ban_form.csrf_token }}
{% endif %} {% if user.is_banned %}
{% if ipbanned %} This user is <strong>banned</strong>.
This user is <strong>ip banned</strong>.<br> {% endif %}
{% endif %} {% if ipbanned %}
{% if not user.is_banned and not bans %} This user is <strong>ip banned</strong>.
This user is <strong>not banned</strong>.<br> {% endif %}
{% endif %} {% if not user.is_banned and not bans %}
<br> This user is <strong>not banned</strong>.
{% endif %}
</div>
</div>
{% if user.is_banned or bans %} {% if user.is_banned or bans %}
<p> <div class="row">
{% for ban in bans %} <div class="col-md-12">
#{{ ban.id }} <p>
by <a href="{{ url_for('users.view_user', user_name=ban.admin.username) }}">{{ ban.admin.username }}</a> {% for ban in bans %}
for <span markdown-text-inline>{{ ban.reason }}</span> #{{ ban.id }}
<br> by <a href="{{ url_for('users.view_user', user_name=ban.admin.username) }}">{{ ban.admin.username }}</a>
{% endfor %} for <span markdown-text-inline>{{ ban.reason }}</span>
</p> {% endfor %}
{{ ban_form.unban(class="btn btn-info") }} </p>
</div>
</div>
<div class="row">
<div class="col-md-12">
{{ ban_form.unban(class="btn btn-info") }}
</div>
</div>
{% endif %} {% endif %}
{% if not user.is_banned or not ipbanned %} {% if not user.is_banned or not ipbanned %}
{% if user.is_banned or bans %} {% if user.is_banned or bans %}
<hr> <hr>
{% endif %} {% endif %}
{{ render_field(ban_form.reason, class_="form-control", placeholder="Specify a ban reason.") }}<br> <div class="row" style="margin-top:1.5em">
<div class="pull-left"> <div class="col-md-12">
{% if not user.is_banned %} {{ render_field(ban_form.reason, class_="form-control", placeholder="Specify a ban reason.") }}<br>
{{ ban_form.ban_user(value="Ban User", class="btn btn-danger") }} </div>
{% else %}
<button type="button" class="btn btn-danger disabled">Already banned</button>
{% endif %}
</div> </div>
<div class="pull-right"> <div class="row">
{% if not ipbanned %} <div class="col-md-4 text-left">
{{ ban_form.ban_userip(value="Ban User+IP", class="btn btn-danger") }} {% if not user.is_banned %}
{% else %} {{ ban_form.ban_user(value="Ban User", class="btn btn-danger") }}
<button type="button" class="btn btn-danger disabled">Already IP banned</button> {% else %}
{% endif %} <button type="button" class="btn btn-danger disabled">Already banned</button>
{% endif %}
</div>
<div class="col-md-4 text-center">
{% if not ipbanned %}
{{ ban_form.ban_userip(value="Ban User+IP", class="btn btn-danger") }}
{% else %}
<button type="button" class="btn btn-danger disabled">Already IP banned</button>
{% endif %}
</div>
<div class="col-md-4 text-right">
{% if g.user.is_superadmin %}
{{ ban_form.nuke(value="\U0001F4A3 Nuke Torrents", class="btn btn-danger") }}
{% else %}
<button type="button" class="btn btn-danger disabled">&#x1f4a3; Nuke Torrents</button>
{% endif %}
</div>
</div> </div>
{% endif %} {% endif %}
</form> </form>
@ -106,10 +129,12 @@
</div> </div>
{% endif %} {% endif %}
<h3> <div class="row">
Browsing <span class="text-{{ user.userlevel_color }}" data-toggle="tooltip" title="{{ user.userlevel_str }}">{{ user.username }}</span>'{{ '' if user.username[-1] == 's' else 's' }} torrents <h3>
</h3> Browsing <span class="text-{{ user.userlevel_color }}" data-toggle="tooltip" title="{{ user.userlevel_str }}">{{ user.username }}</span>'{{ '' if user.username[-1] == 's' else 's' }} torrents
</h3>
{% include "search_results.html" %} {% include "search_results.html" %}
{% endblock %} {% endblock %}
</div>

View File

@ -2,13 +2,14 @@ import binascii
import math import math
import time import time
from ipaddress import ip_address from ipaddress import ip_address
from itertools import chain
import flask import flask
from flask_paginate import Pagination from flask_paginate import Pagination
from itsdangerous import BadSignature, URLSafeSerializer from itsdangerous import BadSignature, URLSafeSerializer
from nyaa import forms, models from nyaa import backend, forms, models
from nyaa.extensions import db from nyaa.extensions import db
from nyaa.search import (DEFAULT_MAX_SEARCH_RESULT, DEFAULT_PER_PAGE, SERACH_PAGINATE_DISPLAY_MSG, from nyaa.search import (DEFAULT_MAX_SEARCH_RESULT, DEFAULT_PER_PAGE, SERACH_PAGINATE_DISPLAY_MSG,
_generate_query_string, search_db, search_elastic) _generate_query_string, search_db, search_elastic)
@ -102,6 +103,41 @@ def view_user(user_name):
flask.flash(flask.Markup('User has been successfully {0}.'.format(action)), 'success') flask.flash(flask.Markup('User has been successfully {0}.'.format(action)), 'success')
return flask.redirect(url) return flask.redirect(url)
if flask.request.method == 'POST' and ban_form and ban_form.nuke.data:
if flask.g.user.is_superadmin:
nyaa_banned = 0
sukebei_banned = 0
info_hashes = []
for t in chain(user.nyaa_torrents, user.sukebei_torrents):
t.deleted = True
t.banned = True
info_hashes.append([t.info_hash])
db.session.add(t)
if isinstance(t, models.NyaaTorrent):
nyaa_banned += 1
else:
sukebei_banned += 1
if info_hashes:
backend.tracker_api(info_hashes, 'ban')
for log_flavour, num in ((models.NyaaAdminLog, nyaa_banned),
(models.SukebeiAdminLog, sukebei_banned)):
if num > 0:
log = "Nuked {0} torrents of [{1}]({2})".format(num,
user.username,
url)
adminlog = log_flavour(log=log, admin_id=flask.g.user.id)
db.session.add(adminlog)
db.session.commit()
flask.flash('Torrents of {0} have been nuked.'.format(user.username),
'success')
return flask.redirect(url)
else:
flask.flash('Insufficient permissions to nuke.', 'danger')
return flask.redirect(url)
req_args = flask.request.args req_args = flask.request.args
search_term = chain_get(req_args, 'q', 'term') search_term = chain_get(req_args, 'q', 'term')