Adding admin and report endpoints

This commit is contained in:
Yusur 2019-11-11 19:15:55 +01:00
parent af299a53c7
commit 6c128d0567
18 changed files with 335 additions and 6 deletions

View file

@ -2,12 +2,16 @@
## 0.8-dev
* Schema changes: moved `full_name` field from table `userprofile` to table `user` for search improvement reasons.
* Added the admin dashboard, accessible from `/admin/` via basic auth. Only users with admin right can access it. Added endpoints `admin.reports` and `admin.reports_detail`.
* Safety is our top priority: added the ability to report someone other's post for everything violating the site's Terms of Service. The current reasons for reporting are: spam, impersonation, pornography, violence, harassment or bullying, hate speech or symbols, self injury, sale or promotion of firearms or drugs, and underage use.
* Schema changes: moved `full_name` field from table `userprofile` to table `user` for search improvement reasons. Added `Report` model.
* Now `profile_search` API endpoint searches by full name too.
* Adding `messages_count`, `followers_count` and `following_count` to `profile_info` API endpoint (what I've done to 0.7.1 too).
* Adding `create2` API endpoint that accepts media, due to an issue with the `create` endpoint that would make it incompatible.
* Adding media URLs to messages in API.
* Added `relationships_follow`, `relationships_unfollow`, `username_availability` and `edit_profile` endpoints to API.
* Added `url` utility to model `Upload`.
* Changed default `robots.txt`, adding report and admin-related lines.
## 0.7.1-dev

View file

@ -6,6 +6,8 @@ To run the app, do "flask run" in the package's parent directory.
Based on Tweepee example of [peewee](https://github.com/coleifer/peewee/).
This is the server. For the client, see [coriplusapp](https://github.com/sakuragasaki46/coriplusapp/).
## Features
* Create text statuses, optionally with image

View file

@ -7,6 +7,9 @@ This module also contains very basic web hooks, such as robots.txt.
For the website hooks, see `app.website`.
For the AJAX hook, see `app.ajax`.
For public API, see `app.api`.
For report pages, see `app.reports`.
For site administration, see `app.admin`.
For template filters, see `app.filters`.
For the database models, see `app.models`.
For other, see `app.utils`.
@ -118,4 +121,8 @@ app.register_blueprint(bp)
from .api import bp
app.register_blueprint(bp)
from .reports import bp
app.register_blueprint(bp)
from .admin import bp
app.register_blueprint(bp)

66
app/admin.py Normal file
View file

@ -0,0 +1,66 @@
'''
Management of reports and the entire site.
New in 0.8.
'''
from flask import Blueprint, redirect, render_template, request, url_for
from .models import User, Message, Report, report_reasons, REPORT_STATUS_ACCEPTED, \
REPORT_MEDIA_USER, REPORT_MEDIA_MESSAGE
from .utils import pwdhash, object_list
from functools import wraps
bp = Blueprint('admin', __name__, url_prefix='/admin')
def check_auth(username, password):
try:
return User.get((User.username == username) & (User.password == pwdhash(password))
).is_admin
except User.DoesNotExist:
return False
def admin_required(f):
@wraps(f)
def wrapped_view(**kwargs):
auth = request.authorization
if not (auth and check_auth(auth.username, auth.password)):
return ('Unauthorized', 401, {
'WWW-Authenticate': 'Basic realm="Login Required"'
})
return f(**kwargs)
return wrapped_view
def review_reports(status, media_type, media_id):
(Report
.update(status=status)
.where((Report.media_type == media_type) & (Report.media_id == media_id))
.execute())
if status == REPORT_STATUS_ACCEPTED:
if media_type == REPORT_MEDIA_USER:
user = User[media_id]
user.is_disabled = 2
user.save()
elif media_type == REPORT_MEDIA_MESSAGE:
Message.delete().where(Message.id == media_id).execute()
@bp.route('/')
@admin_required
def homepage():
return render_template('admin_home.html')
@bp.route('/reports')
@admin_required
def reports():
return object_list('admin_reports.html', Report.select().order_by(Report.created_date.desc()), 'report_list', report_reasons=dict(report_reasons))
@bp.route('/reports/<int:id>', methods=['GET', 'POST'])
@admin_required
def reports_detail(id):
report = Report[id]
if request.method == 'POST':
if request.form.get('take_down'):
review_reports(REPORT_STATUS_ACCEPTED, report.media_type, report.media_id)
elif request.form.get('discard'):
review_reports(REPORT_STATUS_DECLINED, report.media_type, report.media_id)
return redirect(url_for('admin.reports'))
return render_template('admin_report_detail.html', report=report, report_reasons=dict(report_reasons))

View file

@ -241,14 +241,15 @@ def relationships_unfollow(self, userid):
@validate_access
def profile_search(self):
data = request.get_json(True)
query = User.select().where(User.username ** ('%' + data['q'] + '%')).limit(20)
query = User.select().where((User.username ** ('%' + data['q'] + '%')) |
(User.full_name ** ('%' + data['q'] + '%'))).limit(20)
results = []
for result in query:
profile = result.profile
results.append({
"id": result.id,
"username": result.username,
"full_name": profile.full_name,
"full_name": result.full_name,
"followers_count": len(result.followers())
})
return {

View file

@ -199,10 +199,60 @@ class Notification(BaseModel):
pub_date = DateTimeField()
seen = IntegerField(default=0)
REPORT_MEDIA_USER = 1
REPORT_MEDIA_MESSAGE = 2
REPORT_REASON_SPAM = 1
REPORT_REASON_IMPERSONATION = 2
REPORT_REASON_PORN = 3
REPORT_REASON_VIOLENCE = 4
REPORT_REASON_HATE = 5
REPORT_REASON_BULLYING = 6
REPORT_REASON_SELFINJURY = 7
REPORT_REASON_FIREARMS = 8
REPORT_REASON_DRUGS = 9
REPORT_REASON_UNDERAGE = 10
report_reasons = [
(REPORT_REASON_SPAM, "It's spam"),
(REPORT_REASON_IMPERSONATION, "This profile is pretending to be someone else"),
(REPORT_REASON_PORN, "Nudity or pornography"),
(REPORT_REASON_VIOLENCE, "Violence or dangerous organization"),
(REPORT_REASON_HATE, "Hate speech or symbols"),
(REPORT_REASON_BULLYING, "Harassment or bullying"),
(REPORT_REASON_SELFINJURY, "Self injury"),
(REPORT_REASON_FIREARMS, "Sale or promotion of firearms"),
(REPORT_REASON_DRUGS, "Sale or promotion of drugs"),
(REPORT_REASON_UNDERAGE, "This user is less than 13 years old"),
]
REPORT_STATUS_DELIVERED = 0
REPORT_STATUS_ACCEPTED = 1
REPORT_STATUS_DECLINED = 2
# New in 0.8.
class Report(BaseModel):
media_type = IntegerField()
media_id = IntegerField()
sender = ForeignKeyField(User, null=True)
reason = IntegerField()
status = IntegerField(default=REPORT_STATUS_DELIVERED)
created_date = DateTimeField()
@property
def media(self):
try:
if self.media_type == REPORT_MEDIA_USER:
return User[self.media_id]
elif self.media_type == REPORT_MEDIA_MESSAGE:
return Message[self.media_id]
except DoesNotExist:
return
def create_tables():
with database:
database.create_tables([
User, UserAdminship, UserProfile, Message, Relationship,
Upload, Notification])
Upload, Notification, Report])
if not os.path.isdir(UPLOAD_DIRECTORY):
os.makedirs(UPLOAD_DIRECTORY)

42
app/reports.py Normal file
View file

@ -0,0 +1,42 @@
'''
Module for user and message reports.
New in 0.8.
'''
from flask import Blueprint, redirect, request, render_template, url_for
from .models import Report, REPORT_MEDIA_USER, REPORT_MEDIA_MESSAGE, report_reasons
from .utils import get_current_user
import datetime
bp = Blueprint('reports', __name__, url_prefix='/report')
@bp.route('/user/<int:userid>', methods=['GET', 'POST'])
def report_user(userid):
if request.method == "POST":
Report.create(
media_type=REPORT_MEDIA_USER,
media_id=userid,
sender=get_current_user(),
reason=request.form['reason'],
created_date=datetime.datetime.now()
)
return redirect(url_for('reports.report_done'))
return render_template('report_user.html', report_reasons=report_reasons)
@bp.route('/message/<int:userid>', methods=['GET', 'POST'])
def report_message(userid):
if request.method == "POST":
Report.create(
media_type=REPORT_MEDIA_MESSAGE,
media_id=userid,
sender=get_current_user(),
reason=request.form['reason'],
created_date=datetime.datetime.now()
)
return redirect(url_for('reports.report_done'))
return render_template('report_message.html', report_reasons=report_reasons)
@bp.route('/done', methods=['GET', 'POST'])
def report_done():
return render_template('report_done.html')

View file

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<title>{{ site_name }}</title>
<link rel="stylesheet" type="text/css" href="/static/style.css">
<style>.done{opacity:.5}</style>
</head>
<body>
<div class="header">
<h1><a href="{{ url_for('admin.homepage') }}">{{ site_name }} Admin</a></h1>
<div class="metanav">
<!-- what does it go here? -->
</div>
</div>
<div class="content">
{% for message in get_flashed_messages() %}
<div class="flash">{{ message }}</div>
{% endfor %}
{% block body %}{% endblock %}
</div>
<div class="footer">
<p class="copyright">&copy; 2019 Sakuragasaki46.
<a href="/about/">About</a> - <a href="/terms/">Terms</a> -
<a href="/privacy/">Privacy</a></p>
</div>
<script src="/static/lib.js"></script>
</body>
</html>

View file

@ -0,0 +1,9 @@
{% extends "admin_base.html" %}
{% block body %}
<ul>
<li>
<a href="{{ url_for('admin.reports') }}">Reports</a>
</li>
</ul>
{% endblock %}

View file

@ -0,0 +1,27 @@
{% extends "admin_base.html" %}
{% block body %}
<h2>Report detail #{{ report.id }}</h2>
<p>Type: {{ [None, 'user', 'message'][report.media_type] }}</p>
<p>Reason: <strong>{{ report_reasons[report.reason] }}</strong></p>
<p>Status: <strong>{{ ['Unreviewed', 'Accepted', 'Declined'][report.status] }}</strong></p>
<h3>Detail</h3>
{% if report.media is none %}
<p><em>The media is unavailable.</em></p>
{% elif report.media_type == 1 %}
<p><em>Showing first 20 messages of the reported user.</em></p>
<ul>
{% for message in report.media.messages %}
{% include "includes/reported_message.html" %}
{% endfor %}
</ul>
{% elif report.media_type == 2 %}
{% set message = report.media %}
{% include "includes/reported_message.html" %}
{% endif %}
<form method="POST">
<input type="submit" name="take_down" value="Take down">
<input type="submit" name="discard" value="Discard">
</form>
{% endblock %}

View file

@ -0,0 +1,16 @@
{% extends "admin_base.html" %}
{% block body %}
<ul>
{% for report in report_list %}
<li {% if report.status > 0 %}class="done"{% endif %}>
<p><strong>#{{ report.id }}</strong>
(<a href="{{ url_for('admin.reports_detail', id=report.id) }}">detail</a>)</p>
<p>Type: {{ [None, 'user', 'message'][report.media_type] }}</p>
<p>Reason: <strong>{{ report_reasons[report.reason] }}</strong></p>
<p>Status: <strong>{{ ['Unreviewed', 'Accepted', 'Declined'][report.status] }}</strong></p>
</li>
{% endfor %}
</ul>
{% include "includes/pagination.html" %}
{% endblock %}

View file

@ -22,6 +22,6 @@
<li><a href="/edit/{{ message.id }}">Edit or change privacy</a></li>
<li><a href="/delete/{{ message.id }}">Delete permanently</a></li>
{% else %}
<!--li><a href="/report/{{ message.id }}">Report</a></li-->
<li><a href="/report/message/{{ message.id }}" target="_blank">Report</a></li>
{% endif %}
</ul>

View file

@ -0,0 +1,15 @@
<div>
<p><strong>Message #{{ message.id }}</strong> (<a href="{# url_for('admin.message_info', id=message.id) #}">detail</a>)</p>
<p>Author: <a href="{# url_for('admin.user_detail', id=message.user_id #}">{{ message.user.username }}</a></p>
<p>Text:</p>
<div style="border:1px solid gray;padding:12px">
{{ message.text|enrich }}
{% if message.uploads %}
<div>
<img src="/uploads/{{ message.uploads[0].filename() }}">
</div>
{% endif %}
</div>
<p>Privacy: {{ ['public', 'unlisted', 'friends', 'only me'][message.privacy] }}</p>
<p>Date: {{ message.pub_date.strftime('%B %-d, %Y %H:%M:%S') }}</p>
</div>

View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<title>Report &ndash; Cori+</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body{margin:0;font-family:sans-serif}
.item{padding:12px;border-bottom:1px solid gray}
.item h2{font-size:1em;margin:6px 0}
</style>
</head>
<body>
<div class="content">
{% block body %}{% endblock %}
</div>
<form id="reportForm" method="POST">
<input id="reportFormValue" name="reason" type="hidden" value="">
<input type="submit" style="display:none">
</form>
<script>
function submitReport(value){
reportFormValue.value = value;
reportForm.submit();
}
</script>
</body>
</html>

View file

@ -0,0 +1,11 @@
{% extends "report_base.html" %}
{% block body %}
<div class="item">
<h1>Done</h1>
<p>Your report has been sent.<br />
We'll review the user or message, and, if against our Community
Guidelines, we'll remove it.</p>
</div>
{% endblock %}

View file

@ -0,0 +1,11 @@
{% extends "report_base.html" %}
{% block body %}
{% for reason in report_reasons %}
<form method="POST">
<div class="item" onclick="submitReport({{ reason[0] }})">
<h2>{{ reason[1] }}</h2>
</div>
</form>
{% endfor %}
{% endblock %}

View file

@ -0,0 +1,11 @@
{% extends "report_base.html" %}
{% block body %}
{% for reason in report_reasons %}
<form method="POST">
<div class="item" onclick="submitReport({{ reason[0] }})">
<h2>{{ reason[1] }}</h2>
</div>
</form>
{% endfor %}
{% endblock %}

View file

@ -1 +1,3 @@
User-Agent: *
Disallow: /report/
Noindex: /admin/