From 9ad2264edf756fc8312f240620013f31e0351de5 Mon Sep 17 00:00:00 2001 From: Kfir Hadas Date: Wed, 17 May 2017 10:53:06 +0300 Subject: [PATCH 01/90] Travis config --- .travis.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..0a3297b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +language: python + +python: + - 3.6 + +dist: xenial +sudo: false + +cache: pip + +install: + - pip install --upgrade pep8 + +script: + - pep8 nyaa/ --show-source --max-line-length=100 From d7694d6c620597d95fa7730c0fef16bf64c52a46 Mon Sep 17 00:00:00 2001 From: Kfir Hadas Date: Wed, 17 May 2017 11:20:42 +0300 Subject: [PATCH 02/90] Disable email notifications --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 0a3297b..feced64 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,3 +13,6 @@ install: script: - pep8 nyaa/ --show-source --max-line-length=100 + +notifications: + email: false From 2c5a577f7edee4eda867be3b6867611611958bd6 Mon Sep 17 00:00:00 2001 From: Kfir Hadas Date: Wed, 17 May 2017 12:24:17 +0300 Subject: [PATCH 03/90] Use pycodestyle --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index feced64..495b891 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,10 +9,10 @@ sudo: false cache: pip install: - - pip install --upgrade pep8 + - pip install --upgrade pycodestyle script: - - pep8 nyaa/ --show-source --max-line-length=100 + - pycodestyle nyaa/ --show-source --max-line-length=100 notifications: email: false From cc957ccc96ab9f7d1c26485064f8e40068623998 Mon Sep 17 00:00:00 2001 From: Simon Veit Engmann Date: Sat, 20 May 2017 01:13:04 +0200 Subject: [PATCH 04/90] Changed RSS feed to conform to RSS standards and added a namespace --- nyaa/routes.py | 4 ++++ nyaa/templates/rss.xml | 24 ++++++++++++------------ nyaa/templates/xmlns.html | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 12 deletions(-) create mode 100644 nyaa/templates/xmlns.html diff --git a/nyaa/routes.py b/nyaa/routes.py index 4bb4e8f..27aa889 100644 --- a/nyaa/routes.py +++ b/nyaa/routes.py @@ -743,6 +743,10 @@ def site_rules(): def site_help(): return flask.render_template('help.html') +@app.routes('/xmlns/nyaa', methods=['GET']) +def xmlns_nyaa(): + return flask.render_template('xmlns.html') + # #################################### API ROUTES #################################### @app.route('/api/upload', methods=['POST']) diff --git a/nyaa/templates/rss.xml b/nyaa/templates/rss.xml index 7664da9..0cd5181 100644 --- a/nyaa/templates/rss.xml +++ b/nyaa/templates/rss.xml @@ -1,4 +1,4 @@ - + {{ config.SITE_NAME }} Torrent File RSS RSS Feed for {{ term }} @@ -17,10 +17,10 @@ {{ url_for('view_torrent', torrent_id=torrent.meta.id, _external=True) }} {{ torrent.created_time|rfc822_es }} - {{- torrent.seed_count }} - {{- torrent.leech_count }} - {{- torrent.download_count }} - {{- torrent.info_hash }} + {{- torrent.seed_count }} + {{- torrent.leech_count }} + {{- torrent.download_count }} + {{- torrent.info_hash }} {% else %} {% if torrent.has_torrent and not magnet_links %} {{ url_for('download_torrent', torrent_id=torrent.id, _external=True) }} @@ -30,15 +30,15 @@ {{ url_for('view_torrent', torrent_id=torrent.id, _external=True) }} {{ torrent.created_time|rfc822 }} - {{- torrent.stats.seed_count }} - {{- torrent.stats.leech_count }} - {{- torrent.stats.download_count }} - {{- torrent.info_hash_as_hex }} + {{- torrent.stats.seed_count }} + {{- torrent.stats.leech_count }} + {{- torrent.stats.download_count }} + {{- torrent.info_hash_as_hex }} {% endif %} {% set cat_id = use_elastic and ((torrent.main_category_id|string) + '_' + (torrent.sub_category_id|string)) or torrent.sub_category.id_as_string %} - {{- cat_id }} - {{- category_name(cat_id) }} - {{- torrent.filesize | filesizeformat(True) }} + {{- cat_id }} + {{- category_name(cat_id) }} + {{- torrent.filesize | filesizeformat(True) }} {% endfor %} diff --git a/nyaa/templates/xmlns.html b/nyaa/templates/xmlns.html new file mode 100644 index 0000000..9d3481e --- /dev/null +++ b/nyaa/templates/xmlns.html @@ -0,0 +1,32 @@ +{% extends "layout.html" %} +{% block title %}XML Namespace :: {{ config.SITE_NAME }}{% endblock %} +{% block body %} +
+

Nyaa XML Namespace

+

You found this page because our RSS feeds contain an URL that links here. Said URL is not an actual page but rather a unique identifier used to prevent name collisions with other XML namespaces.

+

The namespace contains the following additional, informational tags:

+
    +
  • +

    <nyaa:seeders> holds the current amount of seeders on the respective torrent.

    +
  • +
  • +

    <nyaa:leechers> holds the current amount of leechers on the respective torrent.

    +
  • +
  • +

    <nyaa:downloads> counts the downloads the torrent got up to the point the feed was refreshed.

    +
  • +
  • +

    <nyaa:infoHash> is the torrent's infohash, a unique identifier, in hexadecimal.

    +
  • +
  • +

    <nyaa:categoryId> contains the ID of the category containing the upload in the form category_subcategory.

    +
  • +
  • +

    <nyaa:category> contains the written name of the torrent's category in the form Category - Subcategory.

    +
  • +
  • +

    <nyaa:size> indicates the torrent's download size to one decimal place, using a magnitude prefix according to ISO/IEC 80000-13.

    +
  • +
+
+{% endblock %} From 152e547ac5b7aa027e6dd634c695a631943a7020 Mon Sep 17 00:00:00 2001 From: nyaadev Date: Sun, 21 May 2017 17:47:16 +0200 Subject: [PATCH 05/90] Add flask-Migrate + alembic for automated database migrations. Update some dependencies to their latest version. Make executable scripts executable (chmod +x). --- README.md | 6 +++ WSGI.py | 0 db_create.py | 6 --- db_migrate.py | 13 ++++++ import_to_es.py | 0 migrations/README | 1 + migrations/alembic.ini | 45 ++++++++++++++++++++ migrations/env.py | 87 +++++++++++++++++++++++++++++++++++++++ migrations/script.py.mako | 24 +++++++++++ requirements.txt | 28 ++++++++----- run.py | 0 sync_es.py | 0 12 files changed, 194 insertions(+), 16 deletions(-) mode change 100644 => 100755 WSGI.py mode change 100644 => 100755 db_create.py create mode 100755 db_migrate.py mode change 100644 => 100755 import_to_es.py create mode 100755 migrations/README create mode 100644 migrations/alembic.ini create mode 100755 migrations/env.py create mode 100755 migrations/script.py.mako mode change 100644 => 100755 run.py mode change 100644 => 100755 sync_es.py diff --git a/README.md b/README.md index 843d0ac..c2471a8 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,12 @@ - Set up `sync_es.py` as a service and run it, preferably as the system/root - Make sure `sync_es.py` runs within venv with the right dependencies +## Database migrations +- Uses [flask-Migrate](https://flask-migrate.readthedocs.io/) +- Run `./db_migrate.py db migrate` to generate the migration script after database model changes. +- Take a look at the result in `migrations/versions/...` to make sure nothing went wrong. +- Run `./db_migrate.py db upgrade` to upgrade your database. + ## Good to go! - After that, enable the `USE_ELASTIC_SEARCH` flag and restart the webapp and you're good to go diff --git a/WSGI.py b/WSGI.py old mode 100644 new mode 100755 diff --git a/db_create.py b/db_create.py old mode 100644 new mode 100755 index 99c4a85..d881377 --- a/db_create.py +++ b/db_create.py @@ -33,9 +33,3 @@ if not existing_cats: db.session.add(main_cat) db.session.commit() - -# Create fulltext index - -if app.config['USE_MYSQL']: - db.engine.execute('ALTER TABLE ' + app.config['TABLE_PREFIX'] + 'torrents ADD FULLTEXT KEY (display_name)') - diff --git a/db_migrate.py b/db_migrate.py new file mode 100755 index 0000000..8d4f8f0 --- /dev/null +++ b/db_migrate.py @@ -0,0 +1,13 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +from nyaa import app, db +from flask_script import Manager +from flask_migrate import Migrate, MigrateCommand + +migrate = Migrate(app, db) + +manager = Manager(app) +manager.add_command("db", MigrateCommand) + +if __name__ == "__main__": + manager.run() diff --git a/import_to_es.py b/import_to_es.py old mode 100644 new mode 100755 diff --git a/migrations/README b/migrations/README new file mode 100755 index 0000000..98e4f9c --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..f8ed480 --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,45 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100755 index 0000000..4593816 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,87 @@ +from __future__ import with_statement +from alembic import context +from sqlalchemy import engine_from_config, pool +from logging.config import fileConfig +import logging + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +from flask import current_app +config.set_main_option('sqlalchemy.url', + current_app.config.get('SQLALCHEMY_DATABASE_URI')) +target_metadata = current_app.extensions['migrate'].db.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure(url=url) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.readthedocs.org/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + engine = engine_from_config(config.get_section(config.config_ini_section), + prefix='sqlalchemy.', + poolclass=pool.NullPool) + + connection = engine.connect() + context.configure(connection=connection, + target_metadata=target_metadata, + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args) + + try: + with context.begin_transaction(): + context.run_migrations() + finally: + connection.close() + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100755 index 0000000..2c01563 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/requirements.txt b/requirements.txt index 6e23eca..2a184ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +alembic==0.9.2 appdirs==1.4.3 argon2-cffi==16.3.0 autopep8==1.3.1 @@ -5,9 +6,14 @@ blinker==1.4 cffi==1.10.0 click==6.7 dominate==2.3.1 -Flask==0.12.1 +elasticsearch==5.3.0 +elasticsearch-dsl==5.2.0 +Flask==0.12.2 Flask-Assets==0.12 Flask-DebugToolbar==0.10.1 +Flask-Migrate==2.0.3 +flask-paginate==0.4.5 +Flask-Script==2.0.5 Flask-SQLAlchemy==2.2 Flask-WTF==0.14.2 gevent==1.2.1 @@ -15,27 +21,29 @@ greenlet==0.4.12 itsdangerous==0.24 Jinja2==2.9.6 libsass==0.12.3 +Mako==1.0.6 MarkupSafe==1.0 +mysql-replication==0.13 mysqlclient==1.3.10 orderedset==2.0 packaging==16.8 passlib==1.7.1 +progressbar2==3.20.0 pycodestyle==2.3.1 pycparser==2.17 +PyMySQL==0.7.11 pyparsing==2.2.0 +python-dateutil==2.6.0 +python-editor==1.0.3 +python-utils==2.1.0 six==1.10.0 -SQLAlchemy==1.1.9 +SQLAlchemy==1.1.10 SQLAlchemy-FullText-Search==0.2.3 SQLAlchemy-Utils==0.32.14 +statsd==3.2.1 +urllib3==1.21.1 uWSGI==2.0.15 visitor==0.1.3 webassets==0.12.1 -Werkzeug==0.12.1 +Werkzeug==0.12.2 WTForms==2.1 -## elasticsearch dependencies -elasticsearch==5.3.0 -elasticsearch-dsl==5.2.0 -progressbar2==3.20.0 -mysql-replication==0.13 -flask-paginate==0.4.5 -statsd==3.2.1 diff --git a/run.py b/run.py old mode 100644 new mode 100755 diff --git a/sync_es.py b/sync_es.py old mode 100644 new mode 100755 From 9af778217bfb9a48828cc9ba69114078717671f1 Mon Sep 17 00:00:00 2001 From: nyaadev Date: Sun, 21 May 2017 19:12:15 +0200 Subject: [PATCH 06/90] 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 %}
From a4c7dd7912c2ee63eaaee3819d2651bdf3d1d644 Mon Sep 17 00:00:00 2001 From: nyaadev Date: Thu, 18 May 2017 15:09:35 +0200 Subject: [PATCH 07/90] Add ReCaptcha to upload page if user is not logged in. Bring back CSRF to upload form (Use the upload API) --- nyaa/forms.py | 16 +++++++++++---- nyaa/routes.py | 10 +++++----- nyaa/templates/upload.html | 41 ++++++++++++++++++++++++++------------ 3 files changed, 45 insertions(+), 22 deletions(-) diff --git a/nyaa/forms.py b/nyaa/forms.py index 783e428..1a61706 100644 --- a/nyaa/forms.py +++ b/nyaa/forms.py @@ -1,3 +1,4 @@ +import flask from nyaa import db, app from nyaa.models import User from nyaa import bencode, utils, models @@ -15,6 +16,7 @@ from wtforms.widgets import Select as SelectWidget from wtforms.widgets import html_params, HTMLString from flask_wtf.recaptcha import RecaptchaField +from flask_wtf.recaptcha.validators import Recaptcha as RecaptchaValidator class Unique(object): @@ -164,10 +166,6 @@ class EditForm(FlaskForm): class UploadForm(FlaskForm): - - class Meta: - csrf = False - torrent_file = FileField('Torrent file', [ FileRequired() ]) @@ -179,6 +177,16 @@ class UploadForm(FlaskForm): '%(max)d at most.') ]) + if app.config['USE_RECAPTCHA']: + # Captcha only for not logged in users + _recaptcha_validator = RecaptchaValidator() + + def _validate_recaptcha(form, field): + if not flask.g.user: + return UploadForm._recaptcha_validator(form, field) + + recaptcha = RecaptchaField(validators=[_validate_recaptcha]) + # category = SelectField('Category') category = DisabledSelectField('Category') diff --git a/nyaa/routes.py b/nyaa/routes.py index a9652d5..7dda5e5 100644 --- a/nyaa/routes.py +++ b/nyaa/routes.py @@ -558,17 +558,17 @@ def _create_upload_category_choices(): @app.route('/upload', methods=['GET', 'POST']) def upload(): - form = forms.UploadForm(CombinedMultiDict((flask.request.files, flask.request.form))) - form.category.choices = _create_upload_category_choices() + upload_form = forms.UploadForm(CombinedMultiDict((flask.request.files, flask.request.form))) + upload_form.category.choices = _create_upload_category_choices() - if flask.request.method == 'POST' and form.validate(): - torrent = backend.handle_torrent_upload(form, flask.g.user) + if flask.request.method == 'POST' and upload_form.validate(): + torrent = backend.handle_torrent_upload(upload_form, flask.g.user) return flask.redirect('/view/' + str(torrent.id)) else: # 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', form=form, user=flask.g.user), status_code + return flask.render_template('upload.html', upload_form=upload_form), status_code @app.route('/view/') diff --git a/nyaa/templates/upload.html b/nyaa/templates/upload.html index 778a37d..e54beaa 100644 --- a/nyaa/templates/upload.html +++ b/nyaa/templates/upload.html @@ -7,25 +7,27 @@

Upload Torrent

-{% if not user %} +{% if not g.user %}

You are not logged in, and are uploading anonymously.

{% endif %}
Drop here!
- {% if config.ENFORCE_MAIN_ANNOUNCE_URL %}

Important: Please include {{config.MAIN_ANNOUNCE_URL}} in your trackers

{% endif %} + {{ upload_form.csrf_token }} + + {% if config.ENFORCE_MAIN_ANNOUNCE_URL %}

Important: Please include {{ config.MAIN_ANNOUNCE_URL }} in your trackers

{% endif %}
- {{ render_upload(form.torrent_file, accept=".torrent") }} + {{ render_upload(upload_form.torrent_file, accept=".torrent") }}
- {{ render_field(form.display_name, class_='form-control', placeholder='Display name') }} + {{ render_field(upload_form.display_name, class_='form-control', placeholder='Display name') }}
- {{ render_field(form.category, class_='form-control')}} + {{ render_field(upload_form.category, class_='form-control')}}
@@ -33,30 +35,30 @@
- {{ render_field(form.information, class_='form-control', placeholder='Your website or IRC channel') }} + {{ render_field(upload_form.information, class_='form-control', placeholder='Your website or IRC channel') }}
- {% if user.is_trusted %} + {% if g.user.is_trusted %} {% endif %} @@ -66,10 +68,23 @@
- {{ render_markdown_editor(form.description, field_name='description') }} + {{ render_markdown_editor(upload_form.description, field_name='description') }}
+ {% if config.USE_RECAPTCHA and not g.user %} +
+
+ {% for error in upload_form.recaptcha.errors %} + {{ error }} + {% endfor %} + {{ upload_form.recaptcha }} +
+
+ {% endif %} + +
+
From 0a258d59e1386e2e966cbe831b33d44922ec035d Mon Sep 17 00:00:00 2001 From: nyaadev Date: Mon, 22 May 2017 01:49:02 +0200 Subject: [PATCH 08/90] temporary ghetto import, will be removed once importing is done. --- nyaa/api_handler.py | 137 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/nyaa/api_handler.py b/nyaa/api_handler.py index 6638af7..4ddd510 100644 --- a/nyaa/api_handler.py +++ b/nyaa/api_handler.py @@ -186,3 +186,140 @@ def v2_api_upload(): # Map errors back from form fields into the api keys 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 + + +# #################################### TEMPORARY #################################### + +from orderedset import OrderedSet + + +@api_blueprint.route('/ghetto_import', methods=['POST']) +def ghetto_import(): + if flask.request.remote_addr != '127.0.0.1': + return flask.error(403) + + torrent_file = flask.request.files.get('torrent') + + try: + torrent_dict = bencode.decode(torrent_file) + # field.data.close() + except (bencode.MalformedBencodeException, UnicodeError): + return 'Malformed torrent file', 500 + + try: + forms._validate_torrent_metadata(torrent_dict) + except AssertionError as e: + return 'Malformed torrent metadata ({})'.format(e.args[0]), 500 + + try: + tracker_found = forms._validate_trackers(torrent_dict) + except AssertionError as e: + return 'Malformed torrent trackers ({})'.format(e.args[0]), 500 + + bencoded_info_dict = bencode.encode(torrent_dict['info']) + info_hash = utils.sha1_hash(bencoded_info_dict) + + # Check if the info_hash exists already in the database + torrent = models.Torrent.by_info_hash(info_hash) + if not torrent: + return 'This torrent does not exists', 500 + + if torrent.has_torrent: + return 'This torrent already has_torrent', 500 + + # Torrent is legit, pass original filename and dict along + torrent_data = forms.TorrentFileData(filename=os.path.basename(torrent_file.filename), + torrent_dict=torrent_dict, + info_hash=info_hash, + bencoded_info_dict=bencoded_info_dict) + + # The torrent has been validated and is safe to access with ['foo'] etc - all relevant + # keys and values have been checked for (see UploadForm in forms.py for details) + info_dict = torrent_data.torrent_dict['info'] + + changed_to_utf8 = backend._replace_utf8_values(torrent_data.torrent_dict) + + torrent_filesize = info_dict.get('length') or sum( + f['length'] for f in info_dict.get('files')) + + # In case no encoding, assume UTF-8. + torrent_encoding = torrent_data.torrent_dict.get('encoding', b'utf-8').decode('utf-8') + + # Store bencoded info_dict + torrent.info = models.TorrentInfo(info_dict=torrent_data.bencoded_info_dict) + torrent.has_torrent = True + + # To simplify parsing the filelist, turn single-file torrent into a list + torrent_filelist = info_dict.get('files') + + used_path_encoding = changed_to_utf8 and 'utf-8' or torrent_encoding + + parsed_file_tree = dict() + if not torrent_filelist: + # If single-file, the root will be the file-tree (no directory) + file_tree_root = parsed_file_tree + torrent_filelist = [{'length': torrent_filesize, 'path': [info_dict['name']]}] + else: + # If multi-file, use the directory name as root for files + file_tree_root = parsed_file_tree.setdefault( + info_dict['name'].decode(used_path_encoding), {}) + + # Parse file dicts into a tree + for file_dict in torrent_filelist: + # Decode path parts from utf8-bytes + path_parts = [path_part.decode(used_path_encoding) for path_part in file_dict['path']] + + filename = path_parts.pop() + current_directory = file_tree_root + + for directory in path_parts: + current_directory = current_directory.setdefault(directory, {}) + + # Don't add empty filenames (BitComet directory) + if filename: + current_directory[filename] = file_dict['length'] + + parsed_file_tree = utils.sorted_pathdict(parsed_file_tree) + + json_bytes = json.dumps(parsed_file_tree, separators=(',', ':')).encode('utf8') + torrent.filelist = models.TorrentFilelist(filelist_blob=json_bytes) + + db.session.add(torrent) + db.session.flush() + + # Store the users trackers + trackers = OrderedSet() + announce = torrent_data.torrent_dict.get('announce', b'').decode('ascii') + if announce: + trackers.add(announce) + + # List of lists with single item + announce_list = torrent_data.torrent_dict.get('announce-list', []) + for announce in announce_list: + trackers.add(announce[0].decode('ascii')) + + # Remove our trackers, maybe? TODO ? + + # Search for/Add trackers in DB + db_trackers = OrderedSet() + for announce in trackers: + tracker = models.Trackers.by_uri(announce) + + # Insert new tracker if not found + if not tracker: + tracker = models.Trackers(uri=announce) + db.session.add(tracker) + + db_trackers.add(tracker) + + db.session.flush() + + # Store tracker refs in DB + for order, tracker in enumerate(db_trackers): + torrent_tracker = models.TorrentTrackers(torrent_id=torrent.id, + tracker_id=tracker.id, order=order) + db.session.add(torrent_tracker) + + db.session.commit() + + return 'success' From c2438f39137dd7018c5d15cffae84c8e6d1a0dfb Mon Sep 17 00:00:00 2001 From: aldacron Date: Sun, 21 May 2017 22:10:05 -0700 Subject: [PATCH 09/90] nyaadev is an idiot and broke apiv2 --- nyaa/forms.py | 4 ++++ nyaa/templates/upload.html | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/nyaa/forms.py b/nyaa/forms.py index 1a61706..f21b890 100644 --- a/nyaa/forms.py +++ b/nyaa/forms.py @@ -166,6 +166,10 @@ class EditForm(FlaskForm): class UploadForm(FlaskForm): + + class Meta: + csrf = False + torrent_file = FileField('Torrent file', [ FileRequired() ]) diff --git a/nyaa/templates/upload.html b/nyaa/templates/upload.html index e54beaa..b2df92f 100644 --- a/nyaa/templates/upload.html +++ b/nyaa/templates/upload.html @@ -14,8 +14,6 @@
Drop here!
- {{ upload_form.csrf_token }} - {% if config.ENFORCE_MAIN_ANNOUNCE_URL %}

Important: Please include {{ config.MAIN_ANNOUNCE_URL }} in your trackers

{% endif %}
From 8fc81b395ea98fe9b3432ec86d229d6c96baf978 Mon Sep 17 00:00:00 2001 From: aldacron Date: Sun, 21 May 2017 22:53:28 -0700 Subject: [PATCH 10/90] if is_trusted is not sent and user is trusted, torrent will marked as trusted unless specified. this also enable backward compat of v1 --- nyaa/backend.py | 9 ++++++++- nyaa/templates/home.html | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/nyaa/backend.py b/nyaa/backend.py index b5853d0..b8b9926 100644 --- a/nyaa/backend.py +++ b/nyaa/backend.py @@ -72,7 +72,14 @@ def handle_torrent_upload(upload_form, uploading_user=None, fromAPI=False): torrent.complete = upload_form.is_complete.data # Copy trusted status from user if possible can_mark_trusted = uploading_user and uploading_user.is_trusted - torrent.trusted = upload_form.is_trusted.data if can_mark_trusted else False + # Automatically mark trusted if user is trusted unless user specifies it to not be trusted + if can_mark_trusted: + torrent.trusted = True + if upload_form.is_trusted.data is False: + torrent.trusted = False + else: + torrent.trusted = False + # Set category ids torrent.main_category_id, torrent.sub_category_id = \ upload_form.category.parsed_data.get_category_ids() diff --git a/nyaa/templates/home.html b/nyaa/templates/home.html index 8840da1..95d7f28 100644 --- a/nyaa/templates/home.html +++ b/nyaa/templates/home.html @@ -3,7 +3,7 @@ {% block body %}
-

5/18 Update: We've added an upload api for ease of uploading. See documentation here.

+

5/21 Update: We've updated our upload API to v2 (v1 still works). See documentation here.

5/17 Update: We've added faster and more accurate search! In addition to your typical keyword search in both English and other languages, you can also now use powerful operators like clockwork planet -horrible or commie|horrible|cartel yowamushi to search. For all supported operators, please click here. More features are coming soon!


We welcome you to provide feedback at #nyaa-dev@irc.rizon.net

From ec72d8bf3d11f3637d2ea21768820734404cb16d Mon Sep 17 00:00:00 2001 From: aldacron Date: Sun, 21 May 2017 22:55:42 -0700 Subject: [PATCH 11/90] updated v1 docs --- utils/api_uploader.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/utils/api_uploader.py b/utils/api_uploader.py index ebb426d..5bbb962 100644 --- a/utils/api_uploader.py +++ b/utils/api_uploader.py @@ -23,7 +23,8 @@ The POST payload to the api endpoint (/api/upload) should be multipart/form-data 'is_anonymous': boolean, 'is_hidden': boolean, 'is_remake': boolean, - 'is_complete': boolean + 'is_complete': boolean, + 'is_trusted': boolean #optional }, 'torrent_file': multi part file format @@ -105,6 +106,7 @@ is_anonymous = False is_hidden = False is_remake = False is_complete = False +is_trusted = True # This will only work if a user is trusted, otherwise the option is ignored auth_info = { 'username' : username, From 29e878a1ec6e8290e4865da50b80ad702d7fe842 Mon Sep 17 00:00:00 2001 From: aldacron Date: Mon, 22 May 2017 00:36:01 -0700 Subject: [PATCH 12/90] fixed api_uploader and reverted backend.py change --- nyaa/backend.py | 9 ++------- utils/api_uploader.py | 3 ++- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/nyaa/backend.py b/nyaa/backend.py index b8b9926..a9ea97e 100644 --- a/nyaa/backend.py +++ b/nyaa/backend.py @@ -72,13 +72,8 @@ def handle_torrent_upload(upload_form, uploading_user=None, fromAPI=False): torrent.complete = upload_form.is_complete.data # Copy trusted status from user if possible can_mark_trusted = uploading_user and uploading_user.is_trusted - # Automatically mark trusted if user is trusted unless user specifies it to not be trusted - if can_mark_trusted: - torrent.trusted = True - if upload_form.is_trusted.data is False: - torrent.trusted = False - else: - torrent.trusted = False + # To do, automatically mark trusted if user is trusted unless user specifies otherwise + torrent.trusted = upload_form.is_trusted.data if can_mark_trusted else False # Set category ids torrent.main_category_id, torrent.sub_category_id = \ diff --git a/utils/api_uploader.py b/utils/api_uploader.py index 5bbb962..5119685 100644 --- a/utils/api_uploader.py +++ b/utils/api_uploader.py @@ -121,7 +121,8 @@ metadata={ 'is_anonymous' : is_anonymous, 'is_hidden' : is_hidden, 'is_remake' : is_remake, - 'is_complete' : is_complete + 'is_complete' : is_complete, + 'is_trusted' : is_trusted } files = { From 1016d5be0207cb3d830d3bebb544ecf53c4812be Mon Sep 17 00:00:00 2001 From: katnyaa Date: Mon, 22 May 2017 13:02:02 +0000 Subject: [PATCH 13/90] Fix typo --- nyaa/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nyaa/__init__.py b/nyaa/__init__.py index 7e934cb..98fd995 100644 --- a/nyaa/__init__.py +++ b/nyaa/__init__.py @@ -37,7 +37,7 @@ if not app.config['DEBUG']: def internal_error(exception): app.logger.error(exception) flask.flash(flask.Markup( - 'An error occured! Debugging information has been logged.'), 'danger') + 'An error occurred! Debug information has been logged.'), 'danger') return flask.redirect('/') # Get git commit hash From a1c024a34254626fc6e2226628cf3675becbf350 Mon Sep 17 00:00:00 2001 From: TheAMM Date: Mon, 22 May 2017 16:28:06 +0300 Subject: [PATCH 14/90] Re-enable CSRF token for upload & fix API CSRF handling --- nyaa/api_handler.py | 4 ++-- nyaa/forms.py | 3 --- nyaa/templates/upload.html | 2 ++ 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/nyaa/api_handler.py b/nyaa/api_handler.py index 4ddd510..dd5af3b 100644 --- a/nyaa/api_handler.py +++ b/nyaa/api_handler.py @@ -104,7 +104,7 @@ def api_upload(upload_request, user): return flask.make_response(flask.jsonify( {'Failure': ['No torrent file was attached.']}), 400) - form = forms.UploadForm(CombinedMultiDict((torrent_file, form_info))) + form = forms.UploadForm(CombinedMultiDict((torrent_file, form_info)), csrf_enabled=False) form.category.choices = _create_upload_category_choices() if upload_request.method == 'POST' and form.validate(): @@ -166,7 +166,7 @@ def v2_api_upload(): mapped_dict[mapped_key] = request_data.get(key) or '' # Flask-WTF (very helpfully!!) automatically grabs the request form, so force a None formdata - upload_form = forms.UploadForm(None, data=mapped_dict) + upload_form = forms.UploadForm(None, data=mapped_dict, csrf_enabled=False) upload_form.category.choices = _create_upload_category_choices() if upload_form.validate(): diff --git a/nyaa/forms.py b/nyaa/forms.py index f21b890..f1626d9 100644 --- a/nyaa/forms.py +++ b/nyaa/forms.py @@ -166,9 +166,6 @@ class EditForm(FlaskForm): class UploadForm(FlaskForm): - - class Meta: - csrf = False torrent_file = FileField('Torrent file', [ FileRequired() diff --git a/nyaa/templates/upload.html b/nyaa/templates/upload.html index b2df92f..e54beaa 100644 --- a/nyaa/templates/upload.html +++ b/nyaa/templates/upload.html @@ -14,6 +14,8 @@
Drop here!
+ {{ upload_form.csrf_token }} + {% if config.ENFORCE_MAIN_ANNOUNCE_URL %}

Important: Please include {{ config.MAIN_ANNOUNCE_URL }} in your trackers

{% endif %}
From 0e573783592e3dbf2fc31e1411cd79be716685ee Mon Sep 17 00:00:00 2001 From: TheAMM Date: Mon, 22 May 2017 16:45:00 +0300 Subject: [PATCH 15/90] API: default to trusted, properly pass CSRF to UploadForm --- nyaa/api_handler.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/nyaa/api_handler.py b/nyaa/api_handler.py index dd5af3b..b415242 100644 --- a/nyaa/api_handler.py +++ b/nyaa/api_handler.py @@ -87,11 +87,13 @@ def api_upload(upload_request, user): form_info_as_dict = [] for k, v in form_info.items(): - if k in ['is_anonymous', 'is_hidden', 'is_remake', 'is_complete']: + if k in ['is_anonymous', 'is_hidden', 'is_remake', 'is_complete', 'is_trusted']: if v: form_info_as_dict.append((k, v)) else: form_info_as_dict.append((k, v)) + # Hack for while v1 is still being used: default trusted to true + form_info_as_dict.setdefault('is_trusted', True) form_info = ImmutableMultiDict(form_info_as_dict) except Exception as e: return flask.make_response(flask.jsonify( @@ -104,7 +106,7 @@ def api_upload(upload_request, user): return flask.make_response(flask.jsonify( {'Failure': ['No torrent file was attached.']}), 400) - form = forms.UploadForm(CombinedMultiDict((torrent_file, form_info)), csrf_enabled=False) + form = forms.UploadForm(CombinedMultiDict((torrent_file, form_info)), meta={'csrf':False}) form.category.choices = _create_upload_category_choices() if upload_request.method == 'POST' and form.validate(): @@ -134,17 +136,17 @@ UPLOAD_API_FORM_KEYMAP = { 'is_trusted': 'trusted' } UPLOAD_API_FORM_KEYMAP_REVERSE = {v: k for k, v in UPLOAD_API_FORM_KEYMAP.items()} -UPLOAD_API_KEYS = [ - 'name', - 'category', - 'anonymous', - 'hidden', - 'complete', - 'remake', - 'trusted', - 'information', - 'description' -] +UPLOAD_API_DEFAULTS = { + 'name' : '', + 'category': '', + 'anonymous': False, + 'hidden': False, + 'complete': False, + 'remake': False, + 'trusted': True, + 'information': '', + 'description': '' +} @api_blueprint.route('/v2/upload', methods=['POST']) @@ -161,12 +163,12 @@ def v2_api_upload(): request_data = json.loads(request_data_field) # Map api keys to upload form fields - for key in UPLOAD_API_KEYS: + for key, default in UPLOAD_API_DEFAULTS.items(): mapped_key = UPLOAD_API_FORM_KEYMAP_REVERSE.get(key, key) - mapped_dict[mapped_key] = request_data.get(key) or '' + mapped_dict[mapped_key] = request_data.get(key, default) # Flask-WTF (very helpfully!!) automatically grabs the request form, so force a None formdata - upload_form = forms.UploadForm(None, data=mapped_dict, csrf_enabled=False) + upload_form = forms.UploadForm(None, data=mapped_dict, meta={'csrf':False}) upload_form.category.choices = _create_upload_category_choices() if upload_form.validate(): From 7066fb258e6ad5ade296ecc75bb7b5763c5d4301 Mon Sep 17 00:00:00 2001 From: TheAMM Date: Mon, 22 May 2017 16:54:02 +0300 Subject: [PATCH 16/90] Wait, that was no dict at all! Fix v1 api default --- nyaa/api_handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nyaa/api_handler.py b/nyaa/api_handler.py index b415242..ade1e2b 100644 --- a/nyaa/api_handler.py +++ b/nyaa/api_handler.py @@ -93,7 +93,8 @@ def api_upload(upload_request, user): else: form_info_as_dict.append((k, v)) # Hack for while v1 is still being used: default trusted to true - form_info_as_dict.setdefault('is_trusted', True) + if not [x for x in form_info_as_dict if x[0] == 'is_trusted']: + form_info_as_dict.append(('is_trusted', True)) form_info = ImmutableMultiDict(form_info_as_dict) except Exception as e: return flask.make_response(flask.jsonify( From 159d5a5d9e7339e54b7306f832fdb8c687da8606 Mon Sep 17 00:00:00 2001 From: katnyaa Date: Mon, 22 May 2017 15:03:34 +0100 Subject: [PATCH 17/90] Fix lint errors --- nyaa/api_handler.py | 8 ++++---- nyaa/backend.py | 2 +- nyaa/forms.py | 1 - nyaa/routes.py | 7 +++++-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/nyaa/api_handler.py b/nyaa/api_handler.py index ade1e2b..665511b 100644 --- a/nyaa/api_handler.py +++ b/nyaa/api_handler.py @@ -107,7 +107,7 @@ def api_upload(upload_request, user): return flask.make_response(flask.jsonify( {'Failure': ['No torrent file was attached.']}), 400) - form = forms.UploadForm(CombinedMultiDict((torrent_file, form_info)), meta={'csrf':False}) + form = forms.UploadForm(CombinedMultiDict((torrent_file, form_info)), meta={'csrf': False}) form.category.choices = _create_upload_category_choices() if upload_request.method == 'POST' and form.validate(): @@ -138,7 +138,7 @@ UPLOAD_API_FORM_KEYMAP = { } UPLOAD_API_FORM_KEYMAP_REVERSE = {v: k for k, v in UPLOAD_API_FORM_KEYMAP.items()} UPLOAD_API_DEFAULTS = { - 'name' : '', + 'name': '', 'category': '', 'anonymous': False, 'hidden': False, @@ -169,7 +169,7 @@ def v2_api_upload(): mapped_dict[mapped_key] = request_data.get(key, default) # Flask-WTF (very helpfully!!) automatically grabs the request form, so force a None formdata - upload_form = forms.UploadForm(None, data=mapped_dict, meta={'csrf':False}) + upload_form = forms.UploadForm(None, data=mapped_dict, meta={'csrf': False}) upload_form.category.choices = _create_upload_category_choices() if upload_form.validate(): @@ -193,7 +193,7 @@ def v2_api_upload(): # #################################### TEMPORARY #################################### -from orderedset import OrderedSet +from orderedset import OrderedSet # noqa: E402 @api_blueprint.route('/ghetto_import', methods=['POST']) diff --git a/nyaa/backend.py b/nyaa/backend.py index a9ea97e..1c96095 100644 --- a/nyaa/backend.py +++ b/nyaa/backend.py @@ -73,7 +73,7 @@ def handle_torrent_upload(upload_form, uploading_user=None, fromAPI=False): # Copy trusted status from user if possible can_mark_trusted = uploading_user and uploading_user.is_trusted # To do, automatically mark trusted if user is trusted unless user specifies otherwise - torrent.trusted = upload_form.is_trusted.data if can_mark_trusted else False + torrent.trusted = upload_form.is_trusted.data if can_mark_trusted else False # Set category ids torrent.main_category_id, torrent.sub_category_id = \ diff --git a/nyaa/forms.py b/nyaa/forms.py index f1626d9..1a61706 100644 --- a/nyaa/forms.py +++ b/nyaa/forms.py @@ -166,7 +166,6 @@ class EditForm(FlaskForm): class UploadForm(FlaskForm): - torrent_file = FileField('Torrent file', [ FileRequired() ]) diff --git a/nyaa/routes.py b/nyaa/routes.py index 7dda5e5..013d842 100644 --- a/nyaa/routes.py +++ b/nyaa/routes.py @@ -215,7 +215,9 @@ def home(rss): query_results = search_elastic(**query_args) if render_as_rss: - return render_rss('"{}"'.format(search_term), query_results, use_elastic=True, magnet_links=use_magnet_links) + 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) @@ -637,7 +639,8 @@ def edit_torrent(torrent_id): db.session.commit() flask.flash(flask.Markup( - 'Torrent has been successfully edited! Changes might take a few minutes to show up.'), 'info') + 'Torrent has been successfully edited! Changes might take a few minutes to show up.'), + 'info') return flask.redirect(flask.url_for('view_torrent', torrent_id=torrent.id)) else: From 607d9b2c92b3b52c10f710dc5579ef105be5a252 Mon Sep 17 00:00:00 2001 From: katnyaa Date: Mon, 22 May 2017 15:00:06 +0000 Subject: [PATCH 18/90] Change wording for Downloads heading --- nyaa/templates/view.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nyaa/templates/view.html b/nyaa/templates/view.html index 25d797b..e32fc7d 100644 --- a/nyaa/templates/view.html +++ b/nyaa/templates/view.html @@ -58,7 +58,7 @@
File size:
{{ torrent.filesize | filesizeformat(True) }}
-
Downloads:
+
Completed:
{% if config.ENABLE_SHOW_STATS %}{{ torrent.stats.download_count }}{% else %}Coming soon{% endif %}
From b241dd550888db0ddf89b3ad306ac5a2413d1694 Mon Sep 17 00:00:00 2001 From: A nyaa developer Date: Mon, 22 May 2017 20:08:41 +0200 Subject: [PATCH 19/90] Don't generate empty torrent file for torrents without info dict (has_torrent). --- nyaa/routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nyaa/routes.py b/nyaa/routes.py index 013d842..d7340cb 100644 --- a/nyaa/routes.py +++ b/nyaa/routes.py @@ -679,7 +679,7 @@ def redirect_magnet(torrent_id): def download_torrent(torrent_id): torrent = models.Torrent.by_id(torrent_id) - if not torrent: + if not torrent or not torrent.has_torrent: flask.abort(404) resp = flask.Response(_get_cached_torrent_file(torrent)) From f04c3e1cf3253dcf9a6d1ed2fe8a0bdd58ed5d77 Mon Sep 17 00:00:00 2001 From: TheAMM Date: Mon, 22 May 2017 22:54:33 +0300 Subject: [PATCH 20/90] Remove v1 upload api and update notice --- nyaa/api_handler.py | 86 ++---------------------- nyaa/routes.py | 12 +--- nyaa/templates/home.html | 4 +- utils/api_uploader.py | 137 --------------------------------------- utils/api_uploader_v2.py | 2 +- 5 files changed, 10 insertions(+), 231 deletions(-) delete mode 100644 utils/api_uploader.py diff --git a/nyaa/api_handler.py b/nyaa/api_handler.py index 665511b..1ca6625 100644 --- a/nyaa/api_handler.py +++ b/nyaa/api_handler.py @@ -6,6 +6,9 @@ from nyaa import models, forms from nyaa import bencode, backend, utils from nyaa import torrents +# For _create_upload_category_choices +from nyaa import routes + import functools import json import os.path @@ -42,87 +45,7 @@ def api_require_user(f): return decorator -def validate_user(upload_request): - auth_info = None - try: - 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 - - username = auth_info['username'] - password = auth_info['password'] - user = models.User.by_username(username) - - if not user: - user = models.User.by_email(username) - - if not user or password != user.password_hash or \ - user.status == models.UserStatusType.INACTIVE: - return False, None, None - - return True, user, None - else: - return False, None, None - - except Exception as e: - return False, None, e - - -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 - - # #################################### 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')) - - form_info_as_dict = [] - for k, v in form_info.items(): - if k in ['is_anonymous', 'is_hidden', 'is_remake', 'is_complete', 'is_trusted']: - if v: - form_info_as_dict.append((k, v)) - else: - form_info_as_dict.append((k, v)) - # Hack for while v1 is still being used: default trusted to true - if not [x for x in form_info_as_dict if x[0] == 'is_trusted']: - form_info_as_dict.append(('is_trusted', True)) - form_info = ImmutableMultiDict(form_info_as_dict) - except Exception as e: - return flask.make_response(flask.jsonify( - {'Failure': ['Invalid data. See HELP in api_uploader.py']}), 400) - - try: - torrent_file = upload_request.files['torrent_file'] - torrent_file = ImmutableMultiDict([('torrent_file', torrent_file)]) - except Exception as e: - return flask.make_response(flask.jsonify( - {'Failure': ['No torrent file was attached.']}), 400) - - form = forms.UploadForm(CombinedMultiDict((torrent_file, form_info)), meta={'csrf': False}) - form.category.choices = _create_upload_category_choices() - - if upload_request.method == 'POST' and form.validate(): - torrent = backend.handle_torrent_upload(form, user, True) - - return flask.make_response(flask.jsonify({'Success': int('{0}'.format(torrent.id))}), 200) - else: - return_error_messages = [] - for error_name, error_messages in form.errors.items(): - return_error_messages.extend(error_messages) - - return flask.make_response(flask.jsonify({'Failure': return_error_messages}), 400) - -# V2 below - # Map UploadForm fields to API keys UPLOAD_API_FORM_KEYMAP = { @@ -150,6 +73,7 @@ UPLOAD_API_DEFAULTS = { } +@api_blueprint.route('/upload', methods=['POST']) @api_blueprint.route('/v2/upload', methods=['POST']) @basic_auth_user @api_require_user @@ -170,7 +94,7 @@ def v2_api_upload(): # Flask-WTF (very helpfully!!) automatically grabs the request form, so force a None formdata upload_form = forms.UploadForm(None, data=mapped_dict, meta={'csrf': False}) - upload_form.category.choices = _create_upload_category_choices() + upload_form.category.choices = routes._create_upload_category_choices() if upload_form.validate(): torrent = backend.handle_torrent_upload(upload_form, flask.g.user) diff --git a/nyaa/routes.py b/nyaa/routes.py index d7340cb..cc2c508 100644 --- a/nyaa/routes.py +++ b/nyaa/routes.py @@ -133,9 +133,6 @@ def get_category_id_map(): # Routes start here # -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. @@ -767,10 +764,5 @@ def site_help(): # #################################### API ROUTES #################################### -@app.route('/api/upload', methods=['POST']) -def api_upload(): - is_valid_user, user, debug = api_handler.validate_user(flask.request) - if not is_valid_user: - return flask.make_response(flask.jsonify({"Failure": "Invalid username or password."}), 400) - api_response = api_handler.api_upload(flask.request, user) - return api_response + +app.register_blueprint(api_handler.api_blueprint, url_prefix='/api') diff --git a/nyaa/templates/home.html b/nyaa/templates/home.html index 95d7f28..0e5a06c 100644 --- a/nyaa/templates/home.html +++ b/nyaa/templates/home.html @@ -3,8 +3,8 @@ {% block body %}
-

5/21 Update: We've updated our upload API to v2 (v1 still works). See documentation here.

-

5/17 Update: We've added faster and more accurate search! In addition to your typical keyword search in both English and other languages, you can also now use powerful operators +

2017-05-22 Update: We've updated our upload API to v2 (v1 is now disabled!). See documentation here.

+

2017-05-17 Update: We've added faster and more accurate search! In addition to your typical keyword search in both English and other languages, you can also now use powerful operators like clockwork planet -horrible or commie|horrible|cartel yowamushi to search. For all supported operators, please click here. More features are coming soon!


We welcome you to provide feedback at #nyaa-dev@irc.rizon.net

Our GitHub: https://github.com/nyaadevs - creating issues for features and faults is recommendable!

diff --git a/utils/api_uploader.py b/utils/api_uploader.py deleted file mode 100644 index 5119685..0000000 --- a/utils/api_uploader.py +++ /dev/null @@ -1,137 +0,0 @@ -# Uploads a single torrent file -# Works on nyaa.si and sukebei.nyaa.si - -# Consider using api_uploader_v2.py instead -# It has a nice command line interface - -import json -import requests - -''' -The POST payload to the api endpoint (/api/upload) should be multipart/form-data containing three fields - -'auth_info': file containing "{ - 'username': str, - 'password': str -}", - -'torrent_info': { - 'category': str, # see below - 'display_name': str, # optional - 'information': str, - 'description': str, - 'is_anonymous': boolean, - 'is_hidden': boolean, - 'is_remake': boolean, - 'is_complete': boolean, - 'is_trusted': boolean #optional -}, - -'torrent_file': multi part file format - - -A successful request should return {'Success': int(torrent_id)} -A failed request should return {'Failure': ["Failure 1", "Failure 2"...]]} - -''' - -# ########################################### HELP ############################################ -# ################################# CATEGORIES MUST BE EXACT ################################## -''' -# Nyaa categories only for now, but api still works for sukebei - -Anime - Anime - AMV : '1_1' - Anime - English : '1_2' - Anime - Non-English : '1_3' - Anime - Raw : '1_4' -Audio - Lossless : '2_1' - Lossy : '2_2' -Literature - Literature - English-translated : '3_1' - Literature - Non-English : '3_2' - Literature - Non-English-Translated : '3_3' - Literature - Raw : '3_4' -Live Action - Live Action - English-translated : '4_1' - Live Action - Idol/Promotional Video : '4_2' - Live Action - Non-English-translated : '4_3' - Live Action - Raw : '4_4' -Pictures - Pictures - Graphics : '5_1' - Pictures - Photos : '5_2' -Software - Software - Applications : '6_1' - Software - Games : '6_2' -''' -# ################################# CATEGORIES MUST BE EXACT ################################## - -# ###################################### EXAMPLE REQUEST ###################################### -''' -# Required -username = '' -password = '' -torrent_file = '/path/to/my.torrent' -category = '1_2' - -#Optional -display_name = '' -information = 'API HOWTO' -description = 'Visit #nyaa-dev@irc.rizon.net' - -# Defaults to False, change to True to set -is_anonymous : False, -is_hidden : False, -is_remake : False, -is_complete : False -''' - - -# ######################################## CHANGE HERE ######################################## - -url = 'https://nyaa.si/api/upload' # or 'https://sukebei.nyaa.si/api/upload' or 'http://127.0.0.1:5500/api/upload' - -# Required -username = '' -password = '' -torrent_file = '' -category = '' - -# Optional -display_name = '' -information = '' -description = '' -is_anonymous = False -is_hidden = False -is_remake = False -is_complete = False -is_trusted = True # This will only work if a user is trusted, otherwise the option is ignored - -auth_info = { - 'username' : username, - 'password' : password -} - -metadata={ - 'category' : category, - 'display_name' : display_name, - 'information' : information, - 'description' : description, - 'is_anonymous' : is_anonymous, - 'is_hidden' : is_hidden, - 'is_remake' : is_remake, - 'is_complete' : is_complete, - 'is_trusted' : is_trusted -} - -files = { - 'auth_info' : (json.dumps(auth_info)), - 'torrent_info' : (json.dumps(metadata)), - 'torrent_file' : ('{0}'.format(torrent_file), open(torrent_file, 'rb'), 'application/octet-stream') -} - -response = requests.post(url, files=files) -json_response = response.json() -print(json_response) -# A successful request should print {'Success': int(torrent_id)} diff --git a/utils/api_uploader_v2.py b/utils/api_uploader_v2.py index 60d3c36..6956d81 100755 --- a/utils/api_uploader_v2.py +++ b/utils/api_uploader_v2.py @@ -9,7 +9,7 @@ NYAA_HOST = 'https://nyaa.si' SUKEBEI_HOST = 'https://sukebei.nyaa.si' API_BASE = '/api' -API_UPLOAD = API_BASE + '/v2/upload' +API_UPLOAD = API_BASE + '/upload' NYAA_CATS = '''1_1 - Anime - AMV 1_2 - Anime - English From 1e230584ab951142be968f20ea4256e8a4adf7ed Mon Sep 17 00:00:00 2001 From: TheAMM Date: Mon, 22 May 2017 23:59:21 +0300 Subject: [PATCH 21/90] Re-add generated torrent tracker count limit As found out in #164, this feature disappeared somewhere along the line. --- nyaa/torrents.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nyaa/torrents.py b/nyaa/torrents.py index eff6f54..648683f 100644 --- a/nyaa/torrents.py +++ b/nyaa/torrents.py @@ -11,6 +11,8 @@ from nyaa import models USED_TRACKERS = OrderedSet() +# Limit the amount of trackers added into .torrent files +MAX_TRACKERS = 5 def read_trackers_from_file(file_object): USED_TRACKERS.clear() @@ -116,7 +118,7 @@ def create_default_metadata_base(torrent, trackers=None): metadata_base['announce'] = trackers[0] if len(trackers) > 1: # Yes, it's a list of lists with a single element inside. - metadata_base['announce-list'] = [[tracker] for tracker in trackers] + metadata_base['announce-list'] = [[tracker] for tracker in trackers[:MAX_TRACKERS]] return metadata_base From 6d608ab2f480b3778812bdaec4aa6cdf1e1b8d68 Mon Sep 17 00:00:00 2001 From: Sn0wCrack Date: Sat, 13 May 2017 22:24:42 +1000 Subject: [PATCH 22/90] Added comments --- nyaa/forms.py | 7 ++++++ nyaa/models.py | 19 ++++++++++++++- nyaa/routes.py | 32 +++++++++++++++++++++++++ nyaa/templates/view.html | 51 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 1 deletion(-) diff --git a/nyaa/forms.py b/nyaa/forms.py index 1a61706..a440092 100644 --- a/nyaa/forms.py +++ b/nyaa/forms.py @@ -126,6 +126,13 @@ class DisabledSelectField(SelectField): raise ValueError(self.gettext('Not a valid choice')) +class CommentForm(FlaskForm): + comment = TextAreaField('Make a comment', [ + Length(max=255, message='Comment must be at most %(max)d characters long.'), + Required() + ]) + + class EditForm(FlaskForm): display_name = StringField('Torrent display name', [ Length(min=3, max=255, diff --git a/nyaa/models.py b/nyaa/models.py index 07f75da..1cb8824 100644 --- a/nyaa/models.py +++ b/nyaa/models.py @@ -317,6 +317,22 @@ class SubCategory(db.Model): return cls.query.get((sub_cat_id, main_cat_id)) +class Comment(db.Model): + __tablename__ = DB_TABLE_PREFIX + 'comments' + + id = db.Column(db.Integer, primary_key=True) + torrent = db.Column(db.Integer, db.ForeignKey( + DB_TABLE_PREFIX + 'torrents.id'), primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey( + 'users.id', ondelete='CASCADE')) + text = db.Column(db.String(length=255), nullable=False) + + user = db.relationship('User', uselist=False, back_populates='comments') + + def __repr__(self): + return '' % self.id + + class UserLevelType(IntEnum): REGULAR = 0 TRUSTED = 1 @@ -346,7 +362,8 @@ class User(db.Model): last_login_date = db.Column(db.DateTime(timezone=False), default=None, nullable=True) last_login_ip = db.Column(db.Binary(length=16), default=None, nullable=True) - torrents = db.relationship('Torrent', back_populates='user', lazy="dynamic") + torrents = db.relationship('Torrent', back_populates='user', lazy='dynamic') + comments = db.relationship('Comment', back_populates='user', lazy='dynamic') # session = db.relationship('Session', uselist=False, back_populates='user') def __init__(self, username, email, password): diff --git a/nyaa/routes.py b/nyaa/routes.py index cc2c508..bc43e4c 100644 --- a/nyaa/routes.py +++ b/nyaa/routes.py @@ -573,6 +573,7 @@ def upload(): @app.route('/view/') def view_torrent(torrent_id): torrent = models.Torrent.by_id(torrent_id) + form = forms.CommentForm() viewer = flask.g.user @@ -590,12 +591,43 @@ def view_torrent(torrent_id): if torrent.filelist: files = json.loads(torrent.filelist.filelist_blob.decode('utf-8')) + if flask.g.user is not None and flask.g.user.is_admin: + comments = models.Comment.query.filter(models.Comment.torrent == torrent_id) + else: + comments = models.Comment.query.filter(models.Comment.torrent == torrent_id, + models.Comment.deleted == False) + + comment_count = comments.count() + return flask.render_template('view.html', torrent=torrent, files=files, viewer=viewer, + form=form, + comments=comments, + comment_count=comment_count, can_edit=can_edit) +@app.route('/view//submit_comment', methods=['POST']) +def submit_comment(torrent_id): + form = forms.CommentForm(flask.request.form) + + if flask.request.method == 'POST' and form.validate(): + comment_text = (form.comment.data or '').strip() + + # Null entry for User just means Anonymous + current_user_id = flask.g.user.id if flask.g.user else None + comment = models.Comment( + torrent=torrent_id, + user_id=current_user_id, + text=comment_text) + + db.session.add(comment) + db.session.commit() + + return flask.redirect(flask.url_for('view_torrent', torrent_id=torrent_id)) + + @app.route('/view//edit', methods=['GET', 'POST']) def edit_torrent(torrent_id): torrent = models.Torrent.by_id(torrent_id) diff --git a/nyaa/templates/view.html b/nyaa/templates/view.html index e32fc7d..658402e 100644 --- a/nyaa/templates/view.html +++ b/nyaa/templates/view.html @@ -1,6 +1,7 @@ {% extends "layout.html" %} {% block title %}{{ torrent.display_name }} :: {{ config.SITE_NAME }}{% endblock %} {% block body %} +{% from "_formhelpers.html" import render_field %}

@@ -129,6 +130,56 @@

{% endif %} +
+
+

+ Comments - {{ comment_count }} +

+
+
+ + + {% if g.user.is_admin %} + + {% endif %} + + + + + {% for comment in comments %} + + {% if g.user.is_admin %} + + {% endif %} + + + + {% endfor %} + +
DeleteUserComment
+ {% if not comment.deleted %} + + {% else %} + + {% endif %} + + {% if comment.user %} + + {{ comment.user.username }} + + {% else %} + Anonymous + {% endif %} + {{ comment.text }}
+
+
+ + + {{ form.csrf_token }} + {{ render_field(form.comment, class_='form-control') }} + + + + {% endfor %}
-
+{% if g.user %} + {{ form.csrf_token }} {{ render_field(form.comment, class_='form-control') }} - {{ render_field(form.is_anonymous) }}
+{% endif %} {% endfor %} + {% if g.user %} +
+ {{ form.csrf_token }} + {{ render_field(form.comment, class_='form-control') }} + +
+ {% endif %}
-{% if g.user %} -
- {{ form.csrf_token }} - {{ render_field(form.comment, class_='form-control') }} - -
-{% endif %} - {% endfor %} - {% if g.user %} -
- {{ form.csrf_token }} - {{ render_field(form.comment, class_='form-control') }} + {% if comment_form %} + + {{ comment_form.csrf_token }} + {{ render_field(comment_form.comment, class_='form-control') }}
{% endif %} diff --git a/nyaa/torrents.py b/nyaa/torrents.py index 648683f..3a466a9 100644 --- a/nyaa/torrents.py +++ b/nyaa/torrents.py @@ -14,6 +14,7 @@ USED_TRACKERS = OrderedSet() # Limit the amount of trackers added into .torrent files MAX_TRACKERS = 5 + def read_trackers_from_file(file_object): USED_TRACKERS.clear() From 3fc347d049f9d7839a440fa040c9a67395c45026 Mon Sep 17 00:00:00 2001 From: nyaadev Date: Tue, 23 May 2017 00:04:27 +0200 Subject: [PATCH 31/90] Move posting comments to view_torrent to fix displaying form errors. --- nyaa/routes.py | 66 +++++++++++++++++++--------------------- nyaa/templates/view.html | 2 +- 2 files changed, 33 insertions(+), 35 deletions(-) diff --git a/nyaa/routes.py b/nyaa/routes.py index ecc3df9..0168b2a 100644 --- a/nyaa/routes.py +++ b/nyaa/routes.py @@ -571,13 +571,16 @@ def upload(): return flask.render_template('upload.html', upload_form=upload_form), status_code -@app.route('/view/') +@app.route('/view/', methods=['GET', 'POST']) def view_torrent(torrent_id): - torrent = models.Torrent.query \ - .options(joinedload('filelist'), - joinedload('comments')) \ - .filter_by(id=torrent_id) \ - .first() + if flask.request.method == 'POST': + torrent = models.Torrent.by_id(torrent_id) + else: + torrent = models.Torrent.query \ + .options(joinedload('filelist'), + joinedload('comments')) \ + .filter_by(id=torrent_id) \ + .first() if not torrent: flask.abort(404) @@ -585,6 +588,29 @@ def view_torrent(torrent_id): if torrent.deleted and not (flask.g.user and flask.g.user.is_moderator): flask.abort(404) + comment_form = None + if flask.g.user: + comment_form = forms.CommentForm() + + if flask.request.method == 'POST': + if not flask.g.user: + 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.commit() + + flask.flash('Comment successfully posted.', 'success') + + return flask.redirect(flask.url_for('view_torrent', torrent_id=torrent_id)) + # 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) @@ -592,10 +618,6 @@ def view_torrent(torrent_id): if torrent.filelist: files = json.loads(torrent.filelist.filelist_blob.decode('utf-8')) - comment_form = None - if flask.g.user: - comment_form = forms.CommentForm() - return flask.render_template('view.html', torrent=torrent, files=files, comment_form=comment_form, @@ -603,30 +625,6 @@ def view_torrent(torrent_id): can_edit=can_edit) -@app.route('/view//comment', methods=['POST']) -def submit_comment(torrent_id): - if not flask.g.user: - flask.abort(403) - - torrent = models.Torrent.by_id(torrent_id) - if not torrent: - flask.abort(404) - - form = forms.CommentForm(flask.request.form) - if form.validate(): - comment_text = (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.commit() - - return flask.redirect(flask.url_for('view_torrent', torrent_id=torrent_id)) - - @app.route('/view//comment//delete', methods=['POST']) def delete_comment(torrent_id, comment_id): if not flask.g.user: diff --git a/nyaa/templates/view.html b/nyaa/templates/view.html index a2d7c7e..3494703 100644 --- a/nyaa/templates/view.html +++ b/nyaa/templates/view.html @@ -176,7 +176,7 @@ {% endfor %} {% if comment_form %} -
+ {{ comment_form.csrf_token }} {{ render_field(comment_form.comment, class_='form-control') }} From eb0fdbfef559ac21f3dcd00932f6d977027339cc Mon Sep 17 00:00:00 2001 From: snowfag Date: Mon, 22 May 2017 18:32:48 -0400 Subject: [PATCH 32/90] update message. --- nyaa/templates/home.html | 1 + 1 file changed, 1 insertion(+) diff --git a/nyaa/templates/home.html b/nyaa/templates/home.html index 0e5a06c..55da9c7 100644 --- a/nyaa/templates/home.html +++ b/nyaa/templates/home.html @@ -3,6 +3,7 @@ {% block body %}
+

2017-05-22 Update: We've added comments. You can change your avatar using Gravatar or if you don't like gravatar you can just stick with our spify default avatar.

2017-05-22 Update: We've updated our upload API to v2 (v1 is now disabled!). See documentation here.

2017-05-17 Update: We've added faster and more accurate search! In addition to your typical keyword search in both English and other languages, you can also now use powerful operators like clockwork planet -horrible or commie|horrible|cartel yowamushi to search. For all supported operators, please click here. More features are coming soon!


From 8ef6e915daab83c6d3590035dda3a5562cea1b0a Mon Sep 17 00:00:00 2001 From: nyaadev Date: Tue, 23 May 2017 00:36:53 +0200 Subject: [PATCH 33/90] shameful late edit: change comment text collation to utf8mb4 --- migrations/versions/d0eeb8049623_add_comments.py | 4 ++-- nyaa/models.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/migrations/versions/d0eeb8049623_add_comments.py b/migrations/versions/d0eeb8049623_add_comments.py index 4b8599a..f6fab5b 100644 --- a/migrations/versions/d0eeb8049623_add_comments.py +++ b/migrations/versions/d0eeb8049623_add_comments.py @@ -23,7 +23,7 @@ def upgrade(): sa.Column('torrent_id', sa.Integer(), nullable=False), sa.Column('user_id', sa.Integer(), nullable=True), sa.Column('created_time', sa.DateTime(), nullable=True), - sa.Column('text', sa.String(length=255), nullable=False), + sa.Column('text', sa.String(length=255, collation='utf8mb4_bin'), nullable=False), sa.ForeignKeyConstraint(['torrent_id'], ['nyaa_torrents.id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') @@ -33,7 +33,7 @@ def upgrade(): sa.Column('torrent_id', sa.Integer(), nullable=False), sa.Column('user_id', sa.Integer(), nullable=True), sa.Column('created_time', sa.DateTime(), nullable=True), - sa.Column('text', sa.String(length=255), nullable=False), + sa.Column('text', sa.String(length=255, collation='utf8mb4_bin'), nullable=False), sa.ForeignKeyConstraint(['torrent_id'], ['sukebei_torrents.id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') diff --git a/nyaa/models.py b/nyaa/models.py index e623aa8..aed2f6a 100644 --- a/nyaa/models.py +++ b/nyaa/models.py @@ -329,7 +329,7 @@ class Comment(db.Model): DB_TABLE_PREFIX + 'torrents.id', ondelete='CASCADE'), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE')) created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow) - text = db.Column(db.String(length=255), nullable=False) + text = db.Column(db.String(length=255, collation=COL_UTF8MB4_BIN), nullable=False) user = db.relationship('User', uselist=False, back_populates='comments', lazy="joined") From 76812b0ef087fc197f092c53fe539ddbebb68671 Mon Sep 17 00:00:00 2001 From: nyaadev Date: Tue, 23 May 2017 00:46:26 +0200 Subject: [PATCH 34/90] utf8mb4 for database connection --- config.example.py | 2 +- nyaa/__init__.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/config.example.py b/config.example.py index 73702b9..3b07da0 100644 --- a/config.example.py +++ b/config.example.py @@ -11,7 +11,7 @@ ENABLE_SHOW_STATS = False BASE_DIR = os.path.abspath(os.path.dirname(__file__)) if USE_MYSQL: - SQLALCHEMY_DATABASE_URI = ('mysql://test:test123@localhost/nyaav2') + SQLALCHEMY_DATABASE_URI = ('mysql://test:test123@localhost/nyaav2?charset=utf8mb4') else: SQLALCHEMY_DATABASE_URI = ( 'sqlite:///' + os.path.join(BASE_DIR, 'test.db') + '?check_same_thread=False') diff --git a/nyaa/__init__.py b/nyaa/__init__.py index 98fd995..d8c04bf 100644 --- a/nyaa/__init__.py +++ b/nyaa/__init__.py @@ -12,6 +12,7 @@ app.config.from_object('config') # Database app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +app.config['MYSQL_DATABASE_CHARSET'] = 'utf8mb4' # Don't refresh cookie each request app.config['SESSION_REFRESH_EACH_REQUEST'] = False From f76593a3fba5ca77b386f1af20e3b6b612765a41 Mon Sep 17 00:00:00 2001 From: snowfag Date: Mon, 22 May 2017 19:08:08 -0400 Subject: [PATCH 35/90] Proper sized default avatar. --- nyaa/static/img/avatar/default.png | Bin 6793 -> 4865 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/nyaa/static/img/avatar/default.png b/nyaa/static/img/avatar/default.png index 7cde0a66cf181375c1ec3b3630c4a018fe611845..7fbef132e2918f6856b170dc2437ba1f8a806cc9 100644 GIT binary patch literal 4865 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 literal 6793 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 From af61e5b1e9ec39bd9a0487dfe076c66480a0330b Mon Sep 17 00:00:00 2001 From: nyaadev Date: Tue, 23 May 2017 02:47:20 +0200 Subject: [PATCH 36/90] some html + css changes around comments --- nyaa/models.py | 9 +++++++++ nyaa/routes.py | 2 +- nyaa/static/css/main.css | 25 ++++++++++++++++++++----- nyaa/templates/layout.html | 2 +- nyaa/templates/view.html | 17 ++++++++--------- 5 files changed, 39 insertions(+), 16 deletions(-) diff --git a/nyaa/models.py b/nyaa/models.py index aed2f6a..e91a432 100644 --- a/nyaa/models.py +++ b/nyaa/models.py @@ -414,6 +414,15 @@ class User(db.Model): elif self.level >= UserLevelType.MODERATOR: return 'Moderator' + @property + def userlevel_color(self): + if self.level == UserLevelType.REGULAR: + return 'default' + elif self.level == UserLevelType.TRUSTED: + return 'success' + elif self.level >= UserLevelType.MODERATOR: + return 'purple' + @property def ip_string(self): if self.last_login_ip: diff --git a/nyaa/routes.py b/nyaa/routes.py index 0168b2a..db38eb8 100644 --- a/nyaa/routes.py +++ b/nyaa/routes.py @@ -824,7 +824,7 @@ def timesince(dt, default='just now'): for period, singular, plural in periods: if period >= 1: - return '%d %s ago' % (period, singular if period == 1 else plural) + return '%d %s ago' % (period, singular if int(period) == 1 else plural) return default diff --git a/nyaa/static/css/main.css b/nyaa/static/css/main.css index fdca7db..fbc8366 100644 --- a/nyaa/static/css/main.css +++ b/nyaa/static/css/main.css @@ -219,18 +219,33 @@ ul.nav-tabs#profileTabs { margin-bottom: 15px; } -.comments-panel { +.comment-panel { width: 99%; margin: 0 auto; - margin-top:10px; - margin-bottom:10px; + margin-top: 10px; + margin-bottom: 10px; +} +.comment-panel:target { + border-color: black; + border-width: 2px; +} + +.text-purple, a.text-purple:visited { color: #a760bc; } +a.text-purple:hover, a.text-purple:active, a.text-purple:focus { color: #a760e0; } + +.comment-content { + word-break: break-all; +} +.comment-content img { + max-width: 100%; + max-height: 600px; } .comment-box { width: 95%; margin: 0 auto; - margin-top:30px; - margin-bottom:10px; + margin-top: 30px; + margin-bottom: 10px; } .delete-comment-form { diff --git a/nyaa/templates/layout.html b/nyaa/templates/layout.html index 46efe48..72290cb 100644 --- a/nyaa/templates/layout.html +++ b/nyaa/templates/layout.html @@ -27,7 +27,7 @@ - + diff --git a/nyaa/templates/view.html b/nyaa/templates/view.html index 3494703..17d36ba 100644 --- a/nyaa/templates/view.html +++ b/nyaa/templates/view.html @@ -100,7 +100,8 @@ {%- if value is iterable %} -   {{ key }} +   {{ key }} + {{ loop(value.items()) }} {%- else %} @@ -133,35 +134,33 @@

- Comments - {{ comments|length }} + Comments - {{ comments | length }}

{% for comment in comments %} -
+

- {% set user_url = torrent.user and url_for('view_user', user_name=comment.user.username) %} - {{ comment.user.username }} + {{ comment.user.username }} {% if comment.user.id == torrent.uploader_id and not torrent.anonymous %} (uploader) {% endif %}

-

{{ comment.user.userlevel_str }}

- {{ comment.created_time | timesince }} + {{ comment.created_time | timesince }} {% if g.user.is_moderator or g.user.id == comment.user_id %} - + {% endif %}
{# Escape newlines into html entities because CF strips blank newlines #} -
{{ comment.text }}
+
{{ comment.text }}
From f32d1a0c645caea614b6d2b3ca83229cb09ca1fe Mon Sep 17 00:00:00 2001 From: nyaadev Date: Tue, 23 May 2017 02:59:31 +0200 Subject: [PATCH 37/90] use enumerated index for comment anchors --- nyaa/templates/view.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nyaa/templates/view.html b/nyaa/templates/view.html index 17d36ba..93e2a4b 100644 --- a/nyaa/templates/view.html +++ b/nyaa/templates/view.html @@ -138,7 +138,7 @@
{% for comment in comments %} -
+

@@ -151,7 +151,7 @@

- {{ comment.created_time | timesince }} + {{ comment.created_time | timesince }} {% if g.user.is_moderator or g.user.id == comment.user_id %}
From ad8a5c0d1c0998d808c4af3745cfc9e5c83f547b Mon Sep 17 00:00:00 2001 From: snowfag Date: Tue, 23 May 2017 01:17:18 -0400 Subject: [PATCH 38/90] Update buttons and css. --- nyaa/static/css/main.css | 14 +++++++ nyaa/templates/edit.html | 77 +++++++++++++++++++++----------------- nyaa/templates/upload.html | 18 +++++---- 3 files changed, 67 insertions(+), 42 deletions(-) mode change 100644 => 100755 nyaa/static/css/main.css mode change 100644 => 100755 nyaa/templates/edit.html mode change 100644 => 100755 nyaa/templates/upload.html diff --git a/nyaa/static/css/main.css b/nyaa/static/css/main.css old mode 100644 new mode 100755 index fbc8366..fc54a47 --- a/nyaa/static/css/main.css +++ b/nyaa/static/css/main.css @@ -256,3 +256,17 @@ a.text-purple:hover, a.text-purple:active, a.text-purple:focus { color: #a760e0; .avatar { max-width: 120px; } + +.btn-grey { + color: #000000; + background-color: #cccfd2; + border-color: #ccc; +} +.btn-grey:hover, .btn-grey:focus, .btn-grey:active, .btn-grey.active, .open > .dropdown-toggle.btn-grey { + background-color: #aaaaaa; +} +.btn span.glyphicon { + opacity: 0; +} +.btn.active span.glyphicon { + opacity: 1; diff --git a/nyaa/templates/edit.html b/nyaa/templates/edit.html old mode 100644 new mode 100755 index e608f8d..f95cb83 --- a/nyaa/templates/edit.html +++ b/nyaa/templates/edit.html @@ -29,41 +29,48 @@ {{ render_field(form.information, class_='form-control', placeholder='Your website or IRC channel') }}
- -
- {% if g.user.is_moderator %} - - {% endif %} - - - - - - {# Only allow changing anonymous status when an uploader exists #} - {% if torrent.uploader_id %} - - {% endif %} - {% if g.user.is_trusted %} - - {% endif %} +
+
+ {# Only allow changing anonymous status when an uploader exists #} + {% if torrent.uploader_id %} + + {% endif %} + + + + {% if g.user.is_trusted %} + + {% endif %} +
+

+
+ {% if g.user.is_moderator %} + + {% endif %}
diff --git a/nyaa/templates/upload.html b/nyaa/templates/upload.html old mode 100644 new mode 100755 index e54beaa..74cd39b --- a/nyaa/templates/upload.html +++ b/nyaa/templates/upload.html @@ -38,31 +38,35 @@ {{ render_field(upload_form.information, class_='form-control', placeholder='Your website or IRC channel') }}
- -
-
From 517f5c48e66cb3ecc125fc2f1ac4a1a2c1ecd8f1 Mon Sep 17 00:00:00 2001 From: katnyaa Date: Tue, 23 May 2017 11:55:22 +0000 Subject: [PATCH 39/90] readme: Clean up setup instructions --- README.md | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index c2471a8..ab46758 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,6 @@ # NyaaV2 -## Setup: - -- Create your virtualenv, for example with `pyvenv venv` -- Enter your virtualenv with `source venv/bin/activate` -- Install dependencies with `pip install -r requirements.txt` -- Run `python db_create.py` to create the database -- Start the dev server with `python run.py` - -## Updated Setup (python 3.6.1): +## Setup - Install dependencies https://github.com/pyenv/pyenv/wiki/Common-build-problems - Install `pyenv` https://github.com/pyenv/pyenv/blob/master/README.md#installation @@ -20,7 +12,7 @@ - Copy `config.example.py` into `config.py` - Change TABLE_PREFIX to `nyaa_` or `sukebei_` depending on the site -## Setting up MySQL/MariaDB database for advanced functionality +### Setting up MySQL/MariaDB database for advanced functionality - Enable `USE_MYSQL` flag in config.py - Install latest mariadb by following instructions here https://downloads.mariadb.org/mariadb/repositories/ - Tested versions: `mysql Ver 15.1 Distrib 10.0.30-MariaDB, for debian-linux-gnu (x86_64) using readline 5.2` @@ -35,18 +27,18 @@ - `CREATE DATABASE nyaav2 DEFAULT CHARACTER SET utf8 COLLATE utf8_bin;` - `SOURCE ~/path/to/database/nyaa_maria_vx.sql` -## Finishing up +### Finishing up - Run `python db_create.py` to create the database - Load the .sql file - `mysql -u user -p nyaav2` - `SOURCE cocks.sql` - Remember to change the default user password to an empty string to disable logging in - Start the dev server with `python run.py` -- Deactivate `source deactivate` +- When you are finished developing, deactivate your virtualenv with `source deactivate` -# Enabling ElasticSearch +## Enabling ElasticSearch -## Basics +### Basics - Install jdk `sudo apt-get install openjdk-8-jdk` - Install elasticsearch https://www.elastic.co/guide/en/elasticsearch/reference/current/deb.html - `sudo systemctl enable elasticsearch.service` @@ -54,7 +46,7 @@ - Run `curl -XGET 'localhost:9200'` and make sure ES is running - Optional: install Kabana as a search frontend for ES -## Enable MySQL Binlogging +### Enable MySQL Binlogging - Add the `[mariadb]` bin-log section to my.cnf and reload mysql server - Connect to mysql - `SHOW VARIABLES LIKE 'binlog_format';` @@ -62,7 +54,7 @@ - Connect to root user - `GRANT REPLICATION SLAVE ON *.* TO 'test'@'localhost';` where test is the user you will be running `sync_es.py` with -## Setting up ES +### Setting up ES - Run `./create_es.sh` and this creates two indicies: `nyaa` and `sukebei` - The output should show `acknowledged: true` twice - The safest bet is to disable the webapp here to ensure there's no database writes @@ -70,7 +62,7 @@ - Run `python import_to_es.py` with `SITE_FLAVOR` set to `sukebei` - These will take some time to run as it's indexing -## Setting up sync_es.py +### Setting up sync_es.py - Sync_es.py keeps the ElasticSearch index updated by reading the BinLog - Configure the MySQL options with the user where you granted the REPLICATION permissions - Connect to MySQL, run `SHOW MASTER STATUS;`. @@ -78,15 +70,13 @@ - Set up `sync_es.py` as a service and run it, preferably as the system/root - Make sure `sync_es.py` runs within venv with the right dependencies +Enable the `USE_ELASTIC_SEARCH` flag in `config.py`, restart the application, and you're good to go. + ## Database migrations - Uses [flask-Migrate](https://flask-migrate.readthedocs.io/) - Run `./db_migrate.py db migrate` to generate the migration script after database model changes. - Take a look at the result in `migrations/versions/...` to make sure nothing went wrong. - Run `./db_migrate.py db upgrade` to upgrade your database. -## Good to go! -- After that, enable the `USE_ELASTIC_SEARCH` flag and restart the webapp and you're good to go - - ## Code Quality: - Remember to follow PEP8 style guidelines and run `./lint.sh` before committing. From aa0d7f702f5bdb9c8a1a1e3782e8b362a9b40c8b Mon Sep 17 00:00:00 2001 From: katnyaa Date: Tue, 23 May 2017 13:19:07 +0100 Subject: [PATCH 40/90] api: Invalid JSON is a client-side error --- nyaa/api_handler.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nyaa/api_handler.py b/nyaa/api_handler.py index 1ca6625..c6087b4 100644 --- a/nyaa/api_handler.py +++ b/nyaa/api_handler.py @@ -85,7 +85,11 @@ def v2_api_upload(): request_data_field = flask.request.form.get('torrent_data') if request_data_field is None: return flask.jsonify({'errors': ['missing torrent_data field']}), 400 - request_data = json.loads(request_data_field) + + try: + request_data = json.loads(request_data_field) + except json.decoder.JSONDecodeError: + return flask.jsonify({'errors': ['unable to parse valid JSON in torrent_data']}), 400 # Map api keys to upload form fields for key, default in UPLOAD_API_DEFAULTS.items(): From ba7e0a7c2ea0d9fb8c1e9c07b446a87dff828355 Mon Sep 17 00:00:00 2001 From: katnyaa Date: Tue, 23 May 2017 13:46:29 +0100 Subject: [PATCH 41/90] routes: Fix invalid call from previous merge --- nyaa/routes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nyaa/routes.py b/nyaa/routes.py index 3279ab5..42c258b 100644 --- a/nyaa/routes.py +++ b/nyaa/routes.py @@ -840,7 +840,8 @@ def site_rules(): def site_help(): return flask.render_template('help.html') -@app.routes('/xmlns/nyaa', methods=['GET']) + +@app.route('/xmlns/nyaa', methods=['GET']) def xmlns_nyaa(): return flask.render_template('xmlns.html') From 1b76eaea3520edda243b7abb53d74afacb7c5822 Mon Sep 17 00:00:00 2001 From: nyaadev Date: Tue, 23 May 2017 15:18:17 +0200 Subject: [PATCH 42/90] fix race condition? --- nyaa/api_handler.py | 3 +-- nyaa/backend.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/nyaa/api_handler.py b/nyaa/api_handler.py index c6087b4..571cc4d 100644 --- a/nyaa/api_handler.py +++ b/nyaa/api_handler.py @@ -240,11 +240,10 @@ def ghetto_import(): if not tracker: tracker = models.Trackers(uri=announce) db.session.add(tracker) + db.session.flush() db_trackers.add(tracker) - db.session.flush() - # Store tracker refs in DB for order, tracker in enumerate(db_trackers): torrent_tracker = models.TorrentTrackers(torrent_id=torrent.id, diff --git a/nyaa/backend.py b/nyaa/backend.py index 1c96095..c2b1fe1 100644 --- a/nyaa/backend.py +++ b/nyaa/backend.py @@ -139,11 +139,10 @@ def handle_torrent_upload(upload_form, uploading_user=None, fromAPI=False): if not tracker: tracker = models.Trackers(uri=announce) db.session.add(tracker) + db.session.flush() db_trackers.add(tracker) - db.session.flush() - # Store tracker refs in DB for order, tracker in enumerate(db_trackers): torrent_tracker = models.TorrentTrackers(torrent_id=torrent.id, From a88f6a08479d44cc591270cd62de2d760b0fbc60 Mon Sep 17 00:00:00 2001 From: nyaadev Date: Tue, 23 May 2017 23:37:06 +0200 Subject: [PATCH 43/90] please incease css version when editing it, fix #ad8a5c0d1c0998d808c4af3745cfc9e5c83f547b --- nyaa/templates/layout.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nyaa/templates/layout.html b/nyaa/templates/layout.html index 72290cb..7d6ebeb 100644 --- a/nyaa/templates/layout.html +++ b/nyaa/templates/layout.html @@ -27,7 +27,7 @@ - + From 020a0871bd88ec7111b26a49df338946b96d45b6 Mon Sep 17 00:00:00 2001 From: nyaadev Date: Wed, 24 May 2017 00:28:17 +0200 Subject: [PATCH 44/90] fix #173 and add avatar to mod user info add rank color to username everywhere some other minor changes --- nyaa/models.py | 4 +++- nyaa/routes.py | 9 +-------- nyaa/static/css/main.css | 11 ++++++----- nyaa/templates/layout.html | 4 ++-- nyaa/templates/profile.html | 18 +++++++++--------- nyaa/templates/user.html | 37 ++++++++++++++++++++++--------------- nyaa/templates/view.html | 18 +++++++++--------- 7 files changed, 52 insertions(+), 49 deletions(-) diff --git a/nyaa/models.py b/nyaa/models.py index e91a432..bd6629a 100644 --- a/nyaa/models.py +++ b/nyaa/models.py @@ -411,8 +411,10 @@ class User(db.Model): return 'User' elif self.level == UserLevelType.TRUSTED: return 'Trusted' - elif self.level >= UserLevelType.MODERATOR: + elif self.level == UserLevelType.MODERATOR: return 'Moderator' + elif self.level == UserLevelType.SUPERADMIN: + return 'Administrator' @property def userlevel_color(self): diff --git a/nyaa/routes.py b/nyaa/routes.py index 42c258b..465fabc 100644 --- a/nyaa/routes.py +++ b/nyaa/routes.py @@ -480,8 +480,6 @@ def profile(): form = forms.ProfileForm(flask.request.form) - level = ['Regular', 'Trusted', 'Moderator', 'Administrator'][flask.g.user.level] - if flask.request.method == 'POST' and form.validate(): user = flask.g.user new_email = form.email.data.strip() @@ -511,12 +509,7 @@ def profile(): flask.g.user = user return flask.redirect('/profile') - _user = models.User.by_id(flask.g.user.id) - username = _user.username - current_email = _user.email - - return flask.render_template('profile.html', form=form, name=username, email=current_email, - level=level) + return flask.render_template('profile.html', form=form) @app.route('/user/activate/') diff --git a/nyaa/static/css/main.css b/nyaa/static/css/main.css index fc54a47..90e8b13 100755 --- a/nyaa/static/css/main.css +++ b/nyaa/static/css/main.css @@ -258,15 +258,16 @@ a.text-purple:hover, a.text-purple:active, a.text-purple:focus { color: #a760e0; } .btn-grey { - color: #000000; - background-color: #cccfd2; - border-color: #ccc; + color: #000000; + background-color: #cccfd2; + border-color: #ccc; } .btn-grey:hover, .btn-grey:focus, .btn-grey:active, .btn-grey.active, .open > .dropdown-toggle.btn-grey { - background-color: #aaaaaa; + background-color: #aaaaaa; } .btn span.glyphicon { opacity: 0; } .btn.active span.glyphicon { - opacity: 1; + opacity: 1; +} diff --git a/nyaa/templates/layout.html b/nyaa/templates/layout.html index 7d6ebeb..3bde60b 100644 --- a/nyaa/templates/layout.html +++ b/nyaa/templates/layout.html @@ -27,7 +27,7 @@ - + @@ -82,7 +82,7 @@ {% else %}
+ +