more
This commit is contained in:
parent
3aa87e07f2
commit
a19d36f883
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
3
contrib/frontends/django/nntpchan/nntpchan/admin.py
Normal file
3
contrib/frontends/django/nntpchan/nntpchan/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
@ -2,4 +2,4 @@ from django.apps import AppConfig
|
||||
|
||||
|
||||
class FrontendConfig(AppConfig):
|
||||
name = 'frontend'
|
||||
name = 'nntpchan.frontend'
|
||||
|
@ -0,0 +1,51 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Attachment',
|
||||
fields=[
|
||||
('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)),
|
||||
('filehash', models.CharField(editable=False, max_length=256)),
|
||||
('filename', models.CharField(max_length=256)),
|
||||
('mimetype', models.CharField(max_length=256, default='text/plain')),
|
||||
('width', models.IntegerField(default=0)),
|
||||
('height', models.IntegerField(default=0)),
|
||||
('banned', models.BooleanField(default=False)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Newsgroup',
|
||||
fields=[
|
||||
('name', models.CharField(editable=False, max_length=256, serialize=False, primary_key=True)),
|
||||
('posts_per_page', models.IntegerField(default=10)),
|
||||
('max_pages', models.IntegerField(default=10)),
|
||||
('banned', models.BooleanField(default=False)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Post',
|
||||
fields=[
|
||||
('msgid', models.CharField(editable=False, max_length=256, serialize=False, primary_key=True)),
|
||||
('posthash', models.CharField(editable=False, max_length=256)),
|
||||
('reference', models.CharField(max_length=256, default='')),
|
||||
('message', models.TextField(default='')),
|
||||
('subject', models.CharField(max_length=256, default='None')),
|
||||
('name', models.CharField(max_length=256, default='Anonymous')),
|
||||
('pubkey', models.CharField(max_length=64, default='')),
|
||||
('signature', models.CharField(max_length=64, default='')),
|
||||
('posted', models.DateTimeField()),
|
||||
('placeholder', models.BooleanField(default=False)),
|
||||
('attachments', models.ManyToManyField(to='frontend.Attachment')),
|
||||
('newsgroup', models.ForeignKey(to='frontend.Newsgroup')),
|
||||
],
|
||||
),
|
||||
]
|
@ -3,27 +3,59 @@ from django.db import models
|
||||
from . import util
|
||||
|
||||
class Attachment(models.Model):
|
||||
"""
|
||||
a file attachment assiciated with a post
|
||||
a post may have many attachments
|
||||
"""
|
||||
filehash = models.CharField(max_length=256, editable=False)
|
||||
filename = models.CharField(max_length=256)
|
||||
filepath = models.CharField(max_length=256)
|
||||
width = models.IntegerField()
|
||||
height = models.IntegerField()
|
||||
mimetype = models.CharField(max_length=256, default='text/plain')
|
||||
width = models.IntegerField(default=0)
|
||||
height = models.IntegerField(default=0)
|
||||
banned = models.BooleanField(default=False)
|
||||
|
||||
|
||||
class Newsgroup(models.Model):
|
||||
"""
|
||||
synonym for board
|
||||
"""
|
||||
name = models.CharField(max_length=256, primary_key=True, editable=False)
|
||||
posts_per_page = models.IntegerField(default=10)
|
||||
max_pages = models.IntegerField(default=10)
|
||||
banned = models.BooleanField(default=False)
|
||||
|
||||
def get_absolute_url(self):
|
||||
from django.urls import reverse
|
||||
return reverse('nntpchan.frontend.views.boardpage', args=[self.name, '0'])
|
||||
|
||||
class Post(models.Model):
|
||||
"""
|
||||
a post made
|
||||
"""
|
||||
|
||||
msgid = models.CharField(max_length=256, primary_key=True, editable=False)
|
||||
posthash = models.CharField(max_length=256, editable=False)
|
||||
reference = models.CharField(max_length=256, default='')
|
||||
message = models.TextField(default='')
|
||||
subject = models.CharField(max_length=256, default='None')
|
||||
name = models.CharField(max_length=256, default='Anonymous')
|
||||
pubkey = models.CharField(max_length=64, default='')
|
||||
signature = models.CharField(max_length=64, default='')
|
||||
newsgroup = models.ForeignKey(Newsgroup, on_delete=models.CASCADE)
|
||||
attachments = models.ManyToManyField(Attachment)
|
||||
posted = models.DateTimeField()
|
||||
placeholder = models.BooleanField(default=False)
|
||||
|
||||
def is_op(self):
|
||||
return self.reference is None
|
||||
|
||||
class Post(models.Model):
|
||||
msgid = models.CharField(max_length=256, primary_key=True)
|
||||
reference = models.CharField(max_length=256)
|
||||
message = models.TextField()
|
||||
subject = models.CharField(max_length=256)
|
||||
name = models.CharField(max_length=256)
|
||||
pubkey = models.CharField(max_length=64)
|
||||
signature = models.CharField(max_length=64)
|
||||
|
||||
def get_absolute_url(self):
|
||||
from django.urls import reverse
|
||||
op = self.msgid
|
||||
if self.reference != self.msgid:
|
||||
op = self.reference
|
||||
|
||||
return reverse('frontend.views.threadpage', args=[util.hashid(op)])
|
||||
|
||||
class Board(models.Model):
|
||||
pass
|
||||
if self.is_op():
|
||||
op = util.hashid(self.msgid)
|
||||
return reverse('nntpchan.frontend.views.threadpage', args[op])
|
||||
else:
|
||||
op = util.hashid(self.reference.msgid)
|
||||
frag = util.hashid(self.msgid)
|
||||
return reverse('nntpchan.frontend.views.threadpage', args=[op]) + '#{}'.format(frag)
|
||||
|
@ -3,12 +3,11 @@ from django.conf.urls import url
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^ctl-(?P<page>[0-9])\.html$', views.modlog, name='old-modlog'),
|
||||
url(r'^ctl/((?P<page>[0-9])/)?$', views.modlog, name='modlog'),
|
||||
url(r'^overchan\.(?P<name>[a-zA-z0-9\.]+)-(?P<page>[0-9])\.html$', views.boardpage, name='old-board'),
|
||||
url(r'^(?P<name>[a-zA-z0-9\.]+)/((?P<page>[0-9])/)?$', views.boardpage, name='board'),
|
||||
url(r'^thread-(?P<op>[a-fA-F0-9\.]{40})\.html$', views.threadpage, name='old-thread'),
|
||||
url(r'^t/(?P<op>[a-fA-F0-9\.]{40})\.html$', views.redirect_thread, name='redirect-thread'),
|
||||
url(r'^t/(?P<op>[a-fA-F0-9\.]{40})/$', views.threadpage, name='thread'),
|
||||
url(r'^$', views.frontpage, name='index'),
|
||||
url(r'^ctl-(?P<page>[0-9]+)\.html$', views.modlog, name='old-modlog'),
|
||||
url(r'^ctl/((?P<page>[0-9]+)/)?$', views.modlog, name='modlog'),
|
||||
url(r'^overchan\.(?P<name>[a-zA-z0-9\.]+)-(?P<page>[0-9]+)\.html$', views.BoardView.as_view(), name='old-board'),
|
||||
url(r'^overchan\.(?P<name>[a-zA-z0-9\.]+)/((?P<page>[0-9]+)/)?$', views.BoardView.as_view(), name='board'),
|
||||
url(r'^thread-(?P<op>[a-fA-F0-9\.]{40})\.html$', views.ThreadView.as_view(), name='old-thread'),
|
||||
url(r'^t/(?P<op>[a-fA-F0-9\.]{40})/$', views.ThreadView.as_view(), name='thread'),
|
||||
url(r'^$', views.FrontPageView.as_view(), name='index'),
|
||||
]
|
||||
|
@ -1,5 +1,26 @@
|
||||
import base64
|
||||
import hashlib
|
||||
|
||||
import re
|
||||
|
||||
def hashid(msgid):
|
||||
return hashlib.sha1().hexdigest('%s' % msgid).decode('ascii')
|
||||
h = hashlib.sha1()
|
||||
m = '{}'.format(msgid).encode('ascii')
|
||||
h.update(m)
|
||||
return h.hexdigest()
|
||||
|
||||
def newsgroup_valid(name):
|
||||
return re.match('overchan\.[a-zA-Z0-9\.]+[a-zA-Z0-9]$', name) is not None
|
||||
|
||||
def hashfile(data):
|
||||
h = hashlib.sha512()
|
||||
h.update(data)
|
||||
return base64.b32encode(h.digest()).decode('ascii')
|
||||
|
||||
def msgid_valid(msgid):
|
||||
return re.match("<[a-zA-Z0-9\$\._\-\|]+@[a-zA-Z0-9\$\._\-\|]+>$", msgid) is not None
|
||||
|
||||
def save_part(part):
|
||||
"""
|
||||
save a mime part to disk
|
||||
"""
|
||||
|
||||
|
@ -1,33 +1,43 @@
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.views import generic
|
||||
|
||||
from .models import Post, Board
|
||||
|
||||
class IndexView(generic.DetailView):
|
||||
template_name = 'frontend/index.html'
|
||||
from .models import Post, Newsgroup
|
||||
|
||||
class BoardView(generic.ListView):
|
||||
template_name = 'frontend/board.html'
|
||||
model = Post
|
||||
|
||||
def get_queryset(self):
|
||||
newsgroup = self.kwargs['name']
|
||||
page = int(self.kwargs['page'] or "0")
|
||||
try:
|
||||
group = Newsgroup.objects.get(name=newsgroup)
|
||||
except Newsgroup.DoesNotExist:
|
||||
raise Http404("no such board")
|
||||
else:
|
||||
begin = page * group.posts_per_page
|
||||
end = begin + group.posts_per_page
|
||||
return get_object_or_404(self.model, newsgroup=group)[begin:end]
|
||||
|
||||
|
||||
class ThreadView(generic.ListView):
|
||||
template_name = 'frontend/thread.html'
|
||||
model = Post
|
||||
|
||||
def get_queryset(self):
|
||||
return get_object_or_404(self.model, posthash=self.kwargs['op'])
|
||||
|
||||
def frontpage(request):
|
||||
return HttpResponse('ayyyy')
|
||||
|
||||
def boardpage(request, name, page):
|
||||
if page is None:
|
||||
page = 0
|
||||
name = 'overchan.{}'.format(name)
|
||||
return HttpResponse('{} page {}'.format(name, page))
|
||||
|
||||
def threadpage(request, op):
|
||||
return HttpResponse('thread {}'.format(op))
|
||||
|
||||
def redirect_thread(request, op):
|
||||
pass
|
||||
class FrontPageView(generic.ListView):
|
||||
template_name = 'frontend/frontpage.html'
|
||||
model = Post
|
||||
|
||||
def get_queryset(self):
|
||||
return self.model.objects.order_by('posted')[:10]
|
||||
|
||||
|
||||
def modlog(request, page):
|
||||
if page is None:
|
||||
page = 0
|
||||
|
@ -55,7 +55,7 @@ ROOT_URLCONF = 'nntpchan.urls'
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'DIRS': ['nntpchan/templates'],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
@ -76,8 +76,11 @@ WSGI_APPLICATION = 'nntpchan.wsgi.application'
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||
'HOST': 'localhost',
|
||||
'PASSWORD': 'jeff',
|
||||
'USER': 'jeff',
|
||||
'NAME': 'jeff',
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,3 +122,5 @@ USE_TZ = True
|
||||
# https://docs.djangoproject.com/en/1.10/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
MEDIA_ROOT = '/tmp/'
|
||||
|
@ -0,0 +1,29 @@
|
||||
{% load static %}
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{% block title %} nntpchan {% endblock %}</title>
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'style.css' %}" />
|
||||
<script type="text/javascript" href="{% static 'nntpchan.js' %}">
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrapper">
|
||||
<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>
|
||||
</div>
|
||||
<div id="content">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<p> legal notice goes here </p>
|
||||
</footer>
|
||||
<script>
|
||||
ready();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1 @@
|
||||
{% extends "frontend/base.html" %}
|
@ -0,0 +1 @@
|
||||
{% extends "frontend/base.html" %}
|
@ -15,6 +15,9 @@ Including another URLconf
|
||||
"""
|
||||
from django.conf.urls import url, include
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^nntpchan/', include('nntpchan.frontend.urls'))
|
||||
url(r'^webhook$', views.webhook),
|
||||
url(r'', include('nntpchan.frontend.urls'))
|
||||
]
|
||||
|
@ -1,4 +1,96 @@
|
||||
from django.http import HttpResponse
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse, HttpResponseNotAllowed, JsonResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
def index(request):
|
||||
return HttpResponse('ayyyyy')
|
||||
from .frontend.models import Post, Attachment, Newsgroup
|
||||
from .frontend import util
|
||||
|
||||
import email
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
import mimetypes
|
||||
import os
|
||||
|
||||
@csrf_exempt
|
||||
def webhook(request):
|
||||
"""
|
||||
endpoint for nntpchan daemon webhook
|
||||
"""
|
||||
if request.method != 'POST':
|
||||
return HttpResponseNotAllowed(['POST'])
|
||||
|
||||
try:
|
||||
msg = email.message_from_bytes(request.body)
|
||||
newsgroup = msg.get('Newsgroups')
|
||||
|
||||
if newsgroup is None:
|
||||
raise Exception("no newsgroup specified")
|
||||
|
||||
if not util.newsgroup_valid(newsgroup):
|
||||
raise Exception("invalid newsgroup name")
|
||||
|
||||
|
||||
group, created = Newsgroup.objects.get_or_create(name=newsgroup)
|
||||
|
||||
if group.banned:
|
||||
raise Exception("newsgroup is banned")
|
||||
|
||||
msgid = None
|
||||
for h in ('Message-ID', 'Message-Id', 'MessageId', 'MessageID'):
|
||||
if h in msg:
|
||||
msgid = msg[h]
|
||||
break
|
||||
|
||||
if msgid is None:
|
||||
raise Exception("no message id specified")
|
||||
elif not util.msgid_valid(msgid):
|
||||
raise Exception("invalid message id format: {}".format(msgid))
|
||||
|
||||
h = util.hashid(msgid)
|
||||
atts = list()
|
||||
ref = msg['References'] or ''
|
||||
posted = email.utils.parsedate_to_datetime(msg['Date'])
|
||||
|
||||
f = msg['From'] or 'anon <anon@anon>'
|
||||
name = email.utils.parseaddr(f)[0]
|
||||
post, created = Post.objects.get_or_create(defaults={
|
||||
'posthash': h,
|
||||
'reference': ref,
|
||||
'posted': posted,
|
||||
'name': name,
|
||||
'subject': msg["Subject"] or '',
|
||||
'newsgroup': group}, msgid=msgid)
|
||||
m = ''
|
||||
|
||||
for part in msg.walk():
|
||||
ctype = part.get_content_type()
|
||||
if ctype.startswith("text/plain"):
|
||||
m += '{} '.format(part.get_payload(decode=True).decode('utf-8'))
|
||||
else:
|
||||
print(part.get_content_type())
|
||||
payload = part.get_payload(decode=True)
|
||||
if payload is None:
|
||||
continue
|
||||
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)
|
||||
if not os.path.exists(fname):
|
||||
with open(fname, 'wb') as f:
|
||||
f.write(payload)
|
||||
|
||||
att = Attachment(filehash=fh)
|
||||
att.mimetype = mtype
|
||||
att.filename = part.get_filename()
|
||||
att.save()
|
||||
atts.append(att)
|
||||
post.message = m
|
||||
post.save()
|
||||
|
||||
for att in atts:
|
||||
post.attachments.add(att)
|
||||
except Exception as ex:
|
||||
traceback.print_exc()
|
||||
return JsonResponse({ 'error': '{}'.format(ex) })
|
||||
else:
|
||||
return JsonResponse({'posted': True})
|
||||
|
Reference in New Issue
Block a user