From c677cebde52c79979b21bb0ded3a5b8bc286a503 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Wed, 16 Nov 2016 10:19:00 -0500 Subject: [PATCH] more, add initial mod ui --- .../frontend/migrations/0002_modpriv.py | 24 ++++ .../migrations/0003_auto_20161116_1454.py | 24 ++++ .../nntpchan/nntpchan/frontend/models.py | 131 +++++++++++++++++- .../django/nntpchan/nntpchan/frontend/urls.py | 5 +- .../django/nntpchan/nntpchan/frontend/util.py | 14 +- .../nntpchan/nntpchan/frontend/views.py | 100 +++++++++++-- .../django/nntpchan/nntpchan/settings.py | 2 +- .../nntpchan/templates/frontend/keygen.html | 8 ++ .../nntpchan/templates/frontend/modlogin.html | 11 ++ .../nntpchan/templates/frontend/modpage.html | 10 ++ .../nntpchan/templates/frontend/redirect.html | 7 + .../nntpchan/nntpchan/templates/keygen.html | 15 ++ .../django/nntpchan/nntpchan/urls.py | 2 +- .../django/nntpchan/nntpchan/views.py | 2 + .../django/{ => nntpchan}/requirements.txt | 0 contrib/frontends/django/readme.md | 8 +- 16 files changed, 335 insertions(+), 28 deletions(-) create mode 100644 contrib/frontends/django/nntpchan/nntpchan/frontend/migrations/0002_modpriv.py create mode 100644 contrib/frontends/django/nntpchan/nntpchan/frontend/migrations/0003_auto_20161116_1454.py create mode 100644 contrib/frontends/django/nntpchan/nntpchan/templates/frontend/keygen.html create mode 100644 contrib/frontends/django/nntpchan/nntpchan/templates/frontend/modlogin.html create mode 100644 contrib/frontends/django/nntpchan/nntpchan/templates/frontend/modpage.html create mode 100644 contrib/frontends/django/nntpchan/nntpchan/templates/frontend/redirect.html create mode 100644 contrib/frontends/django/nntpchan/nntpchan/templates/keygen.html rename contrib/frontends/django/{ => nntpchan}/requirements.txt (100%) diff --git a/contrib/frontends/django/nntpchan/nntpchan/frontend/migrations/0002_modpriv.py b/contrib/frontends/django/nntpchan/nntpchan/frontend/migrations/0002_modpriv.py new file mode 100644 index 0000000..a246a90 --- /dev/null +++ b/contrib/frontends/django/nntpchan/nntpchan/frontend/migrations/0002_modpriv.py @@ -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)), + ], + ), + ] diff --git a/contrib/frontends/django/nntpchan/nntpchan/frontend/migrations/0003_auto_20161116_1454.py b/contrib/frontends/django/nntpchan/nntpchan/frontend/migrations/0003_auto_20161116_1454.py new file mode 100644 index 0000000..bb28fdc --- /dev/null +++ b/contrib/frontends/django/nntpchan/nntpchan/frontend/migrations/0003_auto_20161116_1454.py @@ -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) + ] diff --git a/contrib/frontends/django/nntpchan/nntpchan/frontend/models.py b/contrib/frontends/django/nntpchan/nntpchan/frontend/models.py index 824aa67..19ded9e 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/frontend/models.py +++ b/contrib/frontends/django/nntpchan/nntpchan/frontend/models.py @@ -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 diff --git a/contrib/frontends/django/nntpchan/nntpchan/frontend/urls.py b/contrib/frontends/django/nntpchan/nntpchan/frontend/urls.py index 6042a06..f4d2fa0 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/frontend/urls.py +++ b/contrib/frontends/django/nntpchan/nntpchan/frontend/urls.py @@ -4,8 +4,9 @@ from . import views urlpatterns = [ url(r'^ctl-(?P[0-9]+)\.html$', views.modlog, name='old-modlog'), - url(r'^mod/$', views.modlog, name='modlog'), - url(r'^mod/(?P[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[0-9]+)$', views.modlog, name='mod-page'), url(r'^overchan\.(?P[a-zA-Z0-9\.]+)-(?P[0-9]+)\.html$', views.BoardView.as_view(), name='old-board'), url(r'^overchan\.(?P[a-zA-Z0-9\.]+)/', views.BoardView.as_view(), name='board-alt'), url(r'^thread-(?P[a-fA-F0-9\.]{40})\.html$', views.ThreadView.as_view(), name='old-thread'), diff --git a/contrib/frontends/django/nntpchan/nntpchan/frontend/util.py b/contrib/frontends/django/nntpchan/nntpchan/frontend/util.py index b781180..f8008c7 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/frontend/util.py +++ b/contrib/frontends/django/nntpchan/nntpchan/frontend/util.py @@ -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 diff --git a/contrib/frontends/django/nntpchan/nntpchan/frontend/views.py b/contrib/frontends/django/nntpchan/nntpchan/frontend/views.py index 616a094..0c35a16 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/frontend/views.py +++ b/contrib/frontends/django/nntpchan/nntpchan/frontend/views.py @@ -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']: @@ -146,7 +155,66 @@ class FrontPageView(generic.View): ctx = {'posts' : posts} 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) diff --git a/contrib/frontends/django/nntpchan/nntpchan/settings.py b/contrib/frontends/django/nntpchan/nntpchan/settings.py index be92b57..64668de 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/settings.py +++ b/contrib/frontends/django/nntpchan/nntpchan/settings.py @@ -77,7 +77,7 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'HOST': '/var/run/postgresql', - 'NAME': 'postgres', + 'NAME': 'jeff', } } diff --git a/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/keygen.html b/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/keygen.html new file mode 100644 index 0000000..9b65053 --- /dev/null +++ b/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/keygen.html @@ -0,0 +1,8 @@ +{% extends "frontend/base.html" %} +{% block title %} new tripcode keypair {% endblock %} +{% block content %} +
public key: {{pk}}
+secret key: {{sk}}
+
+{% endblock %} + diff --git a/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/modlogin.html b/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/modlogin.html new file mode 100644 index 0000000..2bb4925 --- /dev/null +++ b/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/modlogin.html @@ -0,0 +1,11 @@ +{% extends "frontend/base.html" %} +{% block content %} +
+ {% csrf_token %} + + + + +
+ +{% endblock %} diff --git a/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/modpage.html b/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/modpage.html new file mode 100644 index 0000000..53adc31 --- /dev/null +++ b/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/modpage.html @@ -0,0 +1,10 @@ +{% extends "frontend/base.html" %} +{% block content %} +
+ {% csrf_token %} + + +
+mod page + +{% endblock %} diff --git a/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/redirect.html b/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/redirect.html new file mode 100644 index 0000000..0ed877c --- /dev/null +++ b/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/redirect.html @@ -0,0 +1,7 @@ +{% extends "frontend/base.html" %} +{% block head %} + +{% endblock %} +{% block content %} +
{{msg}}
+{% endblock %} diff --git a/contrib/frontends/django/nntpchan/nntpchan/templates/keygen.html b/contrib/frontends/django/nntpchan/nntpchan/templates/keygen.html new file mode 100644 index 0000000..410246b --- /dev/null +++ b/contrib/frontends/django/nntpchan/nntpchan/templates/keygen.html @@ -0,0 +1,15 @@ +{% extends "frontend/base.html" %} +{% block title %} new tripcode keypair {% endblock %} +{% block content %} +
+
+ public key: + {{pk}} +
+
+ secret key: + {{sk}} +
+
+{% endblock %} + diff --git a/contrib/frontends/django/nntpchan/nntpchan/urls.py b/contrib/frontends/django/nntpchan/nntpchan/urls.py index 4142f27..f40c38c 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/urls.py +++ b/contrib/frontends/django/nntpchan/nntpchan/urls.py @@ -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')) diff --git a/contrib/frontends/django/nntpchan/nntpchan/views.py b/contrib/frontends/django/nntpchan/nntpchan/views.py index 1093711..c1a2110 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/views.py +++ b/contrib/frontends/django/nntpchan/nntpchan/views.py @@ -139,3 +139,5 @@ def webhook(request): return JsonResponse({ 'error': '{}'.format(ex) }) else: return JsonResponse({'posted': True}) + + diff --git a/contrib/frontends/django/requirements.txt b/contrib/frontends/django/nntpchan/requirements.txt similarity index 100% rename from contrib/frontends/django/requirements.txt rename to contrib/frontends/django/nntpchan/requirements.txt diff --git a/contrib/frontends/django/readme.md b/contrib/frontends/django/readme.md index ba76925..8a9a493 100644 --- a/contrib/frontends/django/readme.md +++ b/contrib/frontends/django/readme.md @@ -7,10 +7,10 @@ django frontend for nntpchan 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