signal backend, app cleanup

This commit is contained in:
j 2023-07-24 12:05:45 +01:00
parent 4b157ed1d1
commit 6f18890739
43 changed files with 695 additions and 124 deletions

3
.gitignore vendored
View file

@ -2,3 +2,6 @@ venv
*.swp
__pycache__
db.sqlite3
www/
secret.txt
app/settings/local.py

10
app/brake_backend.py Normal file
View file

@ -0,0 +1,10 @@
from brake.backends import cachebe
class BrakeBackend(cachebe.CacheBackend):
def get_ip(self, request):
ip = request.META.get("HTTP_X_FORWARDED_FOR", request.META.get("REMOTE_ADDR"))
if ip:
ip = ip.split(", ")[-1]
return ip

16
app/celery.py Normal file
View file

@ -0,0 +1,16 @@
import os
from celery import Celery
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')
app = Celery('app')
# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object('django.conf:settings', namespace='CELERY')
# Load task modules from all registered Django app configs.
app.autodiscover_tasks()

View file

@ -0,0 +1,47 @@
# Generated by Django 4.2.3 on 2023-07-24 14:07
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("item", "0002_initial"),
]
operations = [
migrations.AlterModelOptions(
name="comment",
options={
"permissions": [
("can_post_comment", "Can post comments without moderation")
]
},
),
migrations.AlterField(
model_name="comment",
name="user",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="comments",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name="item",
name="announced",
field=models.DateTimeField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name="item",
name="published",
field=models.DateTimeField(
blank=True, default=django.utils.timezone.now, null=True
),
),
]

View file

@ -0,0 +1,17 @@
# Generated by Django 4.2.3 on 2023-07-24 17:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("item", "0003_alter_comment_options_alter_comment_user_and_more"),
]
operations = [
migrations.AddField(
model_name="comment",
name="session_key",
field=models.CharField(blank=True, default=None, max_length=60, null=True),
),
]

View file

@ -13,6 +13,7 @@ from django.urls import reverse
User = get_user_model()
class Settings(models.Model):
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
@ -37,6 +38,7 @@ class Item(models.Model):
if self.url and not self.data:
self.update_data()
super().save(*args, **kwargs)
def __str__(self):
return '%s (%s)' % (self.title, self.url)
@ -60,14 +62,15 @@ class Item(models.Model):
cal = now.date().isocalendar()
monday = now.date() - timedelta(days=now.date().isocalendar().weekday - 1)
monday = timezone.datetime(monday.year, monday.month, monday.day, tzinfo=now.tzinfo)
print(now.tzinfo)
first_post = qs.filter(published__gt=monday).first()
print('!!', first_post.published, now, first_post.published > now)
if first_post.published < now:
print('only this week')
if first_post and first_post.published < now:
week = qs.filter(published__gt=monday)
elif not first_post:
while qs.exists() and not first_post:
monday = monday - timedelta(days=7)
first_post = qs.filter(published__gt=monday).first()
week = qs.filter(published__gt=monday)
else:
print('only last week')
last_monday = monday - timedelta(days=7)
week = qs.filter(published__gt=last_monday)
archive = qs.exclude(id__in=week)
@ -102,6 +105,7 @@ class Comment(models.Model):
item = models.ForeignKey(Item, related_name='comments', on_delete=models.CASCADE)
user = models.ForeignKey(User, null=True, related_name='comments', on_delete=models.CASCADE, blank=True)
session_key = models.CharField(max_length=60, null=True, default=None, blank=True)
name = models.CharField(max_length=1024)
email = models.CharField(max_length=1024)
@ -109,6 +113,11 @@ class Comment(models.Model):
data = models.JSONField(default=dict, editable=False)
published = models.DateTimeField(null=True, default=None)
class Meta:
permissions = [
("can_post_comment", "Can post comments without moderation")
]
@property
def is_published(self):
return bool(self.published)
@ -128,7 +137,11 @@ class Comment(models.Model):
def json(self):
data = {}
if not self.user:
data['name'] = '%s (guest)' % self.name
else:
data['name'] = self.name
data['date'] = self.date
data['text'] = self.text
return data

28
app/item/tasks.py Normal file
View file

@ -0,0 +1,28 @@
from celery.schedules import crontab
from django.conf import settings
from ..signalbot import rpc
from ..celery import app
from . import models
@app.task(queue="default")
def announce_items():
pass
@app.task(queue="default")
def notify_moderators(id, link):
comment = models.Comment.objects.filter(id=id).first()
if comment:
message = "%s commnented on %s\n\n%s" % (comment.name, link, comment.text)
r = rpc.send(message, group=settings.SIGNAL_MODERATORS_GROUP)
if r and "timestamp" in r:
comment.data["moderator_ts"] = r["timestamp"]
comment.save()
else:
print("failed to notify", r)
@app.on_after_finalize.connect
def setup_periodic_tasks(sender, **kwargs):
sender.add_periodic_task(crontab(minute="*/2"), announce_items.s())

8
app/item/utils.py Normal file
View file

@ -0,0 +1,8 @@
import json
from django.http import Http404, HttpResponse
def render_to_json(response):
content = json.dumps(response)
return HttpResponse(content, 'application/json; charset=utf-8')

View file

@ -1,7 +1,16 @@
from datetime import date, datetime, timedelta
import json
from django.utils import timezone
from django.shortcuts import render
from django.db.models import Q
from django.utils.html import mark_safe
from django.conf import settings
from . import models
from . import tasks
from ..signalbot.rpc import send_reaction
from .utils import render_to_json
def index(request):
@ -19,33 +28,74 @@ def archive(request):
context['items'] = archive
return render(request, 'archive.html', context)
def item(request, id):
context = {}
item = models.Item.objects.get(id=id)
context['item'] = item
qs = item.comments.order_by('created')
if not request.user.is_staff:
q = ~Q(published=None)
if request.user.is_authenticated:
q |= Q(user=request.user)
if request.session and request.session.session_key:
q |= Q(session_key=request.session.session_key)
qs = qs.filter(q)
comments = []
for comment in qs:
comments.append({
"id": comment.id,
"name": comment.name,
"date": comment.date,
"text": comment.text,
"published": comment.is_published,
})
context['comments'] = mark_safe(json.dumps(comments))
user = {}
if request.user.is_staff:
user['is_moderator'] = True
if request.user.is_authenticated:
user['username'] = request.user.username
context['user'] = mark_safe(json.dumps(user))
request.session['item'] = id
return render(request, 'item.html', context)
import json
from django.http import Http404, HttpResponse
def render_to_json(response):
content = json.dumps(response)
return HttpResponse(content, 'application/json; charset=utf-8')
def comment(request):
response = {}
data = json.loads(request.body)
print(data)
comment = models.Comment()
comment.item = models.Item.objects.get(id=data['item'])
if request.user.is_authenticated:
comment.user = request.user
comment.published = datetime.now()
if comment.user.has_perm('app.item.can_post_comment'):
comment.published = timezone.now()
else:
comment.name = data['name']
comment.email = data['email']
comment.session_key = request.session.session_key
comment.text = data['text']
comment.save()
if not comment.published:
link = request.build_absolute_uri(comment.item.get_absolute_url())
tasks.notify_moderators.delay(comment.id, link)
response = comment.json()
return render_to_json(response)
def publish_comment(request):
response = {}
data = json.loads(request.body)
if request.user.is_staff:
comment = models.Comment.objects.get(id=data['comment'])
comment.published = timezone.now()
comment.save()
if comment.data.get("moderator_ts"):
account = settings.SIGNAL_ACCOUNT
group = settings.SIGNAL_MODERATORS_GROUP
send_reaction(account, comment.data["moderator_ts"], "🎉", group=group)
response['status'] = 'ok'
else:
response['error'] = 'permission denied'
return render_to_json(response)

4
app/settings/__init__.py Normal file
View file

@ -0,0 +1,4 @@
try:
from .local import *
except ImportError:
from .common import *

View file

@ -13,14 +13,25 @@ https://docs.djangoproject.com/en/4.2/ref/settings/
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
BASE_DIR = Path(__file__).resolve().parent.parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-()6&rdheil=iyz%36dl-fnb)a+*7*^cb%isz6x%fi+ong5#*zz"
SECRET_FILE = BASE_DIR / 'secret.txt'
try:
SECRET_KEY = open(SECRET_FILE).read().strip()
except IOError:
try:
from django.utils.crypto import get_random_string
chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)'
SECRET_KEY = get_random_string(50, chars)
secret = open(SECRET_FILE, 'w')
secret.write(SECRET_KEY)
secret.close()
except IOError:
raise Exception('Please create a %s file with random characters to generate your secret key!' % SECRET_FILE)
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
@ -41,10 +52,13 @@ INSTALLED_APPS = [
'compressor',
'sass_processor',
'django_celery_results',
"brake",
"app",
"app.user",
"app.item",
"app.signalbot",
]
MIDDLEWARE = [
@ -122,7 +136,7 @@ USE_TZ = True
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = "static/"
STATIC_ROOT = "www/static"
STATIC_ROOT = "www/static/"
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
@ -137,3 +151,10 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
AUTH_USER_MODEL = 'user.User'
CELERY_BROKER_URL = "redis://localhost:6379"
CELERY_RESULT_BACKEND = 'django-db'
SIGNAL_MODERATORS = []
RATELIMIT_CACHE_BACKEND = "app.brake_backend.BrakeBackend"

View file

@ -0,0 +1,4 @@
from .common import *
SIGNAL_ACCOUNT =

View file

3
app/signalbot/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
app/signalbot/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class SignalbotConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "app.signalbot"

54
app/signalbot/cli.py Normal file
View file

@ -0,0 +1,54 @@
import json
import subprocess
from django.conf import settings
BASE_CMD = ['signal-cli', '-a', settings.SIGNAL_ACCOUNT, '--output=json']
def send(msg, to=None, group=None):
cmd = BASE_CMD + [
'send', '--message-from-stdin'
]
if group:
cmd += ['-g', group]
else:
cmd += [to]
r = subprocess.check_output(cmd, input=msg, encoding='utf-8')
response = []
if r:
for row in r.strip().split('\n'):
response.append(json.loads(row))
return response
def send_reaction(target_address, target_ts, emoji, to=None, group=None, remove=False):
cmd = BASE_CMD + [
'sendReaction', '-t', str(target_ts), '-e', emoji,
'-a', target_address
]
if remove:
cmd += ['-r']
if group:
cmd += ['-g', group]
else:
cmd += [to]
r = subprocess.check_output(cmd, encoding='utf-8')
response = []
if r:
for row in r.strip().split('\n'):
response.append(json.loads(row))
return response
def receive(timeout=1):
cmd = BASE_CMD + [
'receive', '--timeout', str(timeout), '--send-read-receipts'
]
r = subprocess.check_output(cmd, encoding='utf-8')
response = []
if r:
for row in r.strip().split('\n'):
response.append(json.loads(row))
return response

41
app/signalbot/daemon.py Normal file
View file

@ -0,0 +1,41 @@
import subprocess
import json
from django.conf import settings
from django.utils import timezone
from . import cli
from . import rpc
def main():
cmd = cli.BASE_CMD + [
'daemon', '--http', '--send-read-receipts'
]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True, start_new_session=True)
try:
for line in p.stdout:
try:
msg = json.loads(line)
except:
if settings.DEBUG:
print(">>", line)
continue
if settings.DEBUG:
print('==', msg)
source = msg.get('envelope', {}).get('sourceNumber')
reaction = msg.get('envelope', {}).get('dataMessage', {}).get('reaction', {})
emoji = reaction.get('emoji')
target_author = reaction.get('targetAuthorNumber')
target_ts = reaction.get('targetSentTimestamp')
group = msg.get('envelope', {}).get('dataMessage', {}).get("groupInfo", {}).get("groupId")
if group == settings.SIGNAL_MODERATORS_GROUP:
if emoji == "👍" and target_author == settings.SIGNAL_ACCOUNT:
if source in settings.SIGNAL_MODERATORS:
from ..item import models
now = timezone.now()
models.Comment.objects.filter(data__moderator_ts=target_ts).update(published=now)
rpc.send_reaction(target_author, target_ts, "🎉", group=group)
else:
rpc.send("Ignoring your request, you are not a moderator", group=group)
except:
p.kill()

View file

@ -0,0 +1,14 @@
from django.core.management.base import BaseCommand
from ... import daemon
class Command(BaseCommand):
help = 'process incoming singal messages'
def add_arguments(self, parser):
pass
def handle(self, *args, **options):
daemon.main()

View file

3
app/signalbot/models.py Normal file
View file

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

54
app/signalbot/rpc.py Normal file
View file

@ -0,0 +1,54 @@
import json
import subprocess
import requests
from django.conf import settings
rpc_id = 1
def api(method, params):
global rpc_id
rpc_id += 1
url = "http://127.0.0.1:8080/api/v1/rpc"
msg = {
"jsonrpc": "2.0",
"id": str(rpc_id),
"method": method,
}
if params:
msg["params"] = params
if settings.DEBUG:
print("POST signal rpc:", msg)
response = requests.post(url, json=msg).json()
if "result" in response:
return response["result"]
else:
raise Exception("Error: %s", response)
def send(msg, to=None, group=None):
params = {
"message": msg
}
if group:
params["groupId"] = group
else:
params["recipient"] = to
return api("send", params)
def send_reaction(target_address, target_ts, emoji, to=None, group=None, remove=False):
params = {
"emoji": emoji,
"targetTimestamp": target_ts,
"targetAuthor": target_address,
}
if group:
params["groupId"] = group
else:
params["recipient"] = to
return api("sendReaction", params)

2
app/signalbot/tasks.py Normal file
View file

@ -0,0 +1,2 @@

3
app/signalbot/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
app/signalbot/views.py Normal file
View file

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View file

@ -88,3 +88,14 @@
.comments .meta {
color: gray;
}
button.publish-comment {
border: 0;
margin: 0;
background: none;
cursor: pointer;
svg {
width: 16px;
height: 16px;
}
}

View file

@ -71,8 +71,8 @@ window.VideoPlayer = function(options) {
height: 64px;
}
.mx-controls .toggle .loading svg {
width: 32px;
height: 32px;
width: 64px;
height: 64px;
}
.mx-controls .controls .volume svg,
.mx-controls .controls .fullscreen-btn svg {

View file

@ -1,3 +1,48 @@
async function publish_comment(id) {
var csrf
document.querySelector('.add-comment').querySelectorAll('input,textarea').forEach(input => {
if (input.name == 'csrfmiddlewaretoken') {
csrf = input.value.trim()
}
})
var data = {
"comment": id
}
return fetch("/comment/publish/", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrf
},
body: JSON.stringify(data)
}).then(response => {
return response.json()
})
}
async function login(username, password) {
var csrf
document.querySelector('.add-comment').querySelectorAll('input,textarea').forEach(input => {
if (input.name == 'csrfmiddlewaretoken') {
csrf = input.value.trim()
}
})
var data = {
"username": username,
"password": password,
}
return fetch("/login/", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrf
},
body: JSON.stringify(data)
}).then(response => {
return response.json()
})
}
function renderComments(cdiv, data) {
cdiv.innerHTML = `
<h3>
@ -11,6 +56,14 @@ function renderComments(cdiv, data) {
`
const content = cdiv.querySelector('.comments-content')
comments.forEach(comment => {
var extra = ''
if (!comment.published) {
if (user.is_moderator) {
extra += `<button class="publish-comment">${icon.publishComment}</button>`
} else {
extra += '(under review)'
}
}
var c = document.createElement('div')
c.className = 'comment'
c.innerHTML = `
@ -19,8 +72,18 @@ function renderComments(cdiv, data) {
by <span class="name">${comment.name}</span>
on
<span class="date">${comment.date}</span>
${extra}
</div>
`
c.querySelectorAll('button.publish-comment').forEach(button => {
button.title = "click to publish"
button.addEventListener("click", event => {
button.disabled = true
publish_comment(comment.id).then(response => {
button.remove()
})
})
})
content.append(c)
})
var add = document.querySelector('.add-comment')

View file

@ -123,6 +123,15 @@ icon.pause = `
</svg>
`
icon.loading = `
<svg width="512" height="512" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="#B1B1B1" stroke-dasharray="15" stroke-dashoffset="15" stroke-linecap="round" stroke-width="2" d="M12 3C16.9706 3 21 7.02944 21 12">
<animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="15;0"/>
<animateTransform attributeName="transform" dur="1.5s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/>
</path>
</svg>
`
icon.loading_w = `
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
<g transform="translate(128, 128)" stroke="#404040" stroke-linecap="round" stroke-width="28">
<line x1="0" y1="-114" x2="0" y2="-70" transform="rotate(0)" opacity="1">
@ -180,3 +189,12 @@ icon.down = `
<polygon points="32,56 224,56 128,248" fill="#808080"/>
</svg>
`
icon.publishComment = `
<svg width="512" height="512" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<g fill="#ef4444">
<path d="M12.354 4.354a.5.5 0 0 0-.708-.708L5 10.293L1.854 7.146a.5.5 0 1 0-.708.708l3.5 3.5a.5.5 0 0 0 .708 0l7-7zm-4.208 7l-.896-.897l.707-.707l.543.543l6.646-6.647a.5.5 0 0 1 .708.708l-7 7a.5.5 0 0 1-.708 0z"/>
<path d="m5.354 7.146l.896.897l-.707.707l-.897-.896a.5.5 0 1 1 .708-.708z"/>
</g>
</svg>
`

View file

@ -86,8 +86,8 @@ function render() {
var loadingScreen = `
<style>
svg {
width: 32px;
height: 32px;
width: 64px;
height: 64px;
}
</style>
<div style="margin: auto;width: 64px;height: 64px;">${icon.loading}</div>

View file

@ -1,77 +0,0 @@
{% extends "base.html" %}
{% block head %}
<link rel="stylesheet" href="css/reset.css"></link>
<link rel="stylesheet" href="css/style.css"></link>
{% endblock %}
{% block content %}
<style>
body {
margin: 0;
padding: 0;
background: turquoise;
}
.add-comment {
width: 100%;
height: 30vh;
text-align: center;
}
.add-comment input {
width: 100px;
}
.add-comment textarea {
width: 80vw;
display: block;
background: black;
color: white;
margin: auto;
padding: 0;
border: 0;
}
.add-comment button {
background: red;
border: 0;
}
.comment .text {
white-space: pre-line;
}
</style>
<div class="item">
<iframe src="{{ item.url }}" frameborder="0" allowfullscreen style="border:0;padding:0;margin:0;width:100%;height:100vh"></iframe>
</div>
<div class="comments">
{% for comment in item.public_comments %}
<div class="comment">
<div class="name">{{ comment.name }}</div>
<div class="date">{{ comment.date }}</div>
<div class="text">{{ comment.text }}</div>
</div>
{% endfor %}
</div>
<div class="add-comment">
{% if request.user.is_anonymous %}
<input name="name" type="text" placeholder="your name"></input>
<input name="email" type="email" placeholder="your email"></input>
<br>
{% endif %}
{% csrf_token %}
<textarea name="text" placeholder="your comment"></textarea>
<button id="add-comment">Add comment</button>
</div>
{% endblock %}
{% block end %}
<script src="/static/js/utils./static/js"></script>
<script src="/static/js/api./static/js"></script>
<script src="/static/js/icons./static/js"></script>
<script src="/static/js/VideoElement./static/js"></script>
<script src="/static/js/VideoPlayer./static/js"></script>
<script src="/static/js/documents./static/js"></script>
<script src="/static/js/edits./static/js"></script>
<script src="/static/js/item./static/js"></script>
<script src="/static/js/render./static/js"></script>
<script src="/static/js/main./static/js"></script>
<script src="/static/js/comments./static/js"></script>
{% endblock %}

View file

@ -8,12 +8,11 @@
<div class="add-comment" style="display: none">
{% csrf_token %}
<textarea name="text" placeholder="your comment"></textarea>
{% if false and request.user.is_anonymous %}
<div class="user">
{% if request.user.is_anonymous %}
<input name="name" type="text" placeholder="your name"></input>
<input name="email" type="email" placeholder="your email"></input>
<br>
{% endif %}
<div class="buttons">
<button id="add-comment">Add comment as guest</button>
<button>Login</button>
@ -24,11 +23,18 @@
<button>Login</button>
<button>Register</button>
</div>
{% else %}
<div class="buttons">
<button id="add-comment">Add comment</button>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block end %}
<script>
var comments = {{ item.public_comments_json|safe }};
var comments = {{ comments }};
var user = {{ user }};
</script>
{% compress js file m %}
<script src="/static/js/utils.js"></script>

View file

@ -1,16 +0,0 @@
<!DOCTYPE html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<div class="content"></div>
<link rel="stylesheet" href="css/reset.css"></link>
<link rel="stylesheet" href="css/style.css"></link>
<script src="js/utils.js"></script>
<script src="js/api.js"></script>
<script src="js/icons.js"></script>
<script src="js/VideoElement.js"></script>
<script src="js/VideoPlayer.js"></script>
<script src="js/documents.js"></script>
<script src="js/edits.js"></script>
<script src="js/item.js"></script>
<script src="js/render.js"></script>
<script src="js/main.js"></script>

View file

@ -18,10 +18,15 @@ from django.contrib import admin
from django.urls import path, re_path
from .item import views as item_views
from .user import views as user_views
urlpatterns = [
path("admin/", admin.site.urls),
path('login/', user_views.login, name='login'),
path('logout/', user_views.logout, name='logout'),
path('register/', user_views.register, name='register'),
path('archive/', item_views.archive, name='archive'),
path('comment/publish/', item_views.publish_comment, name='publish-comment'),
path('comment/', item_views.comment, name='comment'),
path('<int:id>/', item_views.item, name='item'),
path('', item_views.index, name='index'),

View file

@ -1,3 +1,5 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User
# Register your models here.
admin.site.register(User, UserAdmin)

View file

@ -1,3 +1,50 @@
from django.shortcuts import render
import json
# Create your views here.
from django.shortcuts import render
from django.shortcuts import redirect
import django.contrib.auth
from django.contrib.auth import get_user_model
from ..item.utils import render_to_json
from brake.decorators import ratelimit
User = get_user_model()
@ratelimit(method="POST", block=True, rate="1/m")
def register(request):
response = {}
data = json.loads(request.body)
if User.objects.filter(username__iexact=data['username']).exists():
response['error'] = 'username not allowed'
elif User.objects.filter(email__iexact=data['email']).exists():
response['error'] = 'username not allowed'
elif not data['password']:
response['error'] = 'password too simple'
if not response:
user = User(username=data['username'], email=data['email'].lower())
user.set_password(data['password'])
user.is_active = True
user.save()
user = django.contrib.auth.authenticate(username=data['username'], password=data['password'])
django.contrib.auth.login(request, user)
response['user'] = user.username
return render_to_json(response)
@ratelimit(method="POST", block=True, rate="1/m")
def login(request):
response = {}
data = json.loads(request.body)
user = django.contrib.auth.authenticate(username=data['username'], password=data['password'])
if user is not None and user.is_active:
django.contrib.auth.login(request, user)
response['user'] = user.username
return render_to_json(response)
def logout(request):
if request.user.is_authenticated:
django.contrib.auth.logout(request)
redirect('/')

4
bootstrap.sh Executable file
View file

@ -0,0 +1,4 @@
#!/bin/sh
python3 -m venv venv
./venv/bin/pip install -U pip
./venv/bin/pip install -r requirements.txt

View file

@ -0,0 +1,20 @@
[Unit]
Description=phantasmobile cron
After=phantasmobile-tasks.service
[Service]
Type=simple
Restart=always
User=phantasmobile
Group=phantasmobile
PIDFile=/run/phantasmobile/phantasmobile-cron.pid
WorkingDirectory=/srv/phantasmobile
ExecStart=/srv/phantasmobile/venv/bin/celery \
-A app beat -l info \
--scheduler django_celery_beat.schedulers:DatabaseScheduler \
--pidfile /run/phantasmobile/phantasmobile-cron.pid
ExecReload=/bin/kill -HUP $MAINPID
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,15 @@
[Unit]
Description=phantasmobile signal-daemon
After=postgresql.service
[Service]
Type=simple
Restart=always
User=phantasmobile
Group=phantasmobile
WorkingDirectory=/srv/phantasmobile
ExecStart=/srv/phantasmobile/manage.py signal-daemon
ExecReload=/bin/kill -HUP $MAINPID
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,19 @@
[Unit]
Description=phantasmobile tasks
After=postgresql.service
[Service]
Type=simple
Restart=always
User=phantasmobile
Group=phantasmobile
PIDFile=/run/phantasmobile/phantasmobile-tasks.pid
WorkingDirectory=/srv/phantasmobile
ExecStart=/srv/phantasmobile/venv/bin/celery \
-A app worker -l info \
-Q default -c 4 \
--pidfile /run/phantasmobile/phantasmobile-tasks.pid
ExecReload=/bin/kill -HUP $MAINPID
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,18 @@
[Unit]
Description=phantasmobile daemon
After=postgresql.service
[Service]
Type=simple
Restart=always
User=phantasmobile
Group=phantasmobile
WorkingDirectory=/srv/phantasmobile
ExecReload=/bin/kill -HUP $MAINPID
ExecStart=/srv/phantasmobile/venv/bin/gunicorn \
app.wsgi:application \
-c app/gunicorn_config.py \
-p /run/phantasmobile/phantasmobile.pid
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1 @@
d /run/phantasmobile 0755 phantasmobile phantasmobile -

View file

@ -1,11 +1,35 @@
#!/usr/bin/env python
#!/usr/bin/env python3
"""Django's command-line utility for administrative tasks."""
import os
import sys
def activate_venv(base):
if os.path.exists(base):
old_os_path = os.environ.get("PATH", "")
os.environ["PATH"] = os.path.join(base, "bin") + os.pathsep + old_os_path
version = "%s.%s" % (sys.version_info.major, sys.version_info.minor)
site_packages = os.path.join(base, "lib", "python%s" % version, "site-packages")
prev_sys_path = list(sys.path)
import site
site.addsitedir(site_packages)
sys.real_prefix = sys.prefix
sys.prefix = base
# Move the added items to the front of the path:
new_sys_path = []
for item in list(sys.path):
if item not in prev_sys_path:
new_sys_path.append(item)
sys.path.remove(item)
sys.path[:0] = new_sys_path
def main():
"""Run administrative tasks."""
root_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__)))
activate_venv(os.path.normpath(os.path.join(root_dir, "venv")))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings")
try:
from django.core.management import execute_from_command_line

View file

@ -1,7 +1,14 @@
django
Pillow
libsass
django-compressor
django-sass-processor
requests
lxml
cssselect
gunicorn
celery
django-celery-results
django-celery-beat
redis
django-brake