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 navbar_title %}nntpchan{% endblock %} |
{% block navbar_links %}{% endblock %} |
-
{% block navbar_left %}settings{% endblock %}
+
{% block navbar_left %}| settings{% endblock %}
{% 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 %}
+
+
+
+
+
+ {% for a in op.attachments.all %}
+
+ {% endfor %}
+
+
{{op.message|memepost}}
+
+ {% for reply in op.get_board_replies %}
+
+
+
+ {% 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 %}
+
+
+
+
+
+ {% for a in op.attachments.all %}
+
+ {% endfor %}
+
+
{{op.message|memepost}}
+
+ {% for reply in op.get_all_replies %}
+
+
+
+ {% 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