Archived
1
0

more, add initial mod ui

This commit is contained in:
Jeff Becker 2016-11-16 10:19:00 -05:00
parent 6f01bac76c
commit c677cebde5
16 changed files with 335 additions and 28 deletions

View File

@ -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)),
],
),
]

View File

@ -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)
]

View File

@ -8,6 +8,8 @@ from . import util
import mimetypes import mimetypes
from datetime import datetime from datetime import datetime
import os
class Attachment(models.Model): class Attachment(models.Model):
""" """
a file attachment assiciated with a post a file attachment assiciated with a post
@ -24,12 +26,19 @@ class Attachment(models.Model):
ext = self.filename.split('.')[-1] ext = self.filename.split('.')[-1]
return '{}.{}'.format(self.filehash, ext) return '{}.{}'.format(self.filehash, ext)
def thumb(self): def thumb(self, root=settings.MEDIA_URL):
return '{}thumb-{}.jpg'.format(settings.MEDIA_URL, self.path()) return '{}thumb-{}.jpg'.format(root, self.path())
def source(self): def source(self, root=settings.MEDIA_URL):
return '{}{}'.format(settings.MEDIA_URL, self.path()) 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): class Newsgroup(models.Model):
""" """
@ -58,23 +67,33 @@ class Post(models.Model):
name = models.CharField(max_length=256, default='Anonymous') name = models.CharField(max_length=256, default='Anonymous')
pubkey = models.CharField(max_length=64, default='') pubkey = models.CharField(max_length=64, default='')
signature = 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) attachments = models.ManyToManyField(Attachment)
posted = models.IntegerField(default=0) posted = models.IntegerField(default=0)
placeholder = models.BooleanField(default=False) placeholder = models.BooleanField(default=False)
last_bumped = models.IntegerField(default=0) last_bumped = models.IntegerField(default=0)
def has_attachment(self, filehash): def has_attachment(self, filehash):
"""
return True if we own a file attachment by its hash
"""
for att in self.attachments.all(): for att in self.attachments.all():
if att.filehash == filehash: if att.filehash in filehash:
return True return True
return False return False
def get_all_replies(self): def get_all_replies(self):
"""
get all replies to this thread
"""
if self.is_op(): if self.is_op():
return Post.objects.filter(reference=self.msgid).order_by('posted') return Post.objects.filter(reference=self.msgid).order_by('posted')
def get_board_replies(self, truncate=5): def get_board_replies(self, truncate=5):
"""
get replies to this thread
truncate to last N replies
"""
rpls = self.get_all_replies() rpls = self.get_all_replies()
l = len(rpls) l = len(rpls)
if l > truncate: if l > truncate:
@ -91,6 +110,9 @@ class Post(models.Model):
return datetime.fromtimestamp(self.posted) return datetime.fromtimestamp(self.posted)
def get_absolute_url(self): def get_absolute_url(self):
"""
self explainitory
"""
if self.is_op(): if self.is_op():
op = util.hashid(self.msgid) op = util.hashid(self.msgid)
return reverse('frontend:thread', args=[op]) return reverse('frontend:thread', args=[op])
@ -100,5 +122,100 @@ class Post(models.Model):
return reverse('frontend:thread', args=[op]) + '#{}'.format(frag) return reverse('frontend:thread', args=[op]) + '#{}'.format(frag)
def bump(self, last): def bump(self, last):
"""
bump thread
"""
if self.is_op(): if self.is_op():
self.last_bumped = last 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

View File

@ -4,8 +4,9 @@ from . import views
urlpatterns = [ urlpatterns = [
url(r'^ctl-(?P<page>[0-9]+)\.html$', views.modlog, name='old-modlog'), url(r'^ctl-(?P<page>[0-9]+)\.html$', views.modlog, name='old-modlog'),
url(r'^mod/$', views.modlog, name='modlog'), url(r'^mod/$', views.ModView.as_view(), name='mod'),
url(r'^mod/(?P<page>[0-9]+)$', views.modlog, name='modlog-page'), 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\.]+)-(?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'^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'), url(r'^thread-(?P<op>[a-fA-F0-9\.]{40})\.html$', views.ThreadView.as_view(), name='old-thread'),

View File

@ -14,6 +14,18 @@ import random
import nntplib import nntplib
import email.message 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): def hashid(msgid):
h = hashlib.sha1() h = hashlib.sha1()
m = '{}'.format(msgid).encode('ascii') 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' msg['Content-Type'] = 'text/plain; charset=UTF-8'
m = '{}'.format(form['message'] or ' ') m = '{}'.format(form['message'] or ' ')
msg.set_payload(m) 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: if secretKey:
msg['Path'] = settings.FRONTEND_NAME msg['Path'] = settings.FRONTEND_NAME
# sign # sign

View File

@ -5,7 +5,7 @@ from django.shortcuts import render, get_object_or_404
from django.views import generic from django.views import generic
from .models import Post, Newsgroup from .models import Post, Newsgroup, ModPriv
from captcha.image import ImageCaptcha from captcha.image import ImageCaptcha
from . import util from . import util
@ -54,17 +54,26 @@ class Postable:
'error' : 'invalid captcha', 'error' : 'invalid captcha',
'only_mod': False 'only_mod': False
} }
solution = request.session['captcha'] # bypass captcha for mod users
if solution is not None: mod = None
if 'captcha' in request.POST: if 'mod' in request.session:
if request.POST['captcha'].lower() == solution.lower(): mod = request.session['mod']
processed, err = self.handle_mod(request) if mod is None:
if processed: solution = request.session['captcha']
ctx['error'] = err or 'report made' if solution is not None:
ctx['msgid'] = '' if 'captcha' in request.POST:
else: if request.POST['captcha'].lower() == solution.lower():
ctx['msgid'], ctx['error'] = self.handle_post(request, **kwargs) processed, err = self.handle_mod(request)
request.session['captcha'] = '' 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() request.session.save()
code = 201 code = 201
if ctx['error']: if ctx['error']:
@ -146,7 +155,66 @@ class FrontPageView(generic.View):
ctx = {'posts' : posts} ctx = {'posts' : posts}
return render(request, self.template_name, ctx) 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): def modlog(request, page=None):
page = int(page or '0') page = int(page or '0')
ctx = { ctx = {
@ -171,3 +239,11 @@ def create_captcha(request):
r =HttpResponse(c) r =HttpResponse(c)
r['Content-Type'] = 'image/png' r['Content-Type'] = 'image/png'
return r return r
def keygen(request):
"""
generate new keypair
"""
ctx = {}
ctx['sk'], ctx['pk'] = util.keygen()
return render(request, 'frontend/keygen.html', ctx)

View File

@ -77,7 +77,7 @@ DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2', 'ENGINE': 'django.db.backends.postgresql_psycopg2',
'HOST': '/var/run/postgresql', 'HOST': '/var/run/postgresql',
'NAME': 'postgres', 'NAME': 'jeff',
} }
} }

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -0,0 +1,7 @@
{% extends "frontend/base.html" %}
{% block head %}
<meta http-equiv="refresh" content="1; {{url}}" />
{% endblock %}
{% block content %}
<pre>{{msg}}</pre>
{% endblock %}

View File

@ -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 %}

View File

@ -21,6 +21,6 @@ from . import views
urlpatterns = [ urlpatterns = [
url(r'^webhook$', views.webhook), 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) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns.append(url(r'', views.frontpage, name='frontpage')) urlpatterns.append(url(r'', views.frontpage, name='frontpage'))

View File

@ -139,3 +139,5 @@ def webhook(request):
return JsonResponse({ 'error': '{}'.format(ex) }) return JsonResponse({ 'error': '{}'.format(ex) })
else: else:
return JsonResponse({'posted': True}) return JsonResponse({'posted': True})

View File

@ -7,10 +7,10 @@ django frontend for nntpchan
suggested setup is with pyvenv suggested setup is with pyvenv
for a dev server, run: for a dev server, run:
cd nntpchan
python3 -m venv v python3 -m venv v
v/bin/pip install -r requirements.txt v/bin/pip install -r requirements.txt
cd nntpchan v/bin/python manage.py migrate
../v/bin/python manage.py migrate v/bin/pyrhon manage.py runserver
../v/bin/pyrhon manage.py runserver