mirror of
https://github.com/sb745/NyaaV3.git
synced 2025-03-12 13:56:55 +02:00

This commit adds the ability hide comments on torrent pages, and a user preferences tab to allow registered users to make this the default behavior.
985 lines
31 KiB
Python
985 lines
31 KiB
Python
import base64
|
|
import os.path
|
|
import re
|
|
from datetime import datetime
|
|
from enum import Enum, IntEnum
|
|
from hashlib import md5
|
|
from ipaddress import ip_address
|
|
from urllib.parse import unquote as unquote_url
|
|
from urllib.parse import urlencode
|
|
|
|
import flask
|
|
from markupsafe import escape as escape_markup
|
|
|
|
from sqlalchemy import ForeignKeyConstraint, Index
|
|
from sqlalchemy.ext import declarative
|
|
from sqlalchemy_fulltext import FullText
|
|
from sqlalchemy_utils import ChoiceType, EmailType, PasswordType
|
|
|
|
from nyaa.extensions import cache, config, db
|
|
from nyaa.torrents import create_magnet
|
|
|
|
app = flask.current_app
|
|
|
|
if config['USE_MYSQL']:
|
|
from sqlalchemy.dialects import mysql
|
|
BinaryType = mysql.BINARY
|
|
TextType = mysql.TEXT
|
|
MediumBlobType = mysql.MEDIUMBLOB
|
|
COL_UTF8_GENERAL_CI = 'utf8_general_ci'
|
|
COL_UTF8MB4_BIN = 'utf8mb4_bin'
|
|
COL_ASCII_GENERAL_CI = 'ascii_general_ci'
|
|
else:
|
|
BinaryType = db.Binary
|
|
TextType = db.String
|
|
MediumBlobType = db.BLOB
|
|
COL_UTF8_GENERAL_CI = 'NOCASE'
|
|
COL_UTF8MB4_BIN = None
|
|
COL_ASCII_GENERAL_CI = 'NOCASE'
|
|
|
|
|
|
# For property timestamps
|
|
UTC_EPOCH = datetime.utcfromtimestamp(0)
|
|
|
|
|
|
class DeclarativeHelperBase(object):
|
|
''' This class eases our nyaa-sukebei shenanigans by automatically adjusting
|
|
__tablename__ and providing class methods for renaming references. '''
|
|
# See http://docs.sqlalchemy.org/en/latest/orm/extensions/declarative/api.html
|
|
|
|
__tablename_base__ = None
|
|
__flavor__ = None
|
|
|
|
@classmethod
|
|
def _table_prefix_string(cls):
|
|
return cls.__flavor__.lower() + '_'
|
|
|
|
@classmethod
|
|
def _table_prefix(cls, table_name):
|
|
return cls._table_prefix_string() + table_name
|
|
|
|
@classmethod
|
|
def _flavor_prefix(cls, table_name):
|
|
return cls.__flavor__ + table_name
|
|
|
|
@declarative.declared_attr
|
|
def __tablename__(cls):
|
|
return cls._table_prefix(cls.__tablename_base__)
|
|
|
|
|
|
class FlagProperty(object):
|
|
''' This class will act as a wrapper between the given flag and the class's
|
|
flag collection. '''
|
|
|
|
def __init__(self, flag, flags_attr='flags'):
|
|
self._flag = flag
|
|
self._flags_attr_name = flags_attr
|
|
|
|
def _get_flags(self, instance):
|
|
return getattr(instance, self._flags_attr_name)
|
|
|
|
def _set_flags(self, instance, value):
|
|
return setattr(instance, self._flags_attr_name, value)
|
|
|
|
def __get__(self, instance, owner_class):
|
|
if instance is None:
|
|
raise AttributeError()
|
|
return bool(self._get_flags(instance) & self._flag)
|
|
|
|
def __set__(self, instance, value):
|
|
new_flags = (self._get_flags(instance) & ~self._flag) | (bool(value) and self._flag)
|
|
self._set_flags(instance, new_flags)
|
|
|
|
|
|
class TorrentFlags(IntEnum):
|
|
NONE = 0
|
|
ANONYMOUS = 1
|
|
HIDDEN = 2
|
|
TRUSTED = 4
|
|
REMAKE = 8
|
|
COMPLETE = 16
|
|
DELETED = 32
|
|
BANNED = 64
|
|
COMMENT_LOCKED = 128
|
|
|
|
|
|
class TorrentBase(DeclarativeHelperBase):
|
|
__tablename_base__ = 'torrents'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
info_hash = db.Column(BinaryType(length=20), unique=True, nullable=False, index=True)
|
|
display_name = db.Column(db.String(length=255, collation=COL_UTF8_GENERAL_CI),
|
|
nullable=False, index=True)
|
|
torrent_name = db.Column(db.String(length=255), nullable=False)
|
|
information = db.Column(db.String(length=255), nullable=False)
|
|
description = db.Column(TextType(collation=COL_UTF8MB4_BIN), nullable=False)
|
|
|
|
filesize = db.Column(db.BIGINT, default=0, nullable=False, index=True)
|
|
encoding = db.Column(db.String(length=32), nullable=False)
|
|
flags = db.Column(db.Integer, default=0, nullable=False, index=True)
|
|
|
|
@declarative.declared_attr
|
|
def uploader_id(cls):
|
|
# Even though this is same for both tables, declarative requires this
|
|
return 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)
|
|
|
|
comment_count = db.Column(db.Integer, default=0, nullable=False, index=True)
|
|
|
|
created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow, nullable=False)
|
|
updated_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow,
|
|
onupdate=datetime.utcnow, nullable=False)
|
|
|
|
@declarative.declared_attr
|
|
def main_category_id(cls):
|
|
fk = db.ForeignKey(cls._table_prefix('main_categories.id'))
|
|
return db.Column(db.Integer, fk, nullable=False)
|
|
|
|
sub_category_id = db.Column(db.Integer, nullable=False)
|
|
|
|
@declarative.declared_attr
|
|
def redirect(cls):
|
|
fk = db.ForeignKey(cls._table_prefix('torrents.id'))
|
|
return db.Column(db.Integer, fk, nullable=True)
|
|
|
|
@declarative.declared_attr
|
|
def __table_args__(cls):
|
|
return (
|
|
Index(cls._table_prefix('uploader_flag_idx'), 'uploader_id', 'flags'),
|
|
ForeignKeyConstraint(
|
|
['main_category_id', 'sub_category_id'],
|
|
[cls._table_prefix('sub_categories.main_category_id'),
|
|
cls._table_prefix('sub_categories.id')]
|
|
), {}
|
|
)
|
|
|
|
@declarative.declared_attr
|
|
def user(cls):
|
|
return db.relationship('User', uselist=False, back_populates=cls._table_prefix('torrents'))
|
|
|
|
@declarative.declared_attr
|
|
def main_category(cls):
|
|
return db.relationship(cls._flavor_prefix('MainCategory'), uselist=False,
|
|
back_populates='torrents', lazy="joined")
|
|
|
|
@declarative.declared_attr
|
|
def sub_category(cls):
|
|
join_sql = ("and_({0}SubCategory.id == foreign({0}Torrent.sub_category_id), "
|
|
"{0}SubCategory.main_category_id == {0}Torrent.main_category_id)")
|
|
return db.relationship(cls._flavor_prefix('SubCategory'), uselist=False,
|
|
backref='torrents', lazy="joined",
|
|
primaryjoin=join_sql.format(cls.__flavor__))
|
|
|
|
@declarative.declared_attr
|
|
def filelist(cls):
|
|
return db.relationship(cls._flavor_prefix('TorrentFilelist'), uselist=False,
|
|
cascade="all, delete-orphan", back_populates='torrent')
|
|
|
|
@declarative.declared_attr
|
|
def stats(cls):
|
|
return db.relationship(cls._flavor_prefix('Statistic'), uselist=False,
|
|
cascade="all, delete-orphan", back_populates='torrent',
|
|
lazy='joined')
|
|
|
|
@declarative.declared_attr
|
|
def trackers(cls):
|
|
return db.relationship(cls._flavor_prefix('TorrentTrackers'), uselist=True,
|
|
cascade="all, delete-orphan", lazy='select',
|
|
order_by=cls._flavor_prefix('TorrentTrackers.order'))
|
|
|
|
@declarative.declared_attr
|
|
def comments(cls):
|
|
return db.relationship(cls._flavor_prefix('Comment'), uselist=True,
|
|
cascade="all, delete-orphan")
|
|
|
|
def __repr__(self):
|
|
return '<{0} #{1.id} \'{1.display_name}\' {1.filesize}b>'.format(type(self).__name__, self)
|
|
|
|
def update_comment_count(self):
|
|
self.comment_count = Comment.query.filter_by(torrent_id=self.id).count()
|
|
return self.comment_count
|
|
|
|
@property
|
|
def created_utc_timestamp(self):
|
|
''' Returns a UTC POSIX timestamp, as seconds '''
|
|
return (self.created_time - UTC_EPOCH).total_seconds()
|
|
|
|
@property
|
|
def information_as_link(self):
|
|
''' Formats the .information into an IRC or HTTP(S) <a> if possible,
|
|
otherwise escapes it. '''
|
|
irc_match = re.match(r'^#([a-zA-Z0-9-_]+)@([a-zA-Z0-9-_.:]+)$', self.information)
|
|
if irc_match:
|
|
# Return a formatted IRC uri
|
|
return '<a href="irc://{1}/{0}">#{0}@{1}</a>'.format(*irc_match.groups())
|
|
|
|
url_match = re.match(r'^(https?:\/\/.+?)$', self.information)
|
|
if url_match:
|
|
url = url_match.group(1)
|
|
|
|
invalid_url_characters = '<>"'
|
|
# Check if url contains invalid characters
|
|
if not any(c in url for c in invalid_url_characters):
|
|
return('<a rel="noopener noreferrer nofollow" '
|
|
'href="{0}">{1}</a>'.format(url, escape_markup(unquote_url(url))))
|
|
# Escaped
|
|
return escape_markup(self.information)
|
|
|
|
@property
|
|
def info_dict_path(self):
|
|
''' Returns a path to the info_dict file in form of 'info_dicts/aa/bb/aabbccddee...' '''
|
|
info_hash = self.info_hash_as_hex
|
|
return os.path.join(app.config['BASE_DIR'], 'info_dicts',
|
|
info_hash[0:2], info_hash[2:4], info_hash)
|
|
|
|
@property
|
|
def info_hash_as_b32(self):
|
|
return base64.b32encode(self.info_hash).decode('utf-8')
|
|
|
|
@property
|
|
def info_hash_as_hex(self):
|
|
return self.info_hash.hex()
|
|
|
|
@property
|
|
@cache.memoize(timeout=3600)
|
|
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))
|
|
|
|
# Flag properties below
|
|
|
|
anonymous = FlagProperty(TorrentFlags.ANONYMOUS)
|
|
hidden = FlagProperty(TorrentFlags.HIDDEN)
|
|
deleted = FlagProperty(TorrentFlags.DELETED)
|
|
banned = FlagProperty(TorrentFlags.BANNED)
|
|
trusted = FlagProperty(TorrentFlags.TRUSTED)
|
|
remake = FlagProperty(TorrentFlags.REMAKE)
|
|
complete = FlagProperty(TorrentFlags.COMPLETE)
|
|
comment_locked = FlagProperty(TorrentFlags.COMMENT_LOCKED)
|
|
|
|
# Class methods
|
|
|
|
@classmethod
|
|
def by_id(cls, id):
|
|
return cls.query.get(id)
|
|
|
|
@classmethod
|
|
def by_info_hash(cls, info_hash):
|
|
return cls.query.filter_by(info_hash=info_hash).first()
|
|
|
|
@classmethod
|
|
def by_info_hash_hex(cls, info_hash_hex):
|
|
info_hash_bytes = bytearray.fromhex(info_hash_hex)
|
|
return cls.by_info_hash(info_hash_bytes)
|
|
|
|
|
|
class TorrentFilelistBase(DeclarativeHelperBase):
|
|
__tablename_base__ = 'torrents_filelist'
|
|
|
|
__table_args__ = {'mysql_row_format': 'COMPRESSED'}
|
|
|
|
@declarative.declared_attr
|
|
def torrent_id(cls):
|
|
fk = db.ForeignKey(cls._table_prefix('torrents.id'), ondelete="CASCADE")
|
|
return db.Column(db.Integer, fk, primary_key=True)
|
|
|
|
filelist_blob = db.Column(MediumBlobType, nullable=True)
|
|
|
|
@declarative.declared_attr
|
|
def torrent(cls):
|
|
return db.relationship(cls._flavor_prefix('Torrent'), uselist=False,
|
|
back_populates='filelist')
|
|
|
|
|
|
class StatisticBase(DeclarativeHelperBase):
|
|
__tablename_base__ = 'statistics'
|
|
|
|
@declarative.declared_attr
|
|
def torrent_id(cls):
|
|
fk = db.ForeignKey(cls._table_prefix('torrents.id'), ondelete="CASCADE")
|
|
return db.Column(db.Integer, fk, primary_key=True)
|
|
|
|
seed_count = db.Column(db.Integer, default=0, nullable=False, index=True)
|
|
leech_count = db.Column(db.Integer, default=0, nullable=False, index=True)
|
|
download_count = db.Column(db.Integer, default=0, nullable=False, index=True)
|
|
last_updated = db.Column(db.DateTime(timezone=False))
|
|
|
|
@declarative.declared_attr
|
|
def torrent(cls):
|
|
return db.relationship(cls._flavor_prefix('Torrent'), uselist=False,
|
|
back_populates='stats')
|
|
|
|
|
|
class Trackers(db.Model):
|
|
__tablename__ = 'trackers'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
uri = db.Column(db.String(length=255, collation=COL_UTF8_GENERAL_CI),
|
|
nullable=False, unique=True)
|
|
is_webseed = db.Column(db.Boolean, nullable=False, default=False)
|
|
disabled = db.Column(db.Boolean, nullable=False, default=False)
|
|
|
|
@classmethod
|
|
def by_uri(cls, uri):
|
|
return cls.query.filter_by(uri=uri).first()
|
|
|
|
|
|
class TorrentTrackersBase(DeclarativeHelperBase):
|
|
__tablename_base__ = 'torrent_trackers'
|
|
|
|
@declarative.declared_attr
|
|
def torrent_id(cls):
|
|
fk = db.ForeignKey(cls._table_prefix('torrents.id'), ondelete="CASCADE")
|
|
return db.Column(db.Integer, fk, primary_key=True)
|
|
|
|
@declarative.declared_attr
|
|
def tracker_id(cls):
|
|
fk = db.ForeignKey('trackers.id', ondelete="CASCADE")
|
|
return db.Column(db.Integer, fk, primary_key=True)
|
|
|
|
order = db.Column(db.Integer, nullable=False, index=True)
|
|
|
|
@declarative.declared_attr
|
|
def tracker(cls):
|
|
return db.relationship('Trackers', uselist=False, lazy='joined')
|
|
|
|
@classmethod
|
|
def by_torrent_id(cls, torrent_id):
|
|
return cls.query.filter_by(torrent_id=torrent_id).order_by(cls.order.desc())
|
|
|
|
|
|
class MainCategoryBase(DeclarativeHelperBase):
|
|
__tablename_base__ = 'main_categories'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
name = db.Column(db.String(length=64), nullable=False)
|
|
|
|
@declarative.declared_attr
|
|
def sub_categories(cls):
|
|
return db.relationship(cls._flavor_prefix('SubCategory'), back_populates='main_category')
|
|
|
|
@declarative.declared_attr
|
|
def torrents(cls):
|
|
return db.relationship(cls._flavor_prefix('Torrent'), back_populates='main_category')
|
|
|
|
def get_category_ids(self):
|
|
return (self.id, 0)
|
|
|
|
@property
|
|
def id_as_string(self):
|
|
return '_'.join(str(x) for x in self.get_category_ids())
|
|
|
|
@classmethod
|
|
def by_id(cls, id):
|
|
return cls.query.get(id)
|
|
|
|
|
|
class SubCategoryBase(DeclarativeHelperBase):
|
|
__tablename_base__ = 'sub_categories'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
|
|
@declarative.declared_attr
|
|
def main_category_id(cls):
|
|
fk = db.ForeignKey(cls._table_prefix('main_categories.id'))
|
|
return db.Column(db.Integer, fk, primary_key=True)
|
|
|
|
name = db.Column(db.String(length=64), nullable=False)
|
|
|
|
@declarative.declared_attr
|
|
def main_category(cls):
|
|
return db.relationship(cls._flavor_prefix('MainCategory'), uselist=False,
|
|
back_populates='sub_categories')
|
|
|
|
def get_category_ids(self):
|
|
return (self.main_category_id, self.id)
|
|
|
|
@property
|
|
def id_as_string(self):
|
|
return '_'.join(str(x) for x in self.get_category_ids())
|
|
|
|
@classmethod
|
|
def by_category_ids(cls, main_cat_id, sub_cat_id):
|
|
return cls.query.get((sub_cat_id, main_cat_id))
|
|
|
|
|
|
class CommentBase(DeclarativeHelperBase):
|
|
__tablename_base__ = 'comments'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
|
|
@declarative.declared_attr
|
|
def torrent_id(cls):
|
|
return db.Column(db.Integer, db.ForeignKey(
|
|
cls._table_prefix('torrents.id'), ondelete='CASCADE'), nullable=False)
|
|
|
|
@declarative.declared_attr
|
|
def user_id(cls):
|
|
return db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'))
|
|
|
|
created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow)
|
|
edited_time = db.Column(db.DateTime(timezone=False), onupdate=datetime.utcnow)
|
|
text = db.Column(TextType(collation=COL_UTF8MB4_BIN), nullable=False)
|
|
|
|
@declarative.declared_attr
|
|
def user(cls):
|
|
return db.relationship('User', uselist=False,
|
|
back_populates=cls._table_prefix('comments'), lazy="joined")
|
|
|
|
@declarative.declared_attr
|
|
def torrent(cls):
|
|
return db.relationship(cls._flavor_prefix('Torrent'), uselist=False,
|
|
back_populates='comments', lazy="joined")
|
|
|
|
def __repr__(self):
|
|
return '<Comment %r>' % self.id
|
|
|
|
@property
|
|
def created_utc_timestamp(self):
|
|
''' Returns a UTC POSIX timestamp, as seconds '''
|
|
return (self.created_time - UTC_EPOCH).total_seconds()
|
|
|
|
@property
|
|
def edited_utc_timestamp(self):
|
|
''' Returns a UTC POSIX timestamp, as seconds '''
|
|
return (self.edited_time - UTC_EPOCH).total_seconds() if self.edited_time else 0
|
|
|
|
@property
|
|
def editable_until(self):
|
|
return self.created_utc_timestamp + config['EDITING_TIME_LIMIT']
|
|
|
|
@property
|
|
def editing_limit_exceeded(self):
|
|
limit = config['EDITING_TIME_LIMIT']
|
|
return bool(limit and (datetime.utcnow() - self.created_time).total_seconds() >= limit)
|
|
|
|
|
|
class UserLevelType(IntEnum):
|
|
REGULAR = 0
|
|
TRUSTED = 1
|
|
MODERATOR = 2
|
|
SUPERADMIN = 3
|
|
|
|
|
|
class UserStatusType(Enum):
|
|
INACTIVE = 0
|
|
ACTIVE = 1
|
|
BANNED = 2
|
|
|
|
|
|
class User(db.Model):
|
|
__tablename__ = 'users'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
username = db.Column(db.String(length=32, collation=COL_ASCII_GENERAL_CI),
|
|
unique=True, nullable=False)
|
|
email = db.Column(EmailType(length=255, collation=COL_ASCII_GENERAL_CI),
|
|
unique=True, nullable=True)
|
|
password_hash = db.Column(PasswordType(max_length=255, schemes=['argon2']), nullable=False)
|
|
status = db.Column(ChoiceType(UserStatusType, impl=db.Integer()), nullable=False)
|
|
level = db.Column(ChoiceType(UserLevelType, impl=db.Integer()), nullable=False)
|
|
|
|
created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow)
|
|
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)
|
|
registration_ip = db.Column(db.Binary(length=16), default=None, nullable=True)
|
|
|
|
nyaa_torrents = db.relationship('NyaaTorrent', back_populates='user', lazy='dynamic')
|
|
nyaa_comments = db.relationship('NyaaComment', back_populates='user', lazy='dynamic')
|
|
|
|
sukebei_torrents = db.relationship('SukebeiTorrent', back_populates='user', lazy='dynamic')
|
|
sukebei_comments = db.relationship('SukebeiComment', back_populates='user', lazy='dynamic')
|
|
|
|
bans = db.relationship('Ban', uselist=True, foreign_keys='Ban.user_id')
|
|
|
|
preferences = db.relationship('UserPreferences',
|
|
back_populates='user', uselist=False, lazy='joined')
|
|
|
|
def __init__(self, username, email, password):
|
|
self.username = username
|
|
self.email = email
|
|
self.password_hash = password
|
|
self.status = UserStatusType.INACTIVE
|
|
self.level = UserLevelType.REGULAR
|
|
|
|
def __repr__(self):
|
|
return '<User %r>' % self.username
|
|
|
|
def validate_authorization(self, password):
|
|
''' Returns a boolean for whether the user can be logged in '''
|
|
checks = [
|
|
# Password must match
|
|
password == self.password_hash,
|
|
# Reject inactive and banned users
|
|
self.status == UserStatusType.ACTIVE
|
|
]
|
|
return all(checks)
|
|
|
|
def gravatar_url(self):
|
|
if 'DEFAULT_GRAVATAR_URL' in app.config:
|
|
default_url = app.config['DEFAULT_GRAVATAR_URL']
|
|
else:
|
|
default_url = flask.url_for('static', filename='img/avatar/default.png',
|
|
_external=True)
|
|
if app.config['ENABLE_GRAVATAR']:
|
|
# from http://en.gravatar.com/site/implement/images/python/
|
|
params = {
|
|
# Image size (https://en.gravatar.com/site/implement/images/#size)
|
|
's': 120,
|
|
# Default image (https://en.gravatar.com/site/implement/images/#default-image)
|
|
'd': default_url,
|
|
# Image rating (https://en.gravatar.com/site/implement/images/#rating)
|
|
# Nyaa: PG-rated, Sukebei: X-rated
|
|
'r': 'pg' if app.config['SITE_FLAVOR'] == 'nyaa' else 'x',
|
|
}
|
|
# construct the url
|
|
return 'https://www.gravatar.com/avatar/{}?{}'.format(
|
|
md5(self.email.encode('utf-8').lower()).hexdigest(), urlencode(params))
|
|
else:
|
|
return default_url
|
|
|
|
@property
|
|
def userlevel_str(self):
|
|
level = ''
|
|
if self.level == UserLevelType.REGULAR:
|
|
level = 'User'
|
|
elif self.level == UserLevelType.TRUSTED:
|
|
level = 'Trusted'
|
|
elif self.level == UserLevelType.MODERATOR:
|
|
level = 'Moderator'
|
|
elif self.level >= UserLevelType.SUPERADMIN:
|
|
level = 'Administrator'
|
|
if self.is_banned:
|
|
level = 'BANNED ' + level
|
|
return level
|
|
|
|
@property
|
|
def userstatus_str(self):
|
|
if self.status == UserStatusType.INACTIVE:
|
|
return 'Inactive'
|
|
elif self.status == UserStatusType.ACTIVE:
|
|
return 'Active'
|
|
elif self.status == UserStatusType.BANNED:
|
|
return 'Banned'
|
|
|
|
@property
|
|
def userlevel_color(self):
|
|
color = ''
|
|
if self.level == UserLevelType.REGULAR:
|
|
color = 'default'
|
|
elif self.level == UserLevelType.TRUSTED:
|
|
color = 'success'
|
|
elif self.level >= UserLevelType.MODERATOR:
|
|
color = 'purple'
|
|
if self.is_banned:
|
|
color += ' strike'
|
|
return color
|
|
|
|
@property
|
|
def ip_string(self):
|
|
if self.last_login_ip:
|
|
return str(ip_address(self.last_login_ip))
|
|
|
|
@property
|
|
def reg_ip_string(self):
|
|
if self.registration_ip:
|
|
return str(ip_address(self.registration_ip))
|
|
|
|
@classmethod
|
|
def by_id(cls, id):
|
|
return cls.query.get(id)
|
|
|
|
@classmethod
|
|
def by_username(cls, username):
|
|
def isascii(s): return len(s) == len(s.encode())
|
|
if not isascii(username):
|
|
return None
|
|
|
|
user = cls.query.filter_by(username=username).first()
|
|
return user
|
|
|
|
@classmethod
|
|
def by_email(cls, email):
|
|
user = cls.query.filter_by(email=email).first()
|
|
return user
|
|
|
|
@classmethod
|
|
def by_username_or_email(cls, username_or_email):
|
|
return cls.by_username(username_or_email) or cls.by_email(username_or_email)
|
|
|
|
@property
|
|
def is_moderator(self):
|
|
return self.level >= UserLevelType.MODERATOR
|
|
|
|
@property
|
|
def is_superadmin(self):
|
|
return self.level == UserLevelType.SUPERADMIN
|
|
|
|
@property
|
|
def is_trusted(self):
|
|
return self.level >= UserLevelType.TRUSTED
|
|
|
|
@property
|
|
def is_banned(self):
|
|
return self.status == UserStatusType.BANNED
|
|
|
|
@property
|
|
def is_active(self):
|
|
return self.status != UserStatusType.INACTIVE
|
|
|
|
@property
|
|
def age(self):
|
|
'''Account age in seconds'''
|
|
return (datetime.utcnow() - self.created_time).total_seconds()
|
|
|
|
@property
|
|
def created_utc_timestamp(self):
|
|
''' Returns a UTC POSIX timestamp, as seconds '''
|
|
return (self.created_time - UTC_EPOCH).total_seconds()
|
|
|
|
|
|
class UserPreferences(db.Model):
|
|
__tablename__ = 'user_preferences'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), unique=True, nullable=False)
|
|
|
|
def __init__(self, user_id):
|
|
self.user_id = user_id
|
|
|
|
def __repr__(self):
|
|
return '<UserPreferences %r>' % self.id
|
|
|
|
user = db.relationship('User', back_populates='preferences')
|
|
hide_comments = db.Column(db.Boolean, nullable=False, default=False)
|
|
|
|
|
|
class AdminLogBase(DeclarativeHelperBase):
|
|
__tablename_base__ = 'adminlog'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow)
|
|
log = db.Column(db.String(length=1024), nullable=False)
|
|
|
|
@declarative.declared_attr
|
|
def admin_id(cls):
|
|
return db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
|
|
|
def __init__(self, log, admin_id):
|
|
self.log = log
|
|
self.admin_id = admin_id
|
|
|
|
def __repr__(self):
|
|
return '<AdminLog %r>' % self.id
|
|
|
|
@property
|
|
def created_utc_timestamp(self):
|
|
''' Returns a UTC POSIX timestamp, as seconds '''
|
|
return (self.created_time - UTC_EPOCH).total_seconds()
|
|
|
|
@declarative.declared_attr
|
|
def admin(cls):
|
|
return db.relationship('User', uselist=False, lazy="joined")
|
|
|
|
@classmethod
|
|
def all_logs(cls):
|
|
return cls.query
|
|
|
|
|
|
class ReportStatus(IntEnum):
|
|
IN_REVIEW = 0
|
|
VALID = 1
|
|
INVALID = 2
|
|
|
|
|
|
class ReportBase(DeclarativeHelperBase):
|
|
__tablename_base__ = 'reports'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow)
|
|
reason = db.Column(db.String(length=255), nullable=False)
|
|
status = db.Column(ChoiceType(ReportStatus, impl=db.Integer()), nullable=False)
|
|
|
|
@declarative.declared_attr
|
|
def torrent_id(cls):
|
|
return db.Column(db.Integer, db.ForeignKey(
|
|
cls._table_prefix('torrents.id'), ondelete='CASCADE'), nullable=False)
|
|
|
|
@declarative.declared_attr
|
|
def user_id(cls):
|
|
return db.Column(db.Integer, db.ForeignKey('users.id'))
|
|
|
|
@declarative.declared_attr
|
|
def user(cls):
|
|
return db.relationship('User', uselist=False, lazy="joined")
|
|
|
|
@declarative.declared_attr
|
|
def torrent(cls):
|
|
return db.relationship(cls._flavor_prefix('Torrent'), uselist=False, lazy="joined")
|
|
|
|
def __init__(self, torrent_id, user_id, reason):
|
|
self.torrent_id = torrent_id
|
|
self.user_id = user_id
|
|
self.reason = reason
|
|
self.status = ReportStatus.IN_REVIEW
|
|
|
|
def __repr__(self):
|
|
return '<Report %r>' % self.id
|
|
|
|
@property
|
|
def created_utc_timestamp(self):
|
|
''' Returns a UTC POSIX timestamp, as seconds '''
|
|
return (self.created_time - UTC_EPOCH).total_seconds()
|
|
|
|
@classmethod
|
|
def by_id(cls, id):
|
|
return cls.query.get(id)
|
|
|
|
@classmethod
|
|
def not_reviewed(cls, page):
|
|
reports = cls.query.filter_by(status=0).paginate(page=page, per_page=20)
|
|
return reports
|
|
|
|
@classmethod
|
|
def remove_reviewed(cls, id):
|
|
return cls.query.filter(cls.torrent_id == id, cls.status == 0).delete()
|
|
|
|
|
|
class Ban(db.Model):
|
|
__tablename__ = 'bans'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow)
|
|
admin_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
|
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
|
|
user_ip = db.Column(db.Binary(length=16), nullable=True)
|
|
reason = db.Column(db.String(length=2048), nullable=False)
|
|
|
|
admin = db.relationship('User', uselist=False, lazy='joined', foreign_keys=[admin_id])
|
|
user = db.relationship('User', uselist=False, lazy='joined', foreign_keys=[user_id])
|
|
|
|
__table_args__ = (
|
|
Index('user_ip_4', 'user_ip', mysql_length=4, unique=True),
|
|
Index('user_ip_16', 'user_ip', mysql_length=16, unique=True),
|
|
)
|
|
|
|
def __repr__(self):
|
|
return '<Ban %r>' % self.id
|
|
|
|
@property
|
|
def ip_string(self):
|
|
if self.user_ip:
|
|
return str(ip_address(self.user_ip))
|
|
|
|
@classmethod
|
|
def all_bans(cls):
|
|
return cls.query
|
|
|
|
@classmethod
|
|
def by_id(cls, id):
|
|
return cls.query.get(id)
|
|
|
|
@classmethod
|
|
def banned(cls, user_id, user_ip):
|
|
if user_id:
|
|
if user_ip:
|
|
return cls.query.filter((cls.user_id == user_id) | (cls.user_ip == user_ip))
|
|
return cls.query.filter(cls.user_id == user_id)
|
|
if user_ip:
|
|
return cls.query.filter(cls.user_ip == user_ip)
|
|
return None
|
|
|
|
|
|
class TrackerApiBase(DeclarativeHelperBase):
|
|
__tablename_base__ = 'trackerapi'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
info_hash = db.Column(BinaryType(length=20), nullable=False)
|
|
method = db.Column(db.String(length=255), nullable=False)
|
|
# Methods = insert, remove
|
|
|
|
def __init__(self, info_hash, method):
|
|
self.info_hash = info_hash
|
|
self.method = method
|
|
|
|
|
|
class RangeBan(db.Model):
|
|
__tablename__ = 'rangebans'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
_cidr_string = db.Column('cidr_string', db.String(length=18), nullable=False)
|
|
masked_cidr = db.Column(db.BigInteger, nullable=False,
|
|
index=True)
|
|
mask = db.Column(db.BigInteger, nullable=False, index=True)
|
|
enabled = db.Column(db.Boolean, nullable=False, default=True)
|
|
# If this rangeban may be automatically cleared once it becomes
|
|
# out of date, set this column to the creation time of the ban.
|
|
# None (or NULL in the db) is understood as the ban being permanent.
|
|
temp = db.Column(db.DateTime(timezone=False), nullable=True, default=None)
|
|
|
|
@property
|
|
def cidr_string(self):
|
|
return self._cidr_string
|
|
|
|
@cidr_string.setter
|
|
def cidr_string(self, s):
|
|
subnet, masked_bits = s.split('/')
|
|
subnet_b = ip_address(subnet).packed
|
|
self.mask = (1 << 32) - (1 << (32 - int(masked_bits)))
|
|
self.masked_cidr = int.from_bytes(subnet_b, 'big') & self.mask
|
|
self._cidr_string = s
|
|
|
|
@classmethod
|
|
def is_rangebanned(cls, ip):
|
|
if len(ip) > 4:
|
|
raise NotImplementedError("IPv6 is unsupported.")
|
|
elif len(ip) < 4:
|
|
raise ValueError("Not an IP address.")
|
|
ip_int = int.from_bytes(ip, 'big')
|
|
q = cls.query.filter(cls.mask.op('&')(ip_int) == cls.masked_cidr,
|
|
cls.enabled)
|
|
return q.count() > 0
|
|
|
|
|
|
# Actually declare our site-specific classes
|
|
|
|
# Torrent
|
|
class NyaaTorrent(TorrentBase, db.Model):
|
|
__flavor__ = 'Nyaa'
|
|
|
|
|
|
class SukebeiTorrent(TorrentBase, db.Model):
|
|
__flavor__ = 'Sukebei'
|
|
|
|
|
|
# Fulltext models for MySQL
|
|
if config['USE_MYSQL']:
|
|
class NyaaTorrentNameSearch(FullText, NyaaTorrent):
|
|
__fulltext_columns__ = ('display_name',)
|
|
__table_args__ = {'extend_existing': True}
|
|
|
|
class SukebeiTorrentNameSearch(FullText, SukebeiTorrent):
|
|
__fulltext_columns__ = ('display_name',)
|
|
__table_args__ = {'extend_existing': True}
|
|
else:
|
|
# Bogus classes for Sqlite
|
|
class NyaaTorrentNameSearch(object):
|
|
pass
|
|
|
|
class SukebeiTorrentNameSearch(object):
|
|
pass
|
|
|
|
|
|
# TorrentFilelist
|
|
class NyaaTorrentFilelist(TorrentFilelistBase, db.Model):
|
|
__flavor__ = 'Nyaa'
|
|
|
|
|
|
class SukebeiTorrentFilelist(TorrentFilelistBase, db.Model):
|
|
__flavor__ = 'Sukebei'
|
|
|
|
|
|
# Statistic
|
|
class NyaaStatistic(StatisticBase, db.Model):
|
|
__flavor__ = 'Nyaa'
|
|
|
|
|
|
class SukebeiStatistic(StatisticBase, db.Model):
|
|
__flavor__ = 'Sukebei'
|
|
|
|
|
|
# TorrentTrackers
|
|
class NyaaTorrentTrackers(TorrentTrackersBase, db.Model):
|
|
__flavor__ = 'Nyaa'
|
|
|
|
|
|
class SukebeiTorrentTrackers(TorrentTrackersBase, db.Model):
|
|
__flavor__ = 'Sukebei'
|
|
|
|
|
|
# MainCategory
|
|
class NyaaMainCategory(MainCategoryBase, db.Model):
|
|
__flavor__ = 'Nyaa'
|
|
|
|
|
|
class SukebeiMainCategory(MainCategoryBase, db.Model):
|
|
__flavor__ = 'Sukebei'
|
|
|
|
|
|
# SubCategory
|
|
class NyaaSubCategory(SubCategoryBase, db.Model):
|
|
__flavor__ = 'Nyaa'
|
|
|
|
|
|
class SukebeiSubCategory(SubCategoryBase, db.Model):
|
|
__flavor__ = 'Sukebei'
|
|
|
|
|
|
# Comment
|
|
class NyaaComment(CommentBase, db.Model):
|
|
__flavor__ = 'Nyaa'
|
|
|
|
|
|
class SukebeiComment(CommentBase, db.Model):
|
|
__flavor__ = 'Sukebei'
|
|
|
|
|
|
# AdminLog
|
|
class NyaaAdminLog(AdminLogBase, db.Model):
|
|
__flavor__ = 'Nyaa'
|
|
|
|
|
|
class SukebeiAdminLog(AdminLogBase, db.Model):
|
|
__flavor__ = 'Sukebei'
|
|
|
|
|
|
# Report
|
|
class NyaaReport(ReportBase, db.Model):
|
|
__flavor__ = 'Nyaa'
|
|
|
|
|
|
class SukebeiReport(ReportBase, db.Model):
|
|
__flavor__ = 'Sukebei'
|
|
|
|
|
|
# TrackerApi
|
|
class NyaaTrackerApi(TrackerApiBase, db.Model):
|
|
__flavor__ = 'Nyaa'
|
|
|
|
|
|
class SukebeiTrackerApi(TrackerApiBase, db.Model):
|
|
__flavor__ = 'Sukebei'
|
|
|
|
|
|
# Choose our defaults for models.Torrent etc
|
|
if config['SITE_FLAVOR'] == 'nyaa':
|
|
Torrent = NyaaTorrent
|
|
TorrentFilelist = NyaaTorrentFilelist
|
|
Statistic = NyaaStatistic
|
|
TorrentTrackers = NyaaTorrentTrackers
|
|
MainCategory = NyaaMainCategory
|
|
SubCategory = NyaaSubCategory
|
|
Comment = NyaaComment
|
|
AdminLog = NyaaAdminLog
|
|
Report = NyaaReport
|
|
TorrentNameSearch = NyaaTorrentNameSearch
|
|
TrackerApi = NyaaTrackerApi
|
|
|
|
elif config['SITE_FLAVOR'] == 'sukebei':
|
|
Torrent = SukebeiTorrent
|
|
TorrentFilelist = SukebeiTorrentFilelist
|
|
Statistic = SukebeiStatistic
|
|
TorrentTrackers = SukebeiTorrentTrackers
|
|
MainCategory = SukebeiMainCategory
|
|
SubCategory = SukebeiSubCategory
|
|
Comment = SukebeiComment
|
|
AdminLog = SukebeiAdminLog
|
|
Report = SukebeiReport
|
|
TorrentNameSearch = SukebeiTorrentNameSearch
|
|
TrackerApi = SukebeiTrackerApi
|