more
This commit is contained in:
parent
a19d36f883
commit
8ffc8c006c
@ -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
|
||||
@ -14,6 +16,16 @@ class Attachment(models.Model):
|
||||
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)
|
||||
|
@ -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;
|
||||
}
|
@ -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">>>%s</a> ' % ( posts[0].get_absolute_url(), posthash)
|
||||
else:
|
||||
return_text += '<span class="greentext">>>%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))
|
||||
|
@ -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'])
|
||||
|
@ -123,4 +123,5 @@ USE_TZ = True
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
MEDIA_ROOT = '/tmp/'
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||
MEDIA_URL = '/media/'
|
||||
|
@ -12,7 +12,7 @@
|
||||
<div id="navbar">
|
||||
<span id="navbar_title">{% block navbar_title %}nntpchan{% 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 id="content">
|
||||
{% block content %}
|
||||
|
@ -1 +1,43 @@
|
||||
|
||||
{% 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}}">>>{{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}}">>>{{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 %}
|
||||
|
@ -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}}">>>{{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}}">>>{{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 %}
|
22
contrib/frontends/django/nntpchan/nntpchan/thumbnail.py
Normal file
22
contrib/frontends/django/nntpchan/nntpchan/thumbnail.py
Normal 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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user