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 os
|
||||||
import sys
|
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):
|
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
|
from . import util
|
||||||
|
|
||||||
class Attachment(models.Model):
|
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)
|
filename = models.CharField(max_length=256)
|
||||||
filepath = models.CharField(max_length=256)
|
mimetype = models.CharField(max_length=256, default='text/plain')
|
||||||
width = models.IntegerField()
|
width = models.IntegerField(default=0)
|
||||||
height = models.IntegerField()
|
height = models.IntegerField(default=0)
|
||||||
|
banned = models.BooleanField(default=False)
|
||||||
|
|
||||||
class Post(models.Model):
|
|
||||||
msgid = models.CharField(max_length=256, primary_key=True)
|
class Newsgroup(models.Model):
|
||||||
reference = models.CharField(max_length=256)
|
"""
|
||||||
message = models.TextField()
|
synonym for board
|
||||||
subject = models.CharField(max_length=256)
|
"""
|
||||||
name = models.CharField(max_length=256)
|
name = models.CharField(max_length=256, primary_key=True, editable=False)
|
||||||
pubkey = models.CharField(max_length=64)
|
posts_per_page = models.IntegerField(default=10)
|
||||||
signature = models.CharField(max_length=64)
|
max_pages = models.IntegerField(default=10)
|
||||||
|
banned = models.BooleanField(default=False)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
op = self.msgid
|
return reverse('nntpchan.frontend.views.boardpage', args=[self.name, '0'])
|
||||||
if self.reference != self.msgid:
|
|
||||||
op = self.reference
|
|
||||||
|
|
||||||
return reverse('frontend.views.threadpage', args=[util.hashid(op)])
|
class Post(models.Model):
|
||||||
|
"""
|
||||||
|
a post made
|
||||||
|
"""
|
||||||
|
|
||||||
class Board(models.Model):
|
msgid = models.CharField(max_length=256, primary_key=True, editable=False)
|
||||||
pass
|
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
|
||||||
|
|
||||||
|
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])
|
||||||
|
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
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^ctl-(?P<page>[0-9])\.html$', views.modlog, name='old-modlog'),
|
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'^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'^overchan\.(?P<name>[a-zA-z0-9\.]+)-(?P<page>[0-9]+)\.html$', views.BoardView.as_view(), name='old-board'),
|
||||||
url(r'^(?P<name>[a-zA-z0-9\.]+)/((?P<page>[0-9])/)?$', views.boardpage, name='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.threadpage, name='old-thread'),
|
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})\.html$', views.redirect_thread, name='redirect-thread'),
|
url(r'^t/(?P<op>[a-fA-F0-9\.]{40})/$', views.ThreadView.as_view(), name='thread'),
|
||||||
url(r'^t/(?P<op>[a-fA-F0-9\.]{40})/$', views.threadpage, name='thread'),
|
url(r'^$', views.FrontPageView.as_view(), name='index'),
|
||||||
url(r'^$', views.frontpage, name='index'),
|
|
||||||
]
|
]
|
||||||
|
@ -1,5 +1,26 @@
|
|||||||
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import re
|
||||||
|
|
||||||
def hashid(msgid):
|
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,32 +1,42 @@
|
|||||||
from django.http import HttpResponse
|
from django.http import HttpResponse, Http404
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.views import generic
|
from django.views import generic
|
||||||
|
|
||||||
from .models import Post, Board
|
from .models import Post, Newsgroup
|
||||||
|
|
||||||
class IndexView(generic.DetailView):
|
|
||||||
template_name = 'frontend/index.html'
|
|
||||||
|
|
||||||
class BoardView(generic.ListView):
|
class BoardView(generic.ListView):
|
||||||
template_name = 'frontend/board.html'
|
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):
|
class ThreadView(generic.ListView):
|
||||||
template_name = 'frontend/thread.html'
|
template_name = 'frontend/thread.html'
|
||||||
|
model = Post
|
||||||
|
|
||||||
def frontpage(request):
|
def get_queryset(self):
|
||||||
return HttpResponse('ayyyy')
|
return get_object_or_404(self.model, posthash=self.kwargs['op'])
|
||||||
|
|
||||||
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):
|
class FrontPageView(generic.ListView):
|
||||||
pass
|
template_name = 'frontend/frontpage.html'
|
||||||
|
model = Post
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.model.objects.order_by('posted')[:10]
|
||||||
|
|
||||||
|
|
||||||
def modlog(request, page):
|
def modlog(request, page):
|
||||||
if page is None:
|
if page is None:
|
||||||
|
@ -55,7 +55,7 @@ ROOT_URLCONF = 'nntpchan.urls'
|
|||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
'DIRS': [],
|
'DIRS': ['nntpchan/templates'],
|
||||||
'APP_DIRS': True,
|
'APP_DIRS': True,
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'context_processors': [
|
'context_processors': [
|
||||||
@ -76,8 +76,11 @@ WSGI_APPLICATION = 'nntpchan.wsgi.application'
|
|||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
'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/
|
# https://docs.djangoproject.com/en/1.10/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
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 django.conf.urls import url, include
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
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):
|
from .frontend.models import Post, Attachment, Newsgroup
|
||||||
return HttpResponse('ayyyyy')
|
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