Archived
1
0
This commit is contained in:
Jeff Becker 2016-11-05 14:54:22 -04:00
parent a19d36f883
commit 8ffc8c006c
No known key found for this signature in database
GPG Key ID: AB950234D6EA286B
12 changed files with 404 additions and 15 deletions

View File

@ -2,6 +2,8 @@ from django.db import models
from . import util from . import util
import mimetypes
class Attachment(models.Model): class Attachment(models.Model):
""" """
a file attachment assiciated with a post a file attachment assiciated with a post
@ -14,6 +16,16 @@ class Attachment(models.Model):
height = models.IntegerField(default=0) height = models.IntegerField(default=0)
banned = models.BooleanField(default=False) 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): class Newsgroup(models.Model):
""" """
@ -46,16 +58,28 @@ class Post(models.Model):
posted = models.DateTimeField() posted = models.DateTimeField()
placeholder = models.BooleanField(default=False) 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): def is_op(self):
return self.reference is None return self.reference == ''
def shorthash(self):
return self.posthash[:10]
def get_absolute_url(self): def get_absolute_url(self):
from django.urls import reverse
if self.is_op(): if self.is_op():
op = util.hashid(self.msgid) op = util.hashid(self.msgid)
return reverse('nntpchan.frontend.views.threadpage', args[op]) return '/t/{}/'.format(op)
else: else:
op = util.hashid(self.reference.msgid) op = util.hashid(self.reference)
frag = util.hashid(self.msgid) frag = util.hashid(self.msgid)
return reverse('nntpchan.frontend.views.threadpage', args=[op]) + '#{}'.format(frag) return '/t/{}/#{}'.format(op, frag)

View File

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

View File

@ -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 += '<span class="greentext">%s </span>' % 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 += '<a href="%s" class="postcite">&gt;&gt%s</a> ' % ( posts[0].get_absolute_url(), posthash)
else:
return_text += '<span class="greentext">&gt;&gt;%s</span> ' % 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 += '<a href="%s" class="boardlink">%s</a> ' % ( group[0].get_absolute_url(), esc(match.string ) )
else:
return_text += '<span class="greentext">%s</span> ' % 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 += '<a href="%s">%s</a> ' % ( 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))

View File

@ -4,13 +4,14 @@ from django.views import generic
from .models import Post, Newsgroup from .models import Post, Newsgroup
class BoardView(generic.ListView): class BoardView(generic.View):
template_name = 'frontend/board.html' template_name = 'frontend/board.html'
context_object_name = 'threads'
model = Post model = Post
def get_queryset(self): def get(self, request, name, page):
newsgroup = self.kwargs['name'] newsgroup = 'overchan.{}'.format(name)
page = int(self.kwargs['page'] or "0") page = int(page or "0")
try: try:
group = Newsgroup.objects.get(name=newsgroup) group = Newsgroup.objects.get(name=newsgroup)
except Newsgroup.DoesNotExist: except Newsgroup.DoesNotExist:
@ -18,12 +19,14 @@ class BoardView(generic.ListView):
else: else:
begin = page * group.posts_per_page begin = page * group.posts_per_page
end = begin + 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): class ThreadView(generic.ListView):
template_name = 'frontend/thread.html' template_name = 'frontend/thread.html'
model = Post model = Post
context_object_name = 'op'
def get_queryset(self): def get_queryset(self):
return get_object_or_404(self.model, posthash=self.kwargs['op']) return get_object_or_404(self.model, posthash=self.kwargs['op'])

View File

@ -123,4 +123,5 @@ USE_TZ = True
STATIC_URL = '/static/' STATIC_URL = '/static/'
MEDIA_ROOT = '/tmp/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

View File

@ -12,7 +12,7 @@
<div id="navbar"> <div id="navbar">
<span id="navbar_title">{% block navbar_title %}nntpchan{% endblock %}</span> | <span id="navbar_title">{% block navbar_title %}nntpchan{% endblock %}</span> |
<span id="navbar_links">{% block navbar_links %}{% endblock %}</span> | <span id="navbar_links">{% block navbar_links %}{% endblock %}</span> |
<span id="navbar_left">{% block navbar_left %}<a href="#" onclick="nntpchan_toggle_settings()">settings</a>{% endblock %}</span> <span id="navbar_left">{% block navbar_left %}| <a href="#" onclick="nntpchan_toggle_settings()">settings</a>{% endblock %}</span>
</div> </div>
<div id="content"> <div id="content">
{% block content %} {% block content %}

View File

@ -1 +1,43 @@
{% extends "frontend/base.html" %} {% extends "frontend/base.html" %}
{% load chanup %}
{% block title %} {{name}} page {{page}} {% endblock %}
{% block content %}
<hr />
{% for op in threads %}
<div id="{{op.posthash}}" class="thread">
<div class="post op">
<div class="header">
<span class="name">{{op.name}}</span> <span class="subject">{{op.subject}}</span>
<span class="msgid">{{op.msgid}}</span>
<span class="posted">{{op.posted}}</span>
<span class="cite"><a href="{{op.get_absolute_url}}">&gt;&gt;{{op.shorthash}}</a></span>
</div>
<div class="attachments">
{% for a in op.attachments.all %}
<img src="{{a.thumb}}"></img>
{% endfor %}
</div>
<pre class="postbody">{{op.message|memepost}}</pre>
</div>
{% for reply in op.get_board_replies %}
<div class="post reply">
<div class="header">
<span class="name">{{reply.name}}</span> <span class="subject">{{reply.subject}}</span>
<span class="msgid">{{reply.msgid}}</span>
<span class="posted">{{reply.posted}}</span>
<span class="cite"><a href="{{reply.get_absolute_url}}">&gt;&gt;{{reply.shorthash}}</a></span>
</div>
<div class="attachments">
{% for a in reply.attachments.all %}
<img src="{{a.thumb}}"></img>
{% endfor %}
</div>
<pre class="postbody">{{reply.message|memepost}}</pre>
</div>
{% endfor %}
</div>
<hr />
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,41 @@
{% extends "frontend/base.html" %}
{% load chanup %}
{% block title %} {{op.subject}} {% endblock %}
{% block content %}
<div id="{{op.posthash}}" class="thread">
<div class="post op">
<div class="header">
<span class="name">{{op.name}}</span> <span class="subject">{{op.subject}}</span>
<span class="msgid">{{op.msgid}}</span>
<span class="posted">{{op.posted}}</span>
<span class="cite"><a href="{{op.get_absolute_url}}">&gt;&gt;{{op.shorthash}}</a></span>
</div>
<div class="attachments">
{% for a in op.attachments.all %}
<img class="thumb" src="{{a.thumb}}"></img>
{% endfor %}
</div>
<pre class="postbody">{{op.message|memepost}}</pre>
</div>
{% for reply in op.get_all_replies %}
<div class="post reply">
<div class="header">
<span class="name">{{reply.name}}</span> <span class="subject">{{reply.subject}}</span>
<span class="msgid">{{reply.msgid}}</span>
<span class="posted">{{reply.posted}}</span>
<span class="cite"><a href="{{reply.get_absolute_url}}">&gt;&gt;{{reply.shorthash}}</a></span>
</div>
<div class="attachments">
{% for a in reply.attachments.all %}
<img class="thumb" src="{{a.thumb}}"></img>
{% endfor %}
</div>
{% autoescape off %}
<pre class="postbody">{{reply.message|memepost}}</pre>
{% endautoescape %}
</div>
{% endfor %}
</div>
<hr />
{% endblock %}

View File

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

View File

@ -14,10 +14,12 @@ Including another URLconf
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
""" """
from django.conf.urls import url, include from django.conf.urls import url, include
from django.conf import settings
from django.conf.urls.static import static
from . import views from . import views
urlpatterns = [ urlpatterns = [
url(r'^webhook$', views.webhook), url(r'^webhook$', views.webhook),
url(r'', include('nntpchan.frontend.urls')) url(r'', include('nntpchan.frontend.urls'))
] ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@ -5,6 +5,8 @@ from django.views.decorators.csrf import csrf_exempt
from .frontend.models import Post, Attachment, Newsgroup from .frontend.models import Post, Attachment, Newsgroup
from .frontend import util from .frontend import util
from . import thumbnail
import email import email
import traceback import traceback
from datetime import datetime from datetime import datetime
@ -74,10 +76,15 @@ def webhook(request):
mtype = part.get_content_type() mtype = part.get_content_type()
ext = mimetypes.guess_extension(mtype) or '' ext = mimetypes.guess_extension(mtype) or ''
fh = util.hashfile(bytes(payload)) 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): if not os.path.exists(fname):
with open(fname, 'wb') as f: with open(fname, 'wb') as f:
f.write(payload) 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 = Attachment(filehash=fh)
att.mimetype = mtype att.mimetype = mtype