From 9af778217bfb9a48828cc9ba69114078717671f1 Mon Sep 17 00:00:00 2001 From: nyaadev Date: Sun, 21 May 2017 19:12:15 +0200 Subject: [PATCH] DB CHANGE: Add uploader ip address to torrent column and show on torrent view page for superadmins. Added migration script!: remove sukebei_ lines if your local db does not have those. Show users ip address on user page for superadmins. Rename Admin to Moderator internally. Moderators can now change user level to trusted. Superadmins can make users moderator. Improve changing user level. --- .../3001f79b7722_add_torrents.uploader_ip.py | 30 +++++++ nyaa/backend.py | 5 +- nyaa/forms.py | 5 +- nyaa/models.py | 24 ++++-- nyaa/routes.py | 84 ++++++++++--------- nyaa/templates/edit.html | 2 +- nyaa/templates/user.html | 22 +++-- nyaa/templates/view.html | 5 +- 8 files changed, 119 insertions(+), 58 deletions(-) create mode 100644 migrations/versions/3001f79b7722_add_torrents.uploader_ip.py diff --git a/migrations/versions/3001f79b7722_add_torrents.uploader_ip.py b/migrations/versions/3001f79b7722_add_torrents.uploader_ip.py new file mode 100644 index 0000000..152c440 --- /dev/null +++ b/migrations/versions/3001f79b7722_add_torrents.uploader_ip.py @@ -0,0 +1,30 @@ +"""Add uploader_ip column to torrents table. + +Revision ID: 3001f79b7722 +Revises: +Create Date: 2017-05-21 18:01:35.472717 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '3001f79b7722' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('nyaa_torrents', sa.Column('uploader_ip', sa.Binary(), nullable=True)) + op.add_column('sukebei_torrents', sa.Column('uploader_ip', sa.Binary(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('nyaa_torrents', 'uploader_ip') + op.drop_column('sukebei_torrents', 'uploader_ip') + # ### end Alembic commands ### diff --git a/nyaa/backend.py b/nyaa/backend.py index 6be9b5d..b5853d0 100644 --- a/nyaa/backend.py +++ b/nyaa/backend.py @@ -1,3 +1,4 @@ +import flask from nyaa import app, db from nyaa import models, forms from nyaa import bencode, utils @@ -8,6 +9,7 @@ import json from werkzeug import secure_filename from collections import OrderedDict from orderedset import OrderedSet +from ipaddress import ip_address def _replace_utf8_values(dict_or_list): @@ -53,7 +55,8 @@ def handle_torrent_upload(upload_form, uploading_user=None, fromAPI=False): description=description, encoding=torrent_encoding, filesize=torrent_filesize, - user=uploading_user) + user=uploading_user, + uploader_ip=ip_address(flask.request.remote_addr).packed) # Store bencoded info_dict torrent.info = models.TorrentInfo(info_dict=torrent_data.bencoded_info_dict) diff --git a/nyaa/forms.py b/nyaa/forms.py index d7cea26..783e428 100644 --- a/nyaa/forms.py +++ b/nyaa/forms.py @@ -263,7 +263,7 @@ class UploadForm(FlaskForm): class UserForm(FlaskForm): - user_class = DisabledSelectField('Change User Class') + user_class = SelectField('Change User Class') def validate_user_class(form, field): if not field.data: @@ -294,7 +294,8 @@ def _validate_trackers(torrent_dict, tracker_to_check_for=None): for announce in announce_list: _validate_list(announce, 'announce-list item') - announce_string = _validate_bytes(announce[0], 'announce-list item url', test_decode='utf-8') + announce_string = _validate_bytes( + announce[0], 'announce-list item url', test_decode='utf-8') if tracker_to_check_for and announce_string.lower() == tracker_to_check_for.lower(): tracker_found = True diff --git a/nyaa/models.py b/nyaa/models.py index ca00e9a..07f75da 100644 --- a/nyaa/models.py +++ b/nyaa/models.py @@ -6,6 +6,7 @@ from sqlalchemy import func, ForeignKeyConstraint, Index from sqlalchemy_utils import ChoiceType, EmailType, PasswordType from werkzeug.security import generate_password_hash, check_password_hash from sqlalchemy_fulltext import FullText +from ipaddress import ip_address import re import base64 @@ -61,6 +62,7 @@ class Torrent(db.Model): encoding = db.Column(db.String(length=32), nullable=False) flags = db.Column(db.Integer, default=0, nullable=False, index=True) uploader_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) + uploader_ip = db.Column(db.Binary(length=16), default=None, nullable=True) has_torrent = db.Column(db.Boolean, nullable=False, default=False) created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow, nullable=False) @@ -92,11 +94,11 @@ class Torrent(db.Model): info = db.relationship('TorrentInfo', uselist=False, cascade="all, delete-orphan", back_populates='torrent') filelist = db.relationship('TorrentFilelist', uselist=False, - cascade="all, delete-orphan", back_populates='torrent') + cascade="all, delete-orphan", back_populates='torrent') stats = db.relationship('Statistic', uselist=False, - cascade="all, delete-orphan", back_populates='torrent', lazy='joined') + cascade="all, delete-orphan", back_populates='torrent', lazy='joined') trackers = db.relationship('TorrentTrackers', uselist=True, - cascade="all, delete-orphan", lazy='joined') + cascade="all, delete-orphan", lazy='joined') def __repr__(self): return '<{0} #{1.id} \'{1.display_name}\' {1.filesize}b>'.format(type(self).__name__, self) @@ -138,6 +140,11 @@ class Torrent(db.Model): def magnet_uri(self): return create_magnet(self) + @property + def uploader_ip_string(self): + if self.uploader_ip: + return str(ip_address(self.uploader_ip)) + @property def anonymous(self): return self.flags & TorrentFlags.ANONYMOUS @@ -313,7 +320,7 @@ class SubCategory(db.Model): class UserLevelType(IntEnum): REGULAR = 0 TRUSTED = 1 - ADMIN = 2 + MODERATOR = 2 SUPERADMIN = 3 @@ -362,6 +369,11 @@ class User(db.Model): ] return all(checks) + @property + def ip_string(self): + if self.last_login_ip: + return str(ip_address(self.last_login_ip)) + @classmethod def by_id(cls, id): return cls.query.get(id) @@ -381,8 +393,8 @@ class User(db.Model): return cls.by_username(username_or_email) or cls.by_email(username_or_email) @property - def is_admin(self): - return self.level >= UserLevelType.ADMIN + def is_moderator(self): + return self.level >= UserLevelType.MODERATOR @property def is_superadmin(self): diff --git a/nyaa/routes.py b/nyaa/routes.py index 23a68d2..a9652d5 100644 --- a/nyaa/routes.py +++ b/nyaa/routes.py @@ -11,7 +11,7 @@ import config import json from datetime import datetime, timedelta -import ipaddress +from ipaddress import ip_address import os.path import base64 from urllib.parse import quote @@ -135,6 +135,7 @@ def get_category_id_map(): app.register_blueprint(api_handler.api_blueprint, url_prefix='/api') + def chain_get(source, *args): ''' Tries to return values from source by the given keys. Returns None if none match. @@ -146,6 +147,7 @@ def chain_get(source, *args): return value return None + @app.route('/rss', defaults={'rss': True}) @app.route('/', defaults={'rss': False}) def home(rss): @@ -194,7 +196,7 @@ def home(rss): if flask.g.user: query_args['logged_in_user'] = flask.g.user - if flask.g.user.is_admin: # God mode + if flask.g.user.is_moderator: # God mode query_args['admin'] = True # If searching, we get results from elastic search @@ -215,7 +217,8 @@ def home(rss): if render_as_rss: return render_rss('"{}"'.format(search_term), query_results, use_elastic=True, magnet_links=use_magnet_links) else: - rss_query_string = _generate_query_string(search_term, category, quality_filter, user_name) + rss_query_string = _generate_query_string( + search_term, category, quality_filter, user_name) max_results = min(max_search_results, query_results['hits']['total']) # change p= argument to whatever you change page_parameter to or pagination breaks pagination = Pagination(p=query_args['page'], per_page=results_per_page, @@ -238,7 +241,8 @@ def home(rss): if render_as_rss: return render_rss('Home', query, use_elastic=False, magnet_links=use_magnet_links) else: - rss_query_string = _generate_query_string(search_term, category, quality_filter, user_name) + rss_query_string = _generate_query_string( + search_term, category, quality_filter, user_name) # Use elastic is always false here because we only hit this section # if we're browsing without a search term (which means we default to DB) # or if ES is disabled @@ -256,22 +260,23 @@ def view_user(user_name): if not user: flask.abort(404) - if flask.g.user and flask.g.user.id != user.id: - admin = flask.g.user.is_admin - superadmin = flask.g.user.is_superadmin - else: - admin = False - superadmin = False + admin_form = None + if flask.g.user and flask.g.user.is_moderator and flask.g.user.level > user.level: + admin_form = forms.UserForm() + default, admin_form.user_class.choices = _create_user_class_choices(user) + if flask.request.method == 'GET': + admin_form.user_class.data = default - form = forms.UserForm() - form.user_class.choices = _create_user_class_choices() - if flask.request.method == 'POST' and form.validate(): - selection = form.user_class.data + if flask.request.method == 'POST' and admin_form and admin_form.validate(): + selection = admin_form.user_class.data if selection == 'regular': user.level = models.UserLevelType.REGULAR elif selection == 'trusted': user.level = models.UserLevelType.TRUSTED + elif selection == 'moderator': + user.level = models.UserLevelType.MODERATOR + db.session.add(user) db.session.commit() @@ -311,7 +316,7 @@ def view_user(user_name): if flask.g.user: query_args['logged_in_user'] = flask.g.user - if flask.g.user.is_admin: # God mode + if flask.g.user.is_moderator: # God mode query_args['admin'] = True # Use elastic search for term searching @@ -344,9 +349,7 @@ def view_user(user_name): user_page=True, rss_filter=rss_query_string, level=user_level, - admin=admin, - superadmin=superadmin, - form=form) + admin_form=admin_form) # Similar logic as home page else: if use_elastic: @@ -362,9 +365,7 @@ def view_user(user_name): user_page=True, rss_filter=rss_query_string, level=user_level, - admin=admin, - superadmin=superadmin, - form=form) + admin_form=admin_form) @app.template_filter('rfc822') @@ -417,7 +418,7 @@ def login(): return flask.redirect(flask.url_for('login')) user.last_login_date = datetime.utcnow() - user.last_login_ip = ipaddress.ip_address(flask.request.remote_addr).packed + user.last_login_ip = ip_address(flask.request.remote_addr).packed db.session.add(user) db.session.commit() @@ -451,7 +452,7 @@ def register(): if flask.request.method == 'POST' and form.validate(): user = models.User(username=form.username.data.strip(), email=form.email.data.strip(), password=form.password.data) - user.last_login_ip = ipaddress.ip_address(flask.request.remote_addr).packed + user.last_login_ip = ip_address(flask.request.remote_addr).packed db.session.add(user) db.session.commit() @@ -479,13 +480,7 @@ def profile(): form = forms.ProfileForm(flask.request.form) - level = 'Regular' - if flask.g.user.is_admin: - level = 'Moderator' - if flask.g.user.is_superadmin: # check this second because we can be admin AND superadmin - level = 'Administrator' - elif flask.g.user.is_trusted: - level = 'Trusted' + level = ['Regular', 'Trusted', 'Moderator', 'Administrator'][flask.g.user.level] if flask.request.method == 'POST' and form.validate(): user = flask.g.user @@ -586,11 +581,11 @@ def view_torrent(torrent_id): flask.abort(404) # Only allow admins see deleted torrents - if torrent.deleted and not (viewer and viewer.is_admin): + if torrent.deleted and not (viewer and viewer.is_moderator): flask.abort(404) # Only allow owners and admins to edit torrents - can_edit = viewer and (viewer is torrent.user or viewer.is_admin) + can_edit = viewer and (viewer is torrent.user or viewer.is_moderator) files = None if torrent.filelist: @@ -614,11 +609,11 @@ def edit_torrent(torrent_id): flask.abort(404) # Only allow admins edit deleted torrents - if torrent.deleted and not (editor and editor.is_admin): + 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_admin): + if not editor or not (editor is torrent.user or editor.is_moderator): flask.abort(403) if flask.request.method == 'POST' and form.validate(): @@ -636,7 +631,7 @@ def edit_torrent(torrent_id): if editor.is_trusted: torrent.trusted = form.is_trusted.data - if editor.is_admin: + if editor.is_moderator: torrent.deleted = form.is_deleted.data db.session.commit() @@ -739,11 +734,22 @@ def send_verification_email(to_address, activ_link): server.quit() -def _create_user_class_choices(): +def _create_user_class_choices(user): choices = [('regular', 'Regular')] - if flask.g.user and flask.g.user.is_superadmin: - choices.append(('trusted', 'Trusted')) - return choices + default = 'regular' + if flask.g.user: + if flask.g.user.is_moderator: + choices.append(('trusted', 'Trusted')) + if flask.g.user.is_superadmin: + choices.append(('moderator', 'Moderator')) + + if user: + if user.is_moderator: + default = 'moderator' + elif user.is_trusted: + default = 'trusted' + + return default, choices # #################################### STATIC PAGES #################################### diff --git a/nyaa/templates/edit.html b/nyaa/templates/edit.html index 44dfd51..b65ce0c 100644 --- a/nyaa/templates/edit.html +++ b/nyaa/templates/edit.html @@ -31,7 +31,7 @@
- {% if editor.is_admin %} + {% if editor.is_moderator %}