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):
url = app.config.get('TRACKER_API_URL')
if not url:
api_url = app.config.get('TRACKER_API_URL')
if not api_url:
return False
qs = []
qs.append(('auth', app.config.get('TRACKER_API_AUTH')))
qs.append(('method', method))
# Split list into at most 100 elements
chunk_size = 100
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:
qs.append(('info_hash', infohash))
for info_hashes_chunk in chunked_info_hashes:
qs = [
('auth', app.config.get('TRACKER_API_AUTH')),
('method', method)
]
qs = urlencode(qs)
url += '?' + qs
try:
req = urlopen(url)
except:
return False
qs.extend(('info_hash', info_hash) for info_hash in info_hashes_chunk)
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):

View File

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

View File

@ -55,47 +55,70 @@
</div>
<div class="panel-body">
<form method="POST">
{{ ban_form.csrf_token }}
{% if user.is_banned %}
This user is <strong>banned</strong>.<br>
{% endif %}
{% if ipbanned %}
This user is <strong>ip banned</strong>.<br>
{% endif %}
{% if not user.is_banned and not bans %}
This user is <strong>not banned</strong>.<br>
{% endif %}
<br>
<div class="row">
<div class="col-md-12">
{{ ban_form.csrf_token }}
{% if user.is_banned %}
This user is <strong>banned</strong>.
{% endif %}
{% if ipbanned %}
This user is <strong>ip banned</strong>.
{% endif %}
{% if not user.is_banned and not bans %}
This user is <strong>not banned</strong>.
{% endif %}
</div>
</div>
{% if user.is_banned or bans %}
<p>
{% for ban in bans %}
#{{ ban.id }}
by <a href="{{ url_for('users.view_user', user_name=ban.admin.username) }}">{{ ban.admin.username }}</a>
for <span markdown-text-inline>{{ ban.reason }}</span>
<br>
{% endfor %}
</p>
{{ ban_form.unban(class="btn btn-info") }}
<div class="row">
<div class="col-md-12">
<p>
{% for ban in bans %}
#{{ ban.id }}
by <a href="{{ url_for('users.view_user', user_name=ban.admin.username) }}">{{ ban.admin.username }}</a>
for <span markdown-text-inline>{{ ban.reason }}</span>
{% endfor %}
</p>
</div>
</div>
<div class="row">
<div class="col-md-12">
{{ ban_form.unban(class="btn btn-info") }}
</div>
</div>
{% endif %}
{% if not user.is_banned or not ipbanned %}
{% if user.is_banned or bans %}
<hr>
{% endif %}
{{ render_field(ban_form.reason, class_="form-control", placeholder="Specify a ban reason.") }}<br>
<div class="pull-left">
{% if not user.is_banned %}
{{ ban_form.ban_user(value="Ban User", class="btn btn-danger") }}
{% else %}
<button type="button" class="btn btn-danger disabled">Already banned</button>
{% endif %}
<div class="row" style="margin-top:1.5em">
<div class="col-md-12">
{{ render_field(ban_form.reason, class_="form-control", placeholder="Specify a ban reason.") }}<br>
</div>
</div>
<div class="pull-right">
{% 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 class="row">
<div class="col-md-4 text-left">
{% if not user.is_banned %}
{{ ban_form.ban_user(value="Ban User", class="btn btn-danger") }}
{% else %}
<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>
{% endif %}
</form>
@ -106,10 +129,12 @@
</div>
{% endif %}
<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>
<div class="row">
<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" %}
{% endblock %}
</div>

View File

@ -2,13 +2,14 @@ import binascii
import math
import time
from ipaddress import ip_address
from itertools import chain
import flask
from flask_paginate import Pagination
from itsdangerous import BadSignature, URLSafeSerializer
from nyaa import forms, models
from nyaa import backend, forms, models
from nyaa.extensions import db
from nyaa.search import (DEFAULT_MAX_SEARCH_RESULT, DEFAULT_PER_PAGE, SERACH_PAGINATE_DISPLAY_MSG,
_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')
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
search_term = chain_get(req_args, 'q', 'term')