diff --git a/contrib/frontends/django/nntpchan/assets/fonts/FreeMono.ttf b/contrib/frontends/django/nntpchan/assets/fonts/FreeMono.ttf new file mode 100644 index 0000000..ff5cc0e Binary files /dev/null and b/contrib/frontends/django/nntpchan/assets/fonts/FreeMono.ttf differ diff --git a/contrib/frontends/django/nntpchan/nntpchan/frontend/models.py b/contrib/frontends/django/nntpchan/nntpchan/frontend/models.py index ba4de79..3174e29 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/frontend/models.py +++ b/contrib/frontends/django/nntpchan/nntpchan/frontend/models.py @@ -61,6 +61,12 @@ class Post(models.Model): posted = models.IntegerField(default=0) placeholder = models.BooleanField(default=False) last_bumped = models.IntegerField(default=0) + + def has_attachment(self, filehash): + for att in self.attachments.all(): + if att.filehash == filehash: + return True + return False def get_all_replies(self): if self.is_op(): diff --git a/contrib/frontends/django/nntpchan/nntpchan/frontend/static/style.css b/contrib/frontends/django/nntpchan/nntpchan/frontend/static/style.css index e20dc52..a08ac09 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/frontend/static/style.css +++ b/contrib/frontends/django/nntpchan/nntpchan/frontend/static/style.css @@ -121,7 +121,8 @@ form { } -.frontpage.posts { +.posts { + flex-direction: column; width: 50%; padding-top: 10%; padding-left: 25%; diff --git a/contrib/frontends/django/nntpchan/nntpchan/frontend/urls.py b/contrib/frontends/django/nntpchan/nntpchan/frontend/urls.py index 8f21407..3e38604 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/frontend/urls.py +++ b/contrib/frontends/django/nntpchan/nntpchan/frontend/urls.py @@ -8,7 +8,8 @@ urlpatterns = [ 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'), - url(r'^b/(?P[a-zA-Z0-9]+)/$', views.BoardView.as_view(), name='board'), + url(r'^b/(?P[a-zA-Z0-9\.]+[a-zA-Z0-9])/$', views.BoardView.as_view(), name='board'), url(r'^t/(?P[a-fA-F0-9\.]{40})/$', views.ThreadView.as_view(), name='thread'), + url(r'captcha.png', views.create_captcha, name='captcha'), url(r'^$', views.FrontPageView.as_view(), name='index'), ] diff --git a/contrib/frontends/django/nntpchan/nntpchan/frontend/util.py b/contrib/frontends/django/nntpchan/nntpchan/frontend/util.py index bff8f9a..cb0269d 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/frontend/util.py +++ b/contrib/frontends/django/nntpchan/nntpchan/frontend/util.py @@ -1,8 +1,15 @@ +from django.conf import settings + import base64 import hashlib import re +from datetime import datetime import time +import string +import random +import nntplib +import email.message def hashid(msgid): h = hashlib.sha1() @@ -24,3 +31,50 @@ def msgid_valid(msgid): def time_int(dtime): return time.mktime(dtime.timetuple()) +def randstr(l, base=string.digits): + r = '' + while l > 0: + r += random.choice(base) + l -= 1 + return r + + +def createPost(newsgroup, ref, form, files): + """ + create a post and post it to a news server + """ + + msg = email.message.Message() + msg['Content-Type'] = 'multipart/mixed' + msg["Subject"] = form["subject"] or "None" + msg['Date'] = email.utils.format_datetime(datetime.now()) + msg['Message-ID'] = email.utils.make_msgid(randstr(13), settings.FRONTEND_NAME) + if not msgid_valid(ref): + return None, "invalid reference: {}".format(ref) + msg["References"] = ref + msg["Newsgroups"] = newsgroup + msg["From"] = '{} '.format(form['name'] or 'Anonymous') + if 'attachment' in files: + f = files['attachment'] + part = email.message.Message() + part['Content-Type'] = f.content_type + part['Content-Disposition'] = 'form-data; filename="{}"; name="attachment"'.format(f.name) + part['Content-Transfer-Encoding'] = 'base64' + part.set_payload(base64.b64encode(f.read())) + msg.attach(part) + text = email.message.Message() + m = '{}'.format(form['message'] or ' ') + text.set_payload(m) + text['Content-Type'] = 'text/plain' + msg.attach(text) + + server = settings.NNTP_SERVER + server['readermode'] = True + msgid = None + try: + with nntplib.NNTP(**server) as nntp: + nntp.login(**settings.NNTP_LOGIN) + msgid = nntp.post(msg.as_bytes()) + except Exception as e: + return None, e + return msgid, None diff --git a/contrib/frontends/django/nntpchan/nntpchan/frontend/views.py b/contrib/frontends/django/nntpchan/nntpchan/frontend/views.py index 17197cf..56ab2da 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/frontend/views.py +++ b/contrib/frontends/django/nntpchan/nntpchan/frontend/views.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.core.urlresolvers import reverse from django.http import HttpResponse, Http404 from django.shortcuts import render, get_object_or_404 @@ -6,18 +7,40 @@ from django.views import generic from .models import Post, Newsgroup +from captcha.image import ImageCaptcha +from . import util + +captcha = ImageCaptcha(fonts=settings.CAPTCHA_FONTS) + + class Postable: """ postable view checks captcha etc """ + def context_for_get(self, request, defaults): + defaults['captcha'] = reverse('captcha') + defaults['refresh_url'] = request.path + return defaults + + def handle_post(self, request, **kwargs): + """ + handle post request, implement in subclass + """ + return None, 'handle_post() not implemented' + def post(self, request, **kwargs): ctx = { - 'error' : None + 'error' : 'invalid captcha' } - - + solution = request.session['captcha'] + if solution is not None: + if 'captcha' in request.POST: + if request.POST['captcha'].lower() == solution.lower(): + ctx['msgid'], ctx['error'] = self.handle_post(request, **kwargs) + request.session['captcha'] = '' + request.session.save() return render(request, 'frontend/postresult.html', ctx) @@ -27,6 +50,16 @@ class BoardView(generic.View, Postable): context_object_name = 'threads' model = Post + def handle_post(self, request, name): + """ + make a new thread + """ + name = 'overchan.{}'.format(name) + if not util.newsgroup_valid(name): + return None, "invalid newsgroup: {}".format(name) + return util.createPost(name, '', request.POST, request.FILES) + + def get(self, request, name): page = 0 if 'p' in request.GET: @@ -45,21 +78,33 @@ class BoardView(generic.View, Postable): else: begin = page * group.posts_per_page end = begin + group.posts_per_page - 1 - roots = self.model.objects.filter(newsgroup=group, reference='').order_by('-last_bumped')[begin:end] - ctx = {'threads': roots, 'page': page, 'name': newsgroup} + roots = self.model.objects.filter(newsgroup=group, reference='').order_by('-last_bumped')[begin:end] + ctx = self.context_for_get(request, {'threads': roots, 'page': page, 'name': newsgroup}) if page < group.max_pages: ctx['nextpage'] = reverse('board', args=[name]) + '?p={}'.format(page + 1) if page > 0: ctx['prevpage'] = reverse('board', args=[name]) + '?p={}'.format(page - 1) return render(request, self.template_name, ctx) -class ThreadView(generic.ListView, Postable): +class ThreadView(generic.View, Postable): template_name = 'frontend/thread.html' model = Post - context_object_name = 'op' - def get_queryset(self): - return get_object_or_404(self.model, posthash=self.kwargs['op']) + def handle_post(self, request, op): + """ + make a new thread + """ + post = get_object_or_404(self.model, posthash=op) + name = post.newsgroup.name + if not util.newsgroup_valid(name): + return None, "invalid newsgroup: {}".format(name) + return util.createPost(name, post.msgid, request.POST, request.FILES) + + + def get(self, request, op): + posts = get_object_or_404(self.model, posthash=op) + ctx = self.context_for_get(request, {'op': posts}) + return render(request, self.template_name, ctx) class FrontPageView(generic.View): template_name = 'frontend/frontpage.html' @@ -77,3 +122,12 @@ def modlog(request, page): if page is None: page = 0 return HttpResponse('mod log page {}'.format(page)) + +def create_captcha(request): + solution = util.randstr(7).lower() + request.session['captcha'] = solution + request.session.save() + c = captcha.generate(solution) + r =HttpResponse(c) + r['Content-Type'] = 'image/png' + return r diff --git a/contrib/frontends/django/nntpchan/nntpchan/settings.py b/contrib/frontends/django/nntpchan/nntpchan/settings.py index 51362d9..679dfa9 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/settings.py +++ b/contrib/frontends/django/nntpchan/nntpchan/settings.py @@ -9,7 +9,7 @@ https://docs.djangoproject.com/en/1.10/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/1.10/ref/settings/ """ - +import glob import os # Build paths inside the project like this: os.path.join(BASE_DIR, ...) @@ -133,3 +133,23 @@ MEDIA_URL = '/media/' # for thumbnailing CONVERT_PATH = '/usr/bin/convert' FFMPEG_PATH = '/usr/bin/ffmpeg' + + +# for captcha +CAPTCHA_FONT_DIR = os.path.join(ASSETS_ROOT, 'fonts') +CAPTCHA_FONTS = glob.glob(os.path.join(CAPTCHA_FONT_DIR, '*.ttf')) + +# for nntp server +# see nntplib module for more info +NNTP_SERVER = { + 'host': '127.0.0.1', + 'port': 1129 +} + +# nntp server login credentials +NNTP_LOGIN = { + 'user': None, + 'password': None +} + +FRONTEND_NAME = 'ebin.tld' diff --git a/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/base.html b/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/base.html index 4df5f48..2a8af3a 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/base.html +++ b/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/base.html @@ -8,6 +8,7 @@ + {% block head %}{% endblock %}
diff --git a/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/postform.html b/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/postform.html index a8f4a2f..2243998 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/postform.html +++ b/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/postform.html @@ -1,6 +1,6 @@ {% load captcha %}
-
+ {% csrf_token %} @@ -33,15 +33,7 @@ - - - - diff --git a/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/postresult.html b/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/postresult.html index b7c8bf1..afa316e 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/postresult.html +++ b/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/postresult.html @@ -1,9 +1,12 @@ {% extends "frontend/base.html" %} +{% block head %} + +{% endblock %} {% block content %} {% if error %} -
 failed to post {{error}} 
+
 failed to post: {{error}} 
{% else %} {% if msgid %}
 posted as {{msgid}} 
diff --git a/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/thread.html b/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/thread.html index 8b9d7f6..8a065eb 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/thread.html +++ b/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/thread.html @@ -5,6 +5,7 @@ back to {{op.newsgroup.name}} {% endblock %} {% block content %} +{% include "frontend/postform.html" %}
diff --git a/contrib/frontends/django/nntpchan/nntpchan/views.py b/contrib/frontends/django/nntpchan/nntpchan/views.py index 6885099..86355e0 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/views.py +++ b/contrib/frontends/django/nntpchan/nntpchan/views.py @@ -105,6 +105,8 @@ def webhook(request): post.save() for att in atts: + if post.has_attachment(att.filehash): + continue post.attachments.add(att) diff --git a/contrib/frontends/django/requirements.txt b/contrib/frontends/django/requirements.txt index 1d8b079..e13a621 100644 --- a/contrib/frontends/django/requirements.txt +++ b/contrib/frontends/django/requirements.txt @@ -1,2 +1,3 @@ captcha django +psycopg2
- -
- - - +