more, add initial mod ui
This commit is contained in:
parent
6f01bac76c
commit
c677cebde5
@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.3 on 2016-11-16 14:37
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('frontend', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ModPriv',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('board', models.CharField(default='all', max_length=128)),
|
||||
('level', models.IntegerField(default=3)),
|
||||
('pubkey', models.CharField(editable=False, max_length=256)),
|
||||
],
|
||||
),
|
||||
]
|
@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.3 on 2016-11-16 14:54
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
from nntpchan.frontend import util
|
||||
|
||||
def make_admin_key(apps, schema_editor):
|
||||
ModPriv = apps.get_model("frontend", "ModPriv")
|
||||
sk, pk = util.keygen()
|
||||
print("!!!!! YOUR ADMIN SECRET KEY IS {} !!!!!".format(sk))
|
||||
mod = ModPriv(level=0, pubkey=pk)
|
||||
mod.save()
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('frontend', '0002_modpriv'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(make_admin_key)
|
||||
]
|
@ -8,6 +8,8 @@ from . import util
|
||||
import mimetypes
|
||||
from datetime import datetime
|
||||
|
||||
import os
|
||||
|
||||
class Attachment(models.Model):
|
||||
"""
|
||||
a file attachment assiciated with a post
|
||||
@ -24,12 +26,19 @@ class Attachment(models.Model):
|
||||
ext = self.filename.split('.')[-1]
|
||||
return '{}.{}'.format(self.filehash, ext)
|
||||
|
||||
def thumb(self):
|
||||
return '{}thumb-{}.jpg'.format(settings.MEDIA_URL, self.path())
|
||||
def thumb(self, root=settings.MEDIA_URL):
|
||||
return '{}thumb-{}.jpg'.format(root, self.path())
|
||||
|
||||
def source(self):
|
||||
return '{}{}'.format(settings.MEDIA_URL, self.path())
|
||||
def source(self, root=settings.MEDIA_URL):
|
||||
return '{}{}'.format(root, self.path())
|
||||
|
||||
def remove(self):
|
||||
"""
|
||||
remove from filesystem and delete self
|
||||
"""
|
||||
os.unlink(os.path.join(settings.MEDIA_ROOT, self.thumb('')))
|
||||
os.unlink(os.path.join(settings.MEDIA_ROOT, self.source('')))
|
||||
self.delete()
|
||||
|
||||
class Newsgroup(models.Model):
|
||||
"""
|
||||
@ -58,23 +67,33 @@ class Post(models.Model):
|
||||
name = models.CharField(max_length=256, default='Anonymous')
|
||||
pubkey = models.CharField(max_length=64, default='')
|
||||
signature = models.CharField(max_length=64, default='')
|
||||
newsgroup = models.ForeignKey(Newsgroup, on_delete=models.CASCADE)
|
||||
newsgroup = models.ForeignKey(Newsgroup)
|
||||
attachments = models.ManyToManyField(Attachment)
|
||||
posted = models.IntegerField(default=0)
|
||||
placeholder = models.BooleanField(default=False)
|
||||
last_bumped = models.IntegerField(default=0)
|
||||
|
||||
def has_attachment(self, filehash):
|
||||
"""
|
||||
return True if we own a file attachment by its hash
|
||||
"""
|
||||
for att in self.attachments.all():
|
||||
if att.filehash == filehash:
|
||||
if att.filehash in filehash:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_all_replies(self):
|
||||
"""
|
||||
get all replies to this thread
|
||||
"""
|
||||
if self.is_op():
|
||||
return Post.objects.filter(reference=self.msgid).order_by('posted')
|
||||
|
||||
def get_board_replies(self, truncate=5):
|
||||
"""
|
||||
get replies to this thread
|
||||
truncate to last N replies
|
||||
"""
|
||||
rpls = self.get_all_replies()
|
||||
l = len(rpls)
|
||||
if l > truncate:
|
||||
@ -91,6 +110,9 @@ class Post(models.Model):
|
||||
return datetime.fromtimestamp(self.posted)
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""
|
||||
self explainitory
|
||||
"""
|
||||
if self.is_op():
|
||||
op = util.hashid(self.msgid)
|
||||
return reverse('frontend:thread', args=[op])
|
||||
@ -100,5 +122,100 @@ class Post(models.Model):
|
||||
return reverse('frontend:thread', args=[op]) + '#{}'.format(frag)
|
||||
|
||||
def bump(self, last):
|
||||
"""
|
||||
bump thread
|
||||
"""
|
||||
if self.is_op():
|
||||
self.last_bumped = last
|
||||
|
||||
def remove(self):
|
||||
"""
|
||||
remove post and all attachments
|
||||
"""
|
||||
for att in self.attachments.all():
|
||||
att.remove()
|
||||
self.delete()
|
||||
|
||||
class ModPriv(models.Model):
|
||||
"""
|
||||
a record that permits moderation actions on certain boards or globally
|
||||
"""
|
||||
|
||||
"""
|
||||
absolute power :^DDDDDDD (does not exist)
|
||||
"""
|
||||
GOD = 0
|
||||
|
||||
"""
|
||||
node admin
|
||||
"""
|
||||
ADMIN = 1
|
||||
|
||||
"""
|
||||
can ban, delete and edit posts
|
||||
"""
|
||||
MOD = 2
|
||||
|
||||
"""
|
||||
can only delete
|
||||
"""
|
||||
JANITOR = 3
|
||||
|
||||
"""
|
||||
lowest access level for login
|
||||
"""
|
||||
LOWEST = JANITOR
|
||||
|
||||
"""
|
||||
what board this priviledge is for or 'all' for global
|
||||
"""
|
||||
board = models.CharField(max_length=128, default='all')
|
||||
|
||||
"""
|
||||
what level of priviledge is granted
|
||||
"""
|
||||
level = models.IntegerField(default=3)
|
||||
|
||||
"""
|
||||
public key of mod mod user
|
||||
"""
|
||||
pubkey = models.CharField(max_length=256, editable=False)
|
||||
|
||||
@staticmethod
|
||||
def has_access(level, pubkey, board_name=None):
|
||||
# check global priviledge
|
||||
global_priv = ModPriv.objects.filter(pubkey=pubkey, board='all')
|
||||
for priv in global_priv:
|
||||
if priv.level <= level:
|
||||
return True
|
||||
# check board level priviledge
|
||||
if board_name:
|
||||
board_priv = ModPriv.objects.filter(pubkey=pubkey, board=board_name)
|
||||
for priv in board_priv:
|
||||
if priv.level <= level:
|
||||
return True
|
||||
# no access allowed
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def try_delete(pubkey, post):
|
||||
"""
|
||||
try deleting a post, return True if it was deleted otherwise return False
|
||||
"""
|
||||
if ModPriv.has_access(ModPriv.JANITOR, pubkey, post.newsgroup.name):
|
||||
# we can do it
|
||||
post.remove()
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def try_edit(pubkey, post, newbody):
|
||||
"""
|
||||
try editing a post by replacing its body with a new one
|
||||
returns True if this was done otherwise return False
|
||||
"""
|
||||
if ModPriv.has_access(ModPriv.MOD, pubkey, post.newsgroup.name):
|
||||
post.message = newbody
|
||||
post.save()
|
||||
return True
|
||||
return False
|
||||
|
@ -4,8 +4,9 @@ from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^ctl-(?P<page>[0-9]+)\.html$', views.modlog, name='old-modlog'),
|
||||
url(r'^mod/$', views.modlog, name='modlog'),
|
||||
url(r'^mod/(?P<page>[0-9]+)$', views.modlog, name='modlog-page'),
|
||||
url(r'^mod/$', views.ModView.as_view(), name='mod'),
|
||||
url(r'^mod/keygen', views.keygen, name='keygen'),
|
||||
url(r'^mod/(?P<page>[0-9]+)$', views.modlog, name='mod-page'),
|
||||
url(r'^overchan\.(?P<name>[a-zA-Z0-9\.]+)-(?P<page>[0-9]+)\.html$', views.BoardView.as_view(), name='old-board'),
|
||||
url(r'^overchan\.(?P<name>[a-zA-Z0-9\.]+)/', views.BoardView.as_view(), name='board-alt'),
|
||||
url(r'^thread-(?P<op>[a-fA-F0-9\.]{40})\.html$', views.ThreadView.as_view(), name='old-thread'),
|
||||
|
@ -14,6 +14,18 @@ import random
|
||||
import nntplib
|
||||
import email.message
|
||||
|
||||
|
||||
def keygen():
|
||||
"""
|
||||
generate a new keypair
|
||||
"""
|
||||
k = nacl.signing.SigningKey.generate()
|
||||
return hexlify(k.encode()).decode('ascii'), hexlify(k.verify_key.encode()).decode('ascii')
|
||||
|
||||
def to_public(sk):
|
||||
k = nacl.signing.SigningKey(sk, nacl.signing.encoding.HexEncoder)
|
||||
return hexlify(k.verify_key.encode()).decode('ascii')
|
||||
|
||||
def hashid(msgid):
|
||||
h = hashlib.sha1()
|
||||
m = '{}'.format(msgid).encode('ascii')
|
||||
@ -80,7 +92,7 @@ def createPost(newsgroup, ref, form, files, secretKey=None):
|
||||
msg['Content-Type'] = 'text/plain; charset=UTF-8'
|
||||
m = '{}'.format(form['message'] or ' ')
|
||||
msg.set_payload(m)
|
||||
msg['Message-Id'] = '<{}${}@signed.{}>'.format(randstr(5), int(time_int(datetime.now())), settings.FRONTEND_NAME)
|
||||
msg['Message-Id'] = '<{}${}@{}>'.format(randstr(5), int(time_int(datetime.now())), settings.FRONTEND_NAME)
|
||||
if secretKey:
|
||||
msg['Path'] = settings.FRONTEND_NAME
|
||||
# sign
|
||||
|
@ -5,7 +5,7 @@ from django.shortcuts import render, get_object_or_404
|
||||
from django.views import generic
|
||||
|
||||
|
||||
from .models import Post, Newsgroup
|
||||
from .models import Post, Newsgroup, ModPriv
|
||||
|
||||
from captcha.image import ImageCaptcha
|
||||
from . import util
|
||||
@ -54,17 +54,26 @@ class Postable:
|
||||
'error' : 'invalid captcha',
|
||||
'only_mod': False
|
||||
}
|
||||
solution = request.session['captcha']
|
||||
if solution is not None:
|
||||
if 'captcha' in request.POST:
|
||||
if request.POST['captcha'].lower() == solution.lower():
|
||||
processed, err = self.handle_mod(request)
|
||||
if processed:
|
||||
ctx['error'] = err or 'report made'
|
||||
ctx['msgid'] = ''
|
||||
else:
|
||||
ctx['msgid'], ctx['error'] = self.handle_post(request, **kwargs)
|
||||
request.session['captcha'] = ''
|
||||
# bypass captcha for mod users
|
||||
mod = None
|
||||
if 'mod' in request.session:
|
||||
mod = request.session['mod']
|
||||
if mod is None:
|
||||
solution = request.session['captcha']
|
||||
if solution is not None:
|
||||
if 'captcha' in request.POST:
|
||||
if request.POST['captcha'].lower() == solution.lower():
|
||||
processed, err = self.handle_mod(request)
|
||||
if processed:
|
||||
ctx['error'] = err or 'report made'
|
||||
ctx['msgid'] = ''
|
||||
else:
|
||||
ctx['msgid'], ctx['error'] = self.handle_post(request, **kwargs)
|
||||
request.session['captcha'] = ''
|
||||
else:
|
||||
# we have a mod session
|
||||
ctx['msgid'], ctx['error'] = self.handle_post(request, **kwargs)
|
||||
|
||||
request.session.save()
|
||||
code = 201
|
||||
if ctx['error']:
|
||||
@ -147,6 +156,65 @@ class FrontPageView(generic.View):
|
||||
return render(request, self.template_name, ctx)
|
||||
|
||||
|
||||
class ModView(generic.View, Postable):
|
||||
|
||||
def show_login(self, request):
|
||||
"""
|
||||
handle login page
|
||||
"""
|
||||
request.session['mod'] = None
|
||||
request.session.save()
|
||||
return render(request, 'frontend/modlogin.html')
|
||||
|
||||
def logout(self, request):
|
||||
request.session['mod'] = None
|
||||
request.session.save()
|
||||
return render(request, 'frontend/redirect.html', { 'url' : reverse('frontend:mod')})
|
||||
|
||||
def post(self, request):
|
||||
action = None
|
||||
if 'action' in request.POST:
|
||||
action = request.POST['action']
|
||||
mod = None
|
||||
msg = 'bad login'
|
||||
if 'mod' in request.session:
|
||||
mod = request.session['mod']
|
||||
if action == 'logout':
|
||||
return self.logout(request)
|
||||
if action == 'login' and 'secret' in request.POST and mod is None:
|
||||
# try login
|
||||
sk = request.POST['secret']
|
||||
pk = None
|
||||
try:
|
||||
pk = util.to_public(sk)
|
||||
print (pk)
|
||||
except:
|
||||
msg = 'bad key format'
|
||||
if pk is not None and ModPriv.has_access(ModPriv.LOWEST, pk):
|
||||
mod = request.session['mod'] = {'sk' : sk, 'pk' : pk}
|
||||
request.session.save()
|
||||
# login okay
|
||||
return render(request, 'frontend/redirect.html', {'url' : reverse('frontend:mod'), 'msg' : 'logged in'})
|
||||
if mod is None:
|
||||
# no mod session
|
||||
return render(request, 'frontend/redirect.html', {'url' : reverse('frontend:mod'), 'msg' : msg } )
|
||||
else:
|
||||
# do mod action
|
||||
return self.handle_mod_action(request)
|
||||
|
||||
def handle_mod_action(self, request):
|
||||
return render(request, 'frontend/redirect.html', {'url' : reverse('frontend:mod')} )
|
||||
|
||||
def get(self, request):
|
||||
mod = None
|
||||
if 'mod' in request.session:
|
||||
mod = request.session['mod']
|
||||
if mod:
|
||||
return render(request, 'frontend/modpage.html', {'mod' : mod })
|
||||
else:
|
||||
return self.show_login(request)
|
||||
|
||||
|
||||
def modlog(request, page=None):
|
||||
page = int(page or '0')
|
||||
ctx = {
|
||||
@ -171,3 +239,11 @@ def create_captcha(request):
|
||||
r =HttpResponse(c)
|
||||
r['Content-Type'] = 'image/png'
|
||||
return r
|
||||
|
||||
def keygen(request):
|
||||
"""
|
||||
generate new keypair
|
||||
"""
|
||||
ctx = {}
|
||||
ctx['sk'], ctx['pk'] = util.keygen()
|
||||
return render(request, 'frontend/keygen.html', ctx)
|
||||
|
@ -77,7 +77,7 @@ DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||
'HOST': '/var/run/postgresql',
|
||||
'NAME': 'postgres',
|
||||
'NAME': 'jeff',
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,8 @@
|
||||
{% extends "frontend/base.html" %}
|
||||
{% block title %} new tripcode keypair {% endblock %}
|
||||
{% block content %}
|
||||
<pre class="tripcode-outer">public key: {{pk}}
|
||||
secret key: {{sk}}
|
||||
</pre>
|
||||
{% endblock %}
|
||||
|
@ -0,0 +1,11 @@
|
||||
{% extends "frontend/base.html" %}
|
||||
{% block content %}
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<label for="secret"> secret key </label>
|
||||
<input type="password" id="secret" name="secret"></input>
|
||||
<input type="hidden" name="action" value="login"></input>
|
||||
<input type="submit" value="login"></input>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
@ -0,0 +1,10 @@
|
||||
{% extends "frontend/base.html" %}
|
||||
{% block content %}
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="logout"></input>
|
||||
<input type="submit" value="logout"></input>
|
||||
</form>
|
||||
mod page
|
||||
|
||||
{% endblock %}
|
@ -0,0 +1,7 @@
|
||||
{% extends "frontend/base.html" %}
|
||||
{% block head %}
|
||||
<meta http-equiv="refresh" content="1; {{url}}" />
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<pre>{{msg}}</pre>
|
||||
{% endblock %}
|
@ -0,0 +1,15 @@
|
||||
{% extends "frontend/base.html" %}
|
||||
{% block title %} new tripcode keypair {% endblock %}
|
||||
{% block content %}
|
||||
<div class="tripcode-outer">
|
||||
<div class="tripcode">
|
||||
public key:
|
||||
<span class="tripcode-public">{{pk}}</span>
|
||||
</div>
|
||||
<div class="tripcode">
|
||||
secret key:
|
||||
<span class="tripcode-secret">{{sk}}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -21,6 +21,6 @@ from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^webhook$', views.webhook),
|
||||
url(r'^nntpchan/', include('nntpchan.frontend.urls', namespace='frontend'), name='nntpchan'),
|
||||
url(r'^nntpchan/', include('nntpchan.frontend.urls', namespace='frontend'), name='nntpchan')
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
urlpatterns.append(url(r'', views.frontpage, name='frontpage'))
|
||||
|
@ -139,3 +139,5 @@ def webhook(request):
|
||||
return JsonResponse({ 'error': '{}'.format(ex) })
|
||||
else:
|
||||
return JsonResponse({'posted': True})
|
||||
|
||||
|
||||
|
@ -8,9 +8,9 @@ suggested setup is with pyvenv
|
||||
|
||||
for a dev server, run:
|
||||
|
||||
cd nntpchan
|
||||
python3 -m venv v
|
||||
v/bin/pip install -r requirements.txt
|
||||
cd nntpchan
|
||||
../v/bin/python manage.py migrate
|
||||
../v/bin/pyrhon manage.py runserver
|
||||
v/bin/python manage.py migrate
|
||||
v/bin/pyrhon manage.py runserver
|
||||
|
||||
|
Reference in New Issue
Block a user