From 8ffc8c006c4810b602b76ba1a88aaefc5a6f059a Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Sat, 5 Nov 2016 14:54:22 -0400 Subject: [PATCH] more --- .../nntpchan/nntpchan/frontend/models.py | 36 ++++- .../nntpchan/frontend/static/style.css | 102 ++++++++++++ .../frontend/templatetags/__init__.py | 0 .../nntpchan/frontend/templatetags/chanup.py | 145 ++++++++++++++++++ .../nntpchan/nntpchan/frontend/views.py | 13 +- .../django/nntpchan/nntpchan/settings.py | 3 +- .../nntpchan/templates/frontend/base.html | 2 +- .../nntpchan/templates/frontend/board.html | 42 +++++ .../nntpchan/templates/frontend/thread.html | 41 +++++ .../django/nntpchan/nntpchan/thumbnail.py | 22 +++ .../django/nntpchan/nntpchan/urls.py | 4 +- .../django/nntpchan/nntpchan/views.py | 9 +- 12 files changed, 404 insertions(+), 15 deletions(-) create mode 100644 contrib/frontends/django/nntpchan/nntpchan/frontend/templatetags/__init__.py create mode 100644 contrib/frontends/django/nntpchan/nntpchan/frontend/templatetags/chanup.py create mode 100644 contrib/frontends/django/nntpchan/nntpchan/thumbnail.py diff --git a/contrib/frontends/django/nntpchan/nntpchan/frontend/models.py b/contrib/frontends/django/nntpchan/nntpchan/frontend/models.py index b3dda9a..0597a0f 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/frontend/models.py +++ b/contrib/frontends/django/nntpchan/nntpchan/frontend/models.py @@ -2,6 +2,8 @@ from django.db import models from . import util +import mimetypes + class Attachment(models.Model): """ a file attachment assiciated with a post @@ -13,6 +15,16 @@ class Attachment(models.Model): width = models.IntegerField(default=0) height = models.IntegerField(default=0) banned = models.BooleanField(default=False) + + def path(self): + ext = self.filename.split('.')[-1] + return '{}{}'.format(self.filehash, ext) + + def thumb(self): + return '/media/thumb-{}.jpg'.format(self.path()) + + def source(self): + return '/media/{}'.format(self.path()) class Newsgroup(models.Model): @@ -46,16 +58,28 @@ class Post(models.Model): posted = models.DateTimeField() placeholder = models.BooleanField(default=False) + def get_all_replies(self): + if self.is_op(): + return Post.objects.filter(reference=self.msgid).order_by('posted') + + def get_board_replies(self, truncate=5): + rpls = self.get_all_replies() + l = len(rpls) + if l > truncate: + rpls = rpls[(l+1)-truncate:l-1] + return rpls + def is_op(self): - return self.reference is None + return self.reference == '' + + def shorthash(self): + return self.posthash[:10] def get_absolute_url(self): - from django.urls import reverse - if self.is_op(): op = util.hashid(self.msgid) - return reverse('nntpchan.frontend.views.threadpage', args[op]) + return '/t/{}/'.format(op) else: - op = util.hashid(self.reference.msgid) + op = util.hashid(self.reference) frag = util.hashid(self.msgid) - return reverse('nntpchan.frontend.views.threadpage', args=[op]) + '#{}'.format(frag) + return '/t/{}/#{}'.format(op, frag) diff --git a/contrib/frontends/django/nntpchan/nntpchan/frontend/static/style.css b/contrib/frontends/django/nntpchan/nntpchan/frontend/static/style.css index e69de29..23fad35 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/frontend/static/style.css +++ b/contrib/frontends/django/nntpchan/nntpchan/frontend/static/style.css @@ -0,0 +1,102 @@ + +body, html { + background-color: #eef2ff; +} + +div { + font-size: 12px; + font-family: sans; +} + +#navbar { + z-index: 20; + position: fixed; + top: 0px; + margin-top: 0px; + left: 0px; + right: 0px; + box-shadow: 1px 10px 20px rgba(0, 0, 0, 0.15); + min-height: 20px; + background-color: #d6daf0; + border-bottom: 1px solid; + color: #34345c +} + +#navbar_left { + float: right; +} + + +img.thumb { + max-width: 300px; + max-height: 200px; +} + +.thread { + padding: 5%; +} + +.post { + margin: 2%; + background-color: #d6daf0; + min-width: 500px; + width: 75%; + border: 2px solid #B7C5D9; + border-radius: 2px 4px 4px 4px; + border-left: none; + border-top: none; + padding: 1%; +} + +.post > .header { + padding-bottom: 1%; +} + +.postbody { + font-family: serif; + font-size: 10pt; + white-space: pre-wrap; +} + +.name { + font-weight: bold; + color: #028241; +} + +.subject { + font-weight: bold; + color: #480188; +} + +.cite { + float: right; +} + +.redtext { + color: red; + font-weight: bold; +} + +.greentext { + color: green; + font-style: italic; +} + +@keyframes psych { + 0% {background-color: red; color: blue; } + 10% {background-color: yellow; color: red; } + 20% {background-color: blue; color: green; } + 30% {background-color: green; color: yellow; } + 40% {background-color: red; color: blue; } + 50% {background-color: yellow; color: green; } + 60% {background-color: blue; color: yellow; } + 70% {background-color: green; color: blue; } + 80% {background-color: red; color: green; } + 90% {background-color: yellow; color: red; } + 95% {background-color: blue; color: yellow; } + 100% {background-color: green; color: white; } +} + +.psy { + animation: psych 2s linear infinite; +} diff --git a/contrib/frontends/django/nntpchan/nntpchan/frontend/templatetags/__init__.py b/contrib/frontends/django/nntpchan/nntpchan/frontend/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/contrib/frontends/django/nntpchan/nntpchan/frontend/templatetags/chanup.py b/contrib/frontends/django/nntpchan/nntpchan/frontend/templatetags/chanup.py new file mode 100644 index 0000000..39f5841 --- /dev/null +++ b/contrib/frontends/django/nntpchan/nntpchan/frontend/templatetags/chanup.py @@ -0,0 +1,145 @@ +from django import template +from django.template.defaultfilters import stringfilter + +from django.utils.html import conditional_escape +from django.utils.safestring import mark_safe + + +from nntpchan.frontend.models import Newsgroup, Post + +import re +from urllib.parse import urlparse + +register = template.Library() + +re_postcite = re.compile('>> ?([0-9a-fA-F]+)') +re_boardlink = re.compile('>>> ?([a-zA-Z0-9\.]+[a-zA-Z0-9])') +re_redtext = re.compile('== ?(.+) ?==') +re_psytext = re.compile('@@ ?(.+) ?@@') + +def greentext(text, esc): + return_text = '' + f = False + for line in text.split('\n'): + line = line.strip() + if len(line) == 0: + continue + if line[0] == '>' and line[1] != '>': + return_text += '%s ' % esc ( line ) + '\n' + f = True + else: + return_text += line + '\n' + return return_text, f + +def blocktext(text, esc, delim='', css='', tag='span'): + parts = text.split(delim) + f = False + if len(parts) > 1: + parts.reverse() + return_text = '' + while len(parts) > 0: + return_text += esc(parts.pop()) + if len(parts) > 0: + f = True + return_text += '<{} class="{}">%s'.format(tag,css,tag) % esc(parts.pop()) + return return_text, f + else: + return text, f + +redtext = lambda t, e : blocktext(t, e, '==', 'redtext') +psytext = lambda t, e : blocktext(t, e, '@@', 'psy') +codeblock = lambda t, e : blocktext(t, e, '[code]', 'code', 'pre') + + +def postcite(text, esc): + return_text = '' + filtered = False + for line in text.split('\n'): + for word in line.split(' '): + match = re_postcite.match(word) + if match: + posthash = match.groups()[0] + posts = Post.objects.filter(posthash__startswith=posthash) + if len(posts) > 0: + filtered = True + return_text += '>>%s ' % ( posts[0].get_absolute_url(), posthash) + else: + return_text += '>>%s ' % match.string + elif filtered: + return_text += word + ' ' + else: + return_text += esc(word) + ' ' + return_text += '\n' + return return_text, filtered + + +def boardlink(text, esc): + return_text = '' + filtered = False + for line in text.split('\n'): + for word in line.split(' '): + match = re_boardlink.match(word) + if match: + name = match.groups()[0] + group = Newsgroup.objects.filter(name=name) + if len(group) > 0: + filtered = True + return_text += '%s ' % ( group[0].get_absolute_url(), esc(match.string ) ) + else: + return_text += '%s ' % esc (match.string) + elif filtered: + return_text += word + ' ' + else: + return_text += esc(word) + ' ' + return_text += '\n' + return return_text, filtered + + +def urlify(text, esc): + return_text = '' + filtered = False + for line in text.split('\n'): + for word in line.split(' '): + u = urlparse(word) + if u.scheme != '' and u.netloc != '': + return_text += '%s ' % ( u.geturl(), esc(word) ) + filtered = True + else: + return_text += esc(word) + ' ' + return_text += '\n' + return return_text, filtered + +line_funcs = [ + greentext, + redtext, + postcite, + boardlink, + urlify, + psytext, + codeblock, +] + +@register.filter(needs_autoescape=True, name='memepost') +def memepost(text, autoescape=True): + if autoescape: + esc = conditional_escape + else: + esc = lambda x : x + return_text = text + + def doFilter(funcs, text, filter): + """ + RECURSIVE FUNCTIONS ARE FUN :^DDDDDD + """ + if len(funcs) == 1: + t, filtered = funcs[0](text, lambda x : x) + return t + else: + t, filtered = funcs[0](text, filter) + if filtered: + return doFilter(funcs[1:], t, lambda x : x) + else: + return doFilter(funcs[1:], t, filter) + + return mark_safe(doFilter(line_funcs, return_text, conditional_escape)) + diff --git a/contrib/frontends/django/nntpchan/nntpchan/frontend/views.py b/contrib/frontends/django/nntpchan/nntpchan/frontend/views.py index f1c2851..6dba7e4 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/frontend/views.py +++ b/contrib/frontends/django/nntpchan/nntpchan/frontend/views.py @@ -4,13 +4,14 @@ from django.views import generic from .models import Post, Newsgroup -class BoardView(generic.ListView): +class BoardView(generic.View): template_name = 'frontend/board.html' + context_object_name = 'threads' model = Post - def get_queryset(self): - newsgroup = self.kwargs['name'] - page = int(self.kwargs['page'] or "0") + def get(self, request, name, page): + newsgroup = 'overchan.{}'.format(name) + page = int(page or "0") try: group = Newsgroup.objects.get(name=newsgroup) except Newsgroup.DoesNotExist: @@ -18,12 +19,14 @@ class BoardView(generic.ListView): else: begin = page * group.posts_per_page end = begin + group.posts_per_page - return get_object_or_404(self.model, newsgroup=group)[begin:end] + posts = self.model.objects.filter(newsgroup=group, reference='').order_by('-posted')[begin:end] + return render(request, self.template_name, {'threads': posts, 'page': page, 'name': newsgroup}) class ThreadView(generic.ListView): 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']) diff --git a/contrib/frontends/django/nntpchan/nntpchan/settings.py b/contrib/frontends/django/nntpchan/nntpchan/settings.py index 728b692..d7be1f2 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/settings.py +++ b/contrib/frontends/django/nntpchan/nntpchan/settings.py @@ -123,4 +123,5 @@ USE_TZ = True STATIC_URL = '/static/' -MEDIA_ROOT = '/tmp/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') +MEDIA_URL = '/media/' diff --git a/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/base.html b/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/base.html index 2da6a97..cece1a4 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/base.html +++ b/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/base.html @@ -12,7 +12,7 @@
{% block content %} diff --git a/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/board.html b/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/board.html index 88cb596..7cd6937 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/board.html +++ b/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/board.html @@ -1 +1,43 @@ + {% extends "frontend/base.html" %} +{% load chanup %} +{% block title %} {{name}} page {{page}} {% endblock %} +{% block content %} +
+{% for op in threads %} +
+
+
+ {{op.name}} {{op.subject}} + {{op.msgid}} + {{op.posted}} + >>{{op.shorthash}} +
+ +
+ {% for a in op.attachments.all %} + + {% endfor %} +
+
{{op.message|memepost}}
+
+ {% for reply in op.get_board_replies %} +
+
+ {{reply.name}} {{reply.subject}} + {{reply.msgid}} + {{reply.posted}} + >>{{reply.shorthash}} +
+
+ {% for a in reply.attachments.all %} + + {% endfor %} +
+
{{reply.message|memepost}}
+
+ {% endfor %} +
+
+{% endfor %} +{% endblock %} diff --git a/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/thread.html b/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/thread.html index e69de29..c7f1939 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/thread.html +++ b/contrib/frontends/django/nntpchan/nntpchan/templates/frontend/thread.html @@ -0,0 +1,41 @@ +{% extends "frontend/base.html" %} +{% load chanup %} +{% block title %} {{op.subject}} {% endblock %} +{% block content %} +
+
+
+ {{op.name}} {{op.subject}} + {{op.msgid}} + {{op.posted}} + >>{{op.shorthash}} +
+ +
+ {% for a in op.attachments.all %} + + {% endfor %} +
+
{{op.message|memepost}}
+
+ {% for reply in op.get_all_replies %} +
+
+ {{reply.name}} {{reply.subject}} + {{reply.msgid}} + {{reply.posted}} + >>{{reply.shorthash}} +
+
+ {% for a in reply.attachments.all %} + + {% endfor %} +
+ {% autoescape off %} +
{{reply.message|memepost}}
+ {% endautoescape %} +
+ {% endfor %} +
+
+{% endblock %} diff --git a/contrib/frontends/django/nntpchan/nntpchan/thumbnail.py b/contrib/frontends/django/nntpchan/nntpchan/thumbnail.py new file mode 100644 index 0000000..f506d24 --- /dev/null +++ b/contrib/frontends/django/nntpchan/nntpchan/thumbnail.py @@ -0,0 +1,22 @@ +import subprocess +import os +img_ext = [] +vid_ext = [] + +def generate(fname, tname, placeholder): + """ + generate thumbnail + """ + ext = fname.split('.')[-1] + cmd = None + if ext in img_ext: + cmd = ['convert', '-thumbnail', '200', fname, tname] + elif ext in vid_ext: + cmd = ['ffmpeg', '-i', fname, '-vf', 'scale=300:200', '-vframes', '1', tname] + + if cmd is None: + os.link(placeholder, tname) + else: + subprocess.call(cmd) + + diff --git a/contrib/frontends/django/nntpchan/nntpchan/urls.py b/contrib/frontends/django/nntpchan/nntpchan/urls.py index 4ae3946..63b5bde 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/urls.py +++ b/contrib/frontends/django/nntpchan/nntpchan/urls.py @@ -14,10 +14,12 @@ Including another URLconf 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ from django.conf.urls import url, include +from django.conf import settings +from django.conf.urls.static import static from . import views urlpatterns = [ url(r'^webhook$', views.webhook), url(r'', include('nntpchan.frontend.urls')) -] +] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/contrib/frontends/django/nntpchan/nntpchan/views.py b/contrib/frontends/django/nntpchan/nntpchan/views.py index 8fc86fe..10c87ce 100644 --- a/contrib/frontends/django/nntpchan/nntpchan/views.py +++ b/contrib/frontends/django/nntpchan/nntpchan/views.py @@ -5,6 +5,8 @@ from django.views.decorators.csrf import csrf_exempt from .frontend.models import Post, Attachment, Newsgroup from .frontend import util +from . import thumbnail + import email import traceback from datetime import datetime @@ -74,10 +76,15 @@ def webhook(request): mtype = part.get_content_type() ext = mimetypes.guess_extension(mtype) or '' fh = util.hashfile(bytes(payload)) - fname = os.path.join(settings.MEDIA_ROOT, fh+ext) + fn = fh + ext + fname = os.path.join(settings.MEDIA_ROOT, fn) if not os.path.exists(fname): with open(fname, 'wb') as f: f.write(payload) + tname = os.path.join(settings.MEDIA_ROOT, 'thumb-{}.jpg'.format(fn)) + placeholder = os.path.join(settings.MEDIA_ROOT, 'placeholder.jpg') + if not os.path.exists(tname): + thumbnail.generate(fname, tname, placeholder) att = Attachment(filehash=fh) att.mimetype = mtype