diff --git a/config.example.py b/config.example.py
index a4c8025..4ca314f 100644
--- a/config.example.py
+++ b/config.example.py
@@ -176,3 +176,15 @@ EDITING_TIME_LIMIT = 0
# Whether to use Gravatar or just always use the default avatar
# (Useful if run as development instance behind NAT/firewall)
ENABLE_GRAVATAR = True
+
+##########################
+## Trusted Requirements ##
+##########################
+
+# Minimum number of uploads the user needs to have in order to apply for trusted
+TRUSTED_MIN_UPLOADS = 10
+# Minimum number of cumulative downloads the user needs to have across their
+# torrents in order to apply for trusted
+TRUSTED_MIN_DOWNLOADS = 10000
+# Number of days an applicant needs to wait before re-applying
+TRUSTED_REAPPLY_COOLDOWN = 90
diff --git a/migrations/versions/5cbcee17bece_add_trusted_applications.py b/migrations/versions/5cbcee17bece_add_trusted_applications.py
new file mode 100644
index 0000000..29167cb
--- /dev/null
+++ b/migrations/versions/5cbcee17bece_add_trusted_applications.py
@@ -0,0 +1,47 @@
+"""Add trusted applications
+
+Revision ID: 5cbcee17bece
+Revises: 8a6a7662eb37
+Create Date: 2018-11-05 15:16:07.497898
+
+"""
+from alembic import op
+import sqlalchemy as sa
+import sqlalchemy_utils
+
+
+# revision identifiers, used by Alembic.
+revision = '5cbcee17bece'
+down_revision = '8a6a7662eb37'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ op.create_table('trusted_applications',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('submitter_id', sa.Integer(), nullable=False, index=True),
+ sa.Column('created_time', sa.DateTime(), nullable=True),
+ sa.Column('closed_time', sa.DateTime(), nullable=True),
+ sa.Column('why_want', sa.String(length=4000), nullable=False),
+ sa.Column('why_give', sa.String(length=4000), nullable=False),
+ sa.Column('status', sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(['submitter_id'], ['users.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('trusted_reviews',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('reviewer_id', sa.Integer(), nullable=False),
+ sa.Column('app_id', sa.Integer(), nullable=False),
+ sa.Column('created_time', sa.DateTime(), nullable=True),
+ sa.Column('comment', sa.String(length=4000), nullable=False),
+ sa.Column('recommendation', sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(['app_id'], ['trusted_applications.id'], ),
+ sa.ForeignKeyConstraint(['reviewer_id'], ['users.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+
+
+def downgrade():
+ op.drop_table('trusted_reviews')
+ op.drop_table('trusted_applications')
diff --git a/nyaa/forms.py b/nyaa/forms.py
index d9e2620..09c3926 100644
--- a/nyaa/forms.py
+++ b/nyaa/forms.py
@@ -485,6 +485,33 @@ class ReportActionForm(FlaskForm):
report = HiddenField()
+class TrustedForm(FlaskForm):
+ why_give_trusted = TextAreaField('Why do you think you should be given trusted status?', [
+ Length(min=32, max=4000,
+ message='Please explain why you think you should be given trusted status in at '
+ 'least %(min)d but less than %(max)d characters.'),
+ DataRequired('Please fill out all of the fields in the form.')
+ ])
+ why_want_trusted = TextAreaField('Why do you want to become a trusted user?', [
+ Length(min=32, max=4000,
+ message='Please explain why you want to become a trusted user in at least %(min)d '
+ 'but less than %(max)d characters.'),
+ DataRequired('Please fill out all of the fields in the form.')
+ ])
+
+
+class TrustedReviewForm(FlaskForm):
+ comment = TextAreaField('Comment',
+ [Length(min=8, max=4000, message='Please provide a comment')])
+ recommendation = SelectField(choices=[('abstain', 'Abstain'), ('reject', 'Reject'),
+ ('accept', 'Accept')])
+
+
+class TrustedDecisionForm(FlaskForm):
+ accept = SubmitField('Accept')
+ reject = SubmitField('Reject')
+
+
def _validate_trackers(torrent_dict, tracker_to_check_for=None):
announce = torrent_dict.get('announce')
assert announce is not None, 'no tracker in torrent'
diff --git a/nyaa/models.py b/nyaa/models.py
index 687bd5c..44697a0 100644
--- a/nyaa/models.py
+++ b/nyaa/models.py
@@ -11,8 +11,9 @@ from urllib.parse import urlencode
import flask
from markupsafe import escape as escape_markup
-from sqlalchemy import ForeignKeyConstraint, Index
+from sqlalchemy import ForeignKeyConstraint, Index, func
from sqlalchemy.ext import declarative
+from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy_fulltext import FullText
from sqlalchemy_utils import ChoiceType, EmailType, PasswordType
@@ -642,6 +643,24 @@ class User(db.Model):
''' Returns a UTC POSIX timestamp, as seconds '''
return (self.created_time - UTC_EPOCH).total_seconds()
+ @property
+ def satisfies_trusted_reqs(self):
+ num_total = 0
+ downloads_total = 0
+ for ts_flavor, t_flavor in ((NyaaStatistic, NyaaTorrent),
+ (SukebeiStatistic, SukebeiTorrent)):
+ uploads = db.session.query(func.count(t_flavor.id)).\
+ filter(t_flavor.user == self).\
+ filter(t_flavor.flags.op('&')(int(TorrentFlags.REMAKE)).is_(False)).scalar()
+ dls = db.session.query(func.sum(ts_flavor.download_count)).\
+ join(t_flavor).\
+ filter(t_flavor.user == self).\
+ filter(t_flavor.flags.op('&')(int(TorrentFlags.REMAKE)).is_(False)).scalar()
+ num_total += uploads or 0
+ downloads_total += dls or 0
+ return (num_total >= config['TRUSTED_MIN_UPLOADS'] and
+ downloads_total >= config['TRUSTED_MIN_DOWNLOADS'])
+
class UserPreferences(db.Model):
__tablename__ = 'user_preferences'
@@ -845,6 +864,77 @@ class RangeBan(db.Model):
return q.count() > 0
+class TrustedApplicationStatus(IntEnum):
+ # If you change these, don't forget to change is_closed in TrustedApplication
+ NEW = 0
+ REVIEWED = 1
+ ACCEPTED = 2
+ REJECTED = 3
+
+
+class TrustedApplication(db.Model):
+ __tablename__ = 'trusted_applications'
+
+ id = db.Column(db.Integer, primary_key=True)
+ submitter_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True)
+ created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow)
+ closed_time = db.Column(db.DateTime(timezone=False))
+ why_want = db.Column(db.String(length=4000), nullable=False)
+ why_give = db.Column(db.String(length=4000), nullable=False)
+ status = db.Column(ChoiceType(TrustedApplicationStatus, impl=db.Integer()), nullable=False,
+ default=TrustedApplicationStatus.NEW)
+ reviews = db.relationship('TrustedReview', backref='trusted_applications')
+ submitter = db.relationship('User', uselist=False, lazy='joined', foreign_keys=[submitter_id])
+
+ @hybrid_property
+ def is_closed(self):
+ # We can't use the attribute names from TrustedApplicationStatus in an or here because of
+ # SQLAlchemy jank. It'll generate the wrong query.
+ return self.status > 1
+
+ @hybrid_property
+ def is_new(self):
+ return self.status == TrustedApplicationStatus.NEW
+
+ @hybrid_property
+ def is_reviewed(self):
+ return self.status == TrustedApplicationStatus.REVIEWED
+
+ @hybrid_property
+ def is_rejected(self):
+ return self.status == TrustedApplicationStatus.REJECTED
+
+ @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)
+
+
+class TrustedRecommendation(IntEnum):
+ ACCEPT = 0
+ REJECT = 1
+ ABSTAIN = 2
+
+
+class TrustedReview(db.Model):
+ __tablename__ = 'trusted_reviews'
+
+ id = db.Column(db.Integer, primary_key=True)
+ reviewer_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
+ app_id = db.Column(db.Integer, db.ForeignKey('trusted_applications.id'), nullable=False)
+ created_time = db.Column(db.DateTime(timezone=False), default=datetime.utcnow)
+ comment = db.Column(db.String(length=4000), nullable=False)
+ recommendation = db.Column(ChoiceType(TrustedRecommendation, impl=db.Integer()),
+ nullable=False)
+ reviewer = db.relationship('User', uselist=False, lazy='joined', foreign_keys=[reviewer_id])
+ application = db.relationship('TrustedApplication', uselist=False, lazy='joined',
+ foreign_keys=[app_id])
+
+
# Actually declare our site-specific classes
# Torrent
diff --git a/nyaa/static/css/main.css b/nyaa/static/css/main.css
index 455f702..0ecd081 100644
--- a/nyaa/static/css/main.css
+++ b/nyaa/static/css/main.css
@@ -448,6 +448,9 @@ h6:hover .header-anchor {
visibility: visible;
display: inline-block;
}
+.trusted-form textarea {
+ height:12em;
+}
/* Dark theme */
diff --git a/nyaa/static/js/main.js b/nyaa/static/js/main.js
index 8eb3624..efe41d9 100644
--- a/nyaa/static/js/main.js
+++ b/nyaa/static/js/main.js
@@ -251,6 +251,11 @@ document.addEventListener("DOMContentLoaded", function() {
var target = markdownTargets[i];
var rendered;
var markdownSource = htmlDecode(target.innerHTML);
+ if (target.attributes["markdown-no-images"]) {
+ markdown.disable('image');
+ } else {
+ markdown.enable('image');
+ }
if (target.attributes["markdown-text-inline"]) {
rendered = markdown.renderInline(markdownSource);
} else {
diff --git a/nyaa/templates/_formhelpers.html b/nyaa/templates/_formhelpers.html
index 71673d1..9a8b00c 100644
--- a/nyaa/templates/_formhelpers.html
+++ b/nyaa/templates/_formhelpers.html
@@ -113,7 +113,7 @@
{% endmacro %}
-{% macro render_menu_with_button(field) %}
+{% macro render_menu_with_button(field, button_label='Apply') %}
{% if field.errors %}
{% else %}
@@ -123,7 +123,7 @@
{% if field.errors %}
diff --git a/nyaa/templates/admin_trusted.html b/nyaa/templates/admin_trusted.html
new file mode 100644
index 0000000..a04b6bf
--- /dev/null
+++ b/nyaa/templates/admin_trusted.html
@@ -0,0 +1,54 @@
+{% extends "layout.html" %}
+{% macro render_filter_tab(name) %}
+
+
+ {% if name %}
+ {{ name.capitalize() }}
+ {% else %}
+ Open
+ {% endif %}
+
+
+{% endmacro %}
+{% block title %}Trusted Applications :: {{ config.SITE_NAME }}{% endblock %}
+{% block body %}
+
+ {{ render_filter_tab(None) }}
+ {{ render_filter_tab('new') }}
+ {{ render_filter_tab('reviewed') }}
+ {{ render_filter_tab('closed') }}
+
+
+
+ List of {{ list_filter or 'open' }} applications
+
+
+ # |
+ Submitter |
+ Submitted on |
+ Status |
+ |
+
+
+
+ {% for app in apps.items %}
+
+ {{ app.id }} |
+
+
+ {{ app.submitter.username }}
+
+ |
+ {{ app.created_time.strftime('%Y-%m-%d %H:%M') }} |
+ {{ app.status.name.capitalize() }} |
+ View |
+
+ {% endfor %}
+
+
+
+
+{% endblock %}
diff --git a/nyaa/templates/admin_trusted_view.html b/nyaa/templates/admin_trusted_view.html
new file mode 100644
index 0000000..afccbf9
--- /dev/null
+++ b/nyaa/templates/admin_trusted_view.html
@@ -0,0 +1,114 @@
+{% extends "layout.html" %}
+{% from "_formhelpers.html" import render_field, render_menu_with_button %}
+{%- macro review_class(rec) -%}
+{%- if rec.name == 'ACCEPT' -%}
+{{ 'panel-success' -}}
+{%- elif rec.name == 'REJECT' -%}
+{{ 'panel-danger' -}}
+{%- elif rec.name == 'ABSTAIN' -%}
+{{ 'panel-default' -}}
+{%- endif -%}
+{%- endmacro -%}
+{% block title %}{{ app.submitter.username }}'s Application :: {{ config.SITE_NAME }}{% endblock %}
+{% block body %}
+
+
+
{{ app.submitter.username }}'s Application
+
+
+
+
+
+
+
- Submitted on
+ -
+ {{ app.created_time.strftime('%Y-%m-%d %H:%M') }}
+
+
+
+
- Status
+ - {{ app.status.name.capitalize() }}
+
+
+
+
+
+
+
Why do you think you should be given trusted status?
+
+
+ {{- app.why_give | escape | replace('\r\n', '\n') | replace('\n', '
'|safe) -}}
+
+
+
+
+
+
+
Why do you want to become a trusted user?
+
+
+ {{- app.why_want | escape | replace('\r\n', '\n') | replace('\n', '
'|safe) -}}
+
+
+
+
+
+ {%- if decision_form -%}
+
+ {%- endif -%}
+
+
+
+
Reviews - {{ app.reviews | length }}
+
+
+ {% for rev in app.reviews %}
+
+
+
{{ rev.reviewer.username }}'s Review
+
+
+
+ {{- rev.comment | escape | replace('\r\n', '\n') | replace('\n', '
'|safe) -}}
+
+
+
+
+ {% endfor %}
+
+
+
+{% endblock %}
diff --git a/nyaa/templates/email/trusted.html b/nyaa/templates/email/trusted.html
new file mode 100644
index 0000000..2b41f81
--- /dev/null
+++ b/nyaa/templates/email/trusted.html
@@ -0,0 +1,14 @@
+
+
+
Your {{ config.GLOBAL_SITE_NAME }} Trusted Application was {{ 'accepted' if is_accepted else 'rejected' }}
+
+
+ {% if is_accepted %}
+
Congratulations! Your Trusted status application on {{ config.GLOBAL_SITE_NAME }} was accepted. You can now edit your torrents and set the Trusted flag on them.
+ {% else %}
+
We're sorry to inform you that we've rejected your Trusted status application on {{ config.GLOBAL_SITE_NAME }}. You can re-apply for Trusted status in {{ config.TRUSTED_REAPPLY_COOLDOWN }} days if you wish to do so.
+ {% endif %}
+
Regards
+ The {{ config.GLOBAL_SITE_NAME }} Moderation Team
+
+
diff --git a/nyaa/templates/email/trusted.txt b/nyaa/templates/email/trusted.txt
new file mode 100644
index 0000000..5bf9935
--- /dev/null
+++ b/nyaa/templates/email/trusted.txt
@@ -0,0 +1,8 @@
+{% if is_accepted %}
+Congratulations! Your Trusted status application on {{ config.GLOBAL_SITE_NAME }} was accepted. You can now edit your torrents and set the Trusted flag on them.
+{% else %}
+We're sorry to inform you that we've rejected your Trusted status application on {{ config.GLOBAL_SITE_NAME }}. You can re-apply for Trusted status in {{ config.TRUSTED_REAPPLY_COOLDOWN }} days if you wish to do so.
+{% endif %}
+
+Regards
+The {{ config.GLOBAL_SITE_NAME }} Moderation Team
diff --git a/nyaa/templates/layout.html b/nyaa/templates/layout.html
index 2c8c1e5..029b1e3 100644
--- a/nyaa/templates/layout.html
+++ b/nyaa/templates/layout.html
@@ -90,6 +90,7 @@
RSS
@@ -108,6 +109,7 @@
Reports
Log
Bans
+
Trusted
{% endif %}
diff --git a/nyaa/templates/trusted.html b/nyaa/templates/trusted.html
new file mode 100644
index 0000000..f296769
--- /dev/null
+++ b/nyaa/templates/trusted.html
@@ -0,0 +1,17 @@
+{% extends "layout.html" %}
+{% block title %}Trusted :: {{ config.SITE_NAME }}{% endblock %}
+{% block body %}
+
+
+
+
+ {% include "trusted_rules.html" %}
+
+
+
+
+{% endblock %}
diff --git a/nyaa/templates/trusted_form.html b/nyaa/templates/trusted_form.html
new file mode 100644
index 0000000..e052020
--- /dev/null
+++ b/nyaa/templates/trusted_form.html
@@ -0,0 +1,51 @@
+{% extends "layout.html" %}
+{% from "_formhelpers.html" import render_field %}
+{% block title %}Apply for Trusted :: {{ config.SITE_NAME }}{% endblock %}
+{% block body %}
+
+ {% if trusted_form %}
+
+
+
You are eligible to apply for trusted status
+
+
+
+ {% else %}
+
+
+
You are currently not eligible to apply for trusted status
+
+
+
+
+
+ You currently are not eligible to apply for trusted status for the following
+ reason{% if deny_reasons|length > 1 %}s{% endif %}:
+
+
+ {% for reason in deny_reasons %}
+ - {{ reason }}
+ {% endfor %}
+
+
+
+ {% endif %}
+
+{% endblock %}
diff --git a/nyaa/templates/trusted_rules.html b/nyaa/templates/trusted_rules.html
new file mode 100644
index 0000000..954750d
--- /dev/null
+++ b/nyaa/templates/trusted_rules.html
@@ -0,0 +1 @@
+
Trusted rules go here
diff --git a/nyaa/views/account.py b/nyaa/views/account.py
index f4fe6a0..15c0619 100644
--- a/nyaa/views/account.py
+++ b/nyaa/views/account.py
@@ -233,6 +233,49 @@ def profile():
return flask.render_template('profile.html', form=form)
+@bp.route('/trusted/request', methods=['GET', 'POST'])
+def request_trusted():
+ if not flask.g.user:
+ return flask.redirect(flask.url_for('account.login'))
+ trusted_form = None
+ deny_reasons = []
+ if flask.g.user.is_trusted:
+ deny_reasons.append('You are already trusted.')
+ if not flask.g.user.satisfies_trusted_reqs:
+ deny_reasons.append('You do not satisfy the minimum requirements.')
+ if (models.TrustedApplication.query.
+ filter(models.TrustedApplication.submitter_id == flask.g.user.id).
+ filter_by(is_closed=False).first()):
+ deny_reasons.append('You already have an open application.')
+ last_app = models.TrustedApplication.query \
+ .filter(models.TrustedApplication.submitter_id == flask.g.user.id) \
+ .filter_by(is_rejected=True) \
+ .order_by(models.TrustedApplication.closed_time.desc()) \
+ .first()
+ if last_app:
+ if ((datetime.utcnow() - last_app.closed_time).days <
+ app.config['TRUSTED_REAPPLY_COOLDOWN']):
+ deny_reasons.append('Your last application was rejected less than {} days ago.'
+ .format(app.config['TRUSTED_REAPPLY_COOLDOWN']))
+ if flask.request.method == 'POST':
+ trusted_form = forms.TrustedForm(flask.request.form)
+ if trusted_form.validate() and not deny_reasons:
+ ta = models.TrustedApplication()
+ ta.submitter_id = flask.g.user.id
+ ta.why_want = trusted_form.why_want_trusted.data.rstrip()
+ ta.why_give = trusted_form.why_give_trusted.data.rstrip()
+ db.session.add(ta)
+ db.session.commit()
+ flask.flash('Your trusted application has been submitted. '
+ 'You will receive an email when a decision has been made.', 'success')
+ return flask.redirect(flask.url_for('site.trusted'))
+ else:
+ if len(deny_reasons) == 0:
+ trusted_form = forms.TrustedForm()
+ return flask.render_template('trusted_form.html', trusted_form=trusted_form,
+ deny_reasons=deny_reasons)
+
+
def redirect_url():
next_url = flask.request.args.get('next', '')
referrer = flask.request.referrer or ''
diff --git a/nyaa/views/admin.py b/nyaa/views/admin.py
index 27a58e3..e2f4c68 100644
--- a/nyaa/views/admin.py
+++ b/nyaa/views/admin.py
@@ -1,10 +1,12 @@
+from datetime import datetime
from ipaddress import ip_address
import flask
-from nyaa import forms, models
+from nyaa import email, forms, models
from nyaa.extensions import db
+app = flask.current_app
bp = flask.Blueprint('admin', __name__, url_prefix='/admin')
@@ -114,3 +116,84 @@ def view_reports():
return flask.render_template('reports.html',
reports=reports,
report_action=report_action)
+
+
+@bp.route('/trusted/
', endpoint='trusted', methods=['GET'])
+@bp.route('/trusted', endpoint='trusted', methods=['GET'])
+def view_trusted(list_filter=None):
+ if not flask.g.user or not flask.g.user.is_moderator:
+ flask.abort(403)
+
+ page = flask.request.args.get('p', flask.request.args.get('offset', 1, int), int)
+ q = db.session.query(models.TrustedApplication)
+ if list_filter == 'closed':
+ q = q.filter_by(is_closed=True)
+ else:
+ q = q.filter_by(is_closed=False)
+ if list_filter == 'new':
+ q = q.filter_by(is_new=True)
+ elif list_filter == 'reviewed':
+ q = q.filter_by(is_reviewed=True)
+ elif list_filter is not None:
+ flask.abort(404)
+ apps = q.order_by(models.TrustedApplication.created_time.desc()) \
+ .paginate(page=page, per_page=20)
+
+ return flask.render_template('admin_trusted.html', apps=apps,
+ list_filter=list_filter)
+
+
+@bp.route('/trusted/application/', endpoint='trusted_application',
+ methods=['GET', 'POST'])
+def view_trusted_application(app_id):
+ if not flask.g.user or not flask.g.user.is_moderator:
+ flask.abort(403)
+ app = models.TrustedApplication.by_id(app_id)
+ if not app:
+ flask.abort(404)
+ decision_form = None
+ review_form = forms.TrustedReviewForm(flask.request.form)
+ if flask.g.user.is_superadmin and not app.is_closed:
+ decision_form = forms.TrustedDecisionForm()
+ if flask.request.method == 'POST':
+ do_decide = decision_form and (decision_form.accept.data or decision_form.reject.data)
+ if do_decide and decision_form.validate():
+ app.closed_time = datetime.utcnow()
+ if decision_form.accept.data:
+ app.status = models.TrustedApplicationStatus.ACCEPTED
+ app.submitter.level = models.UserLevelType.TRUSTED
+ flask.flash(flask.Markup('Application has been accepted.'), 'success')
+ elif decision_form.reject.data:
+ app.status = models.TrustedApplicationStatus.REJECTED
+ flask.flash(flask.Markup('Application has been rejected.'), 'success')
+ _send_trusted_decision_email(app.submitter, bool(decision_form.accept.data))
+ db.session.commit()
+ return flask.redirect(flask.url_for('admin.trusted_application', app_id=app_id))
+ elif review_form.comment.data and review_form.validate():
+ tr = models.TrustedReview()
+ tr.reviewer_id = flask.g.user.id
+ tr.app_id = app_id
+ tr.comment = review_form.comment.data
+ tr.recommendation = getattr(models.TrustedRecommendation,
+ review_form.recommendation.data.upper())
+ if app.status == models.TrustedApplicationStatus.NEW:
+ app.status = models.TrustedApplicationStatus.REVIEWED
+ db.session.add(tr)
+ db.session.commit()
+ flask.flash('Review successfully posted.', 'success')
+ return flask.redirect(flask.url_for('admin.trusted_application', app_id=app_id))
+
+ return flask.render_template('admin_trusted_view.html', app=app, review_form=review_form,
+ decision_form=decision_form)
+
+
+def _send_trusted_decision_email(user, is_accepted):
+ email_msg = email.EmailHolder(
+ subject='Your {} Trusted Application was {}.'.format(app.config['GLOBAL_SITE_NAME'],
+ ('rejected', 'accepted')[is_accepted]),
+ recipient=user,
+ text=flask.render_template('email/trusted.txt', is_accepted=is_accepted),
+ html=flask.render_template('email/trusted.html', is_accepted=is_accepted),
+ )
+
+ email.send_email(email_msg)
diff --git a/nyaa/views/site.py b/nyaa/views/site.py
index 6b1ba5e..b99cf4e 100644
--- a/nyaa/views/site.py
+++ b/nyaa/views/site.py
@@ -21,3 +21,8 @@ def help():
@bp.route('/xmlns/nyaa', methods=['GET'])
def xmlns_nyaa():
return flask.render_template('xmlns.html')
+
+
+@bp.route('/trusted', methods=['GET'])
+def trusted():
+ return flask.render_template('trusted.html')