signal backend, app cleanup
This commit is contained in:
parent
4b157ed1d1
commit
6f18890739
43 changed files with 695 additions and 124 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -2,3 +2,6 @@ venv
|
||||||
*.swp
|
*.swp
|
||||||
__pycache__
|
__pycache__
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
|
www/
|
||||||
|
secret.txt
|
||||||
|
app/settings/local.py
|
||||||
|
|
10
app/brake_backend.py
Normal file
10
app/brake_backend.py
Normal 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
16
app/celery.py
Normal 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()
|
|
@ -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
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
17
app/item/migrations/0004_comment_session_key.py
Normal file
17
app/item/migrations/0004_comment_session_key.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -13,6 +13,7 @@ from django.urls import reverse
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Settings(models.Model):
|
class Settings(models.Model):
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
modified = models.DateTimeField(auto_now=True)
|
modified = models.DateTimeField(auto_now=True)
|
||||||
|
@ -37,6 +38,7 @@ class Item(models.Model):
|
||||||
if self.url and not self.data:
|
if self.url and not self.data:
|
||||||
self.update_data()
|
self.update_data()
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '%s (%s)' % (self.title, self.url)
|
return '%s (%s)' % (self.title, self.url)
|
||||||
|
|
||||||
|
@ -60,14 +62,15 @@ class Item(models.Model):
|
||||||
cal = now.date().isocalendar()
|
cal = now.date().isocalendar()
|
||||||
monday = now.date() - timedelta(days=now.date().isocalendar().weekday - 1)
|
monday = now.date() - timedelta(days=now.date().isocalendar().weekday - 1)
|
||||||
monday = timezone.datetime(monday.year, monday.month, monday.day, tzinfo=now.tzinfo)
|
monday = timezone.datetime(monday.year, monday.month, monday.day, tzinfo=now.tzinfo)
|
||||||
print(now.tzinfo)
|
|
||||||
first_post = qs.filter(published__gt=monday).first()
|
first_post = qs.filter(published__gt=monday).first()
|
||||||
print('!!', first_post.published, now, first_post.published > now)
|
if first_post and first_post.published < now:
|
||||||
if first_post.published < now:
|
week = qs.filter(published__gt=monday)
|
||||||
print('only this week')
|
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)
|
week = qs.filter(published__gt=monday)
|
||||||
else:
|
else:
|
||||||
print('only last week')
|
|
||||||
last_monday = monday - timedelta(days=7)
|
last_monday = monday - timedelta(days=7)
|
||||||
week = qs.filter(published__gt=last_monday)
|
week = qs.filter(published__gt=last_monday)
|
||||||
archive = qs.exclude(id__in=week)
|
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)
|
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)
|
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)
|
name = models.CharField(max_length=1024)
|
||||||
email = 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)
|
data = models.JSONField(default=dict, editable=False)
|
||||||
published = models.DateTimeField(null=True, default=None)
|
published = models.DateTimeField(null=True, default=None)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
permissions = [
|
||||||
|
("can_post_comment", "Can post comments without moderation")
|
||||||
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_published(self):
|
def is_published(self):
|
||||||
return bool(self.published)
|
return bool(self.published)
|
||||||
|
@ -128,7 +137,11 @@ class Comment(models.Model):
|
||||||
|
|
||||||
def json(self):
|
def json(self):
|
||||||
data = {}
|
data = {}
|
||||||
data['name'] = self.name
|
if not self.user:
|
||||||
|
data['name'] = '%s (guest)' % self.name
|
||||||
|
else:
|
||||||
|
data['name'] = self.name
|
||||||
data['date'] = self.date
|
data['date'] = self.date
|
||||||
data['text'] = self.text
|
data['text'] = self.text
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
28
app/item/tasks.py
Normal file
28
app/item/tasks.py
Normal 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
8
app/item/utils.py
Normal 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')
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
|
import json
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
from django.shortcuts import render
|
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 models
|
||||||
|
from . import tasks
|
||||||
|
from ..signalbot.rpc import send_reaction
|
||||||
|
from .utils import render_to_json
|
||||||
|
|
||||||
|
|
||||||
def index(request):
|
def index(request):
|
||||||
|
@ -19,33 +28,74 @@ def archive(request):
|
||||||
context['items'] = archive
|
context['items'] = archive
|
||||||
return render(request, 'archive.html', context)
|
return render(request, 'archive.html', context)
|
||||||
|
|
||||||
|
|
||||||
def item(request, id):
|
def item(request, id):
|
||||||
context = {}
|
context = {}
|
||||||
item = models.Item.objects.get(id=id)
|
item = models.Item.objects.get(id=id)
|
||||||
context['item'] = item
|
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)
|
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):
|
def comment(request):
|
||||||
response = {}
|
response = {}
|
||||||
data = json.loads(request.body)
|
data = json.loads(request.body)
|
||||||
print(data)
|
|
||||||
comment = models.Comment()
|
comment = models.Comment()
|
||||||
comment.item = models.Item.objects.get(id=data['item'])
|
comment.item = models.Item.objects.get(id=data['item'])
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
comment.user = request.user
|
comment.user = request.user
|
||||||
comment.published = datetime.now()
|
if comment.user.has_perm('app.item.can_post_comment'):
|
||||||
|
comment.published = timezone.now()
|
||||||
else:
|
else:
|
||||||
comment.name = data['name']
|
comment.name = data['name']
|
||||||
comment.email = data['email']
|
comment.email = data['email']
|
||||||
|
comment.session_key = request.session.session_key
|
||||||
comment.text = data['text']
|
comment.text = data['text']
|
||||||
comment.save()
|
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()
|
response = comment.json()
|
||||||
return render_to_json(response)
|
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
4
app/settings/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
try:
|
||||||
|
from .local import *
|
||||||
|
except ImportError:
|
||||||
|
from .common import *
|
|
@ -13,14 +13,25 @@ https://docs.djangoproject.com/en/4.2/ref/settings/
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# 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
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
SECRET_FILE = BASE_DIR / 'secret.txt'
|
||||||
SECRET_KEY = "django-insecure-()6&rdheil=iyz%36dl-fnb)a+*7*^cb%isz6x%fi+ong5#*zz"
|
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!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
@ -41,10 +52,13 @@ INSTALLED_APPS = [
|
||||||
|
|
||||||
'compressor',
|
'compressor',
|
||||||
'sass_processor',
|
'sass_processor',
|
||||||
|
'django_celery_results',
|
||||||
|
"brake",
|
||||||
|
|
||||||
"app",
|
"app",
|
||||||
"app.user",
|
"app.user",
|
||||||
"app.item",
|
"app.item",
|
||||||
|
"app.signalbot",
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
@ -122,7 +136,7 @@ USE_TZ = True
|
||||||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = "static/"
|
STATIC_URL = "static/"
|
||||||
STATIC_ROOT = "www/static"
|
STATIC_ROOT = "www/static/"
|
||||||
STATICFILES_FINDERS = (
|
STATICFILES_FINDERS = (
|
||||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||||
|
@ -137,3 +151,10 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
|
|
||||||
AUTH_USER_MODEL = 'user.User'
|
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"
|
4
app/settings/local.py.dist
Normal file
4
app/settings/local.py.dist
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
from .common import *
|
||||||
|
|
||||||
|
SIGNAL_ACCOUNT =
|
||||||
|
|
0
app/signalbot/__init__.py
Normal file
0
app/signalbot/__init__.py
Normal file
3
app/signalbot/admin.py
Normal file
3
app/signalbot/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
6
app/signalbot/apps.py
Normal file
6
app/signalbot/apps.py
Normal 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
54
app/signalbot/cli.py
Normal 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
41
app/signalbot/daemon.py
Normal 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()
|
14
app/signalbot/management/commands/signal-daemon.py
Normal file
14
app/signalbot/management/commands/signal-daemon.py
Normal 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()
|
||||||
|
|
0
app/signalbot/migrations/__init__.py
Normal file
0
app/signalbot/migrations/__init__.py
Normal file
3
app/signalbot/models.py
Normal file
3
app/signalbot/models.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
54
app/signalbot/rpc.py
Normal file
54
app/signalbot/rpc.py
Normal 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
2
app/signalbot/tasks.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
|
3
app/signalbot/tests.py
Normal file
3
app/signalbot/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
3
app/signalbot/views.py
Normal file
3
app/signalbot/views.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
|
@ -88,3 +88,14 @@
|
||||||
.comments .meta {
|
.comments .meta {
|
||||||
color: gray;
|
color: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.publish-comment {
|
||||||
|
border: 0;
|
||||||
|
margin: 0;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -71,8 +71,8 @@ window.VideoPlayer = function(options) {
|
||||||
height: 64px;
|
height: 64px;
|
||||||
}
|
}
|
||||||
.mx-controls .toggle .loading svg {
|
.mx-controls .toggle .loading svg {
|
||||||
width: 32px;
|
width: 64px;
|
||||||
height: 32px;
|
height: 64px;
|
||||||
}
|
}
|
||||||
.mx-controls .controls .volume svg,
|
.mx-controls .controls .volume svg,
|
||||||
.mx-controls .controls .fullscreen-btn svg {
|
.mx-controls .controls .fullscreen-btn svg {
|
||||||
|
|
|
@ -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) {
|
function renderComments(cdiv, data) {
|
||||||
cdiv.innerHTML = `
|
cdiv.innerHTML = `
|
||||||
<h3>
|
<h3>
|
||||||
|
@ -11,6 +56,14 @@ function renderComments(cdiv, data) {
|
||||||
`
|
`
|
||||||
const content = cdiv.querySelector('.comments-content')
|
const content = cdiv.querySelector('.comments-content')
|
||||||
comments.forEach(comment => {
|
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')
|
var c = document.createElement('div')
|
||||||
c.className = 'comment'
|
c.className = 'comment'
|
||||||
c.innerHTML = `
|
c.innerHTML = `
|
||||||
|
@ -19,8 +72,18 @@ function renderComments(cdiv, data) {
|
||||||
by <span class="name">${comment.name}</span>
|
by <span class="name">${comment.name}</span>
|
||||||
on
|
on
|
||||||
<span class="date">${comment.date}</span>
|
<span class="date">${comment.date}</span>
|
||||||
|
${extra}
|
||||||
</div>
|
</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)
|
content.append(c)
|
||||||
})
|
})
|
||||||
var add = document.querySelector('.add-comment')
|
var add = document.querySelector('.add-comment')
|
||||||
|
|
|
@ -123,6 +123,15 @@ icon.pause = `
|
||||||
</svg>
|
</svg>
|
||||||
`
|
`
|
||||||
icon.loading = `
|
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">
|
<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">
|
<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">
|
<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"/>
|
<polygon points="32,56 224,56 128,248" fill="#808080"/>
|
||||||
</svg>
|
</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>
|
||||||
|
`
|
||||||
|
|
|
@ -86,8 +86,8 @@ function render() {
|
||||||
var loadingScreen = `
|
var loadingScreen = `
|
||||||
<style>
|
<style>
|
||||||
svg {
|
svg {
|
||||||
width: 32px;
|
width: 64px;
|
||||||
height: 32px;
|
height: 64px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div style="margin: auto;width: 64px;height: 64px;">${icon.loading}</div>
|
<div style="margin: auto;width: 64px;height: 64px;">${icon.loading}</div>
|
||||||
|
|
|
@ -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 %}
|
|
|
@ -8,12 +8,11 @@
|
||||||
<div class="add-comment" style="display: none">
|
<div class="add-comment" style="display: none">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<textarea name="text" placeholder="your comment"></textarea>
|
<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="name" type="text" placeholder="your name"></input>
|
||||||
<input name="email" type="email" placeholder="your email"></input>
|
<input name="email" type="email" placeholder="your email"></input>
|
||||||
<br>
|
<br>
|
||||||
{% endif %}
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button id="add-comment">Add comment as guest</button>
|
<button id="add-comment">Add comment as guest</button>
|
||||||
<button>Login</button>
|
<button>Login</button>
|
||||||
|
@ -24,11 +23,18 @@
|
||||||
<button>Login</button>
|
<button>Login</button>
|
||||||
<button>Register</button>
|
<button>Register</button>
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="buttons">
|
||||||
|
<button id="add-comment">Add comment</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block end %}
|
{% block end %}
|
||||||
<script>
|
<script>
|
||||||
var comments = {{ item.public_comments_json|safe }};
|
var comments = {{ comments }};
|
||||||
|
var user = {{ user }};
|
||||||
</script>
|
</script>
|
||||||
{% compress js file m %}
|
{% compress js file m %}
|
||||||
<script src="/static/js/utils.js"></script>
|
<script src="/static/js/utils.js"></script>
|
||||||
|
|
|
@ -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>
|
|
|
@ -18,10 +18,15 @@ from django.contrib import admin
|
||||||
from django.urls import path, re_path
|
from django.urls import path, re_path
|
||||||
|
|
||||||
from .item import views as item_views
|
from .item import views as item_views
|
||||||
|
from .user import views as user_views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("admin/", admin.site.urls),
|
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('archive/', item_views.archive, name='archive'),
|
||||||
|
path('comment/publish/', item_views.publish_comment, name='publish-comment'),
|
||||||
path('comment/', item_views.comment, name='comment'),
|
path('comment/', item_views.comment, name='comment'),
|
||||||
path('<int:id>/', item_views.item, name='item'),
|
path('<int:id>/', item_views.item, name='item'),
|
||||||
path('', item_views.index, name='index'),
|
path('', item_views.index, name='index'),
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
from django.contrib import admin
|
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)
|
||||||
|
|
|
@ -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
4
bootstrap.sh
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/sh
|
||||||
|
python3 -m venv venv
|
||||||
|
./venv/bin/pip install -U pip
|
||||||
|
./venv/bin/pip install -r requirements.txt
|
20
etc/systemd/system/phantasmobile-cron.service
Normal file
20
etc/systemd/system/phantasmobile-cron.service
Normal 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
|
||||||
|
|
15
etc/systemd/system/phantasmobile-signal-daemon.service
Normal file
15
etc/systemd/system/phantasmobile-signal-daemon.service
Normal 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
|
19
etc/systemd/system/phantasmobile-tasks.service
Normal file
19
etc/systemd/system/phantasmobile-tasks.service
Normal 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
|
18
etc/systemd/system/phantasmobile.service
Normal file
18
etc/systemd/system/phantasmobile.service
Normal 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
|
1
etc/tmpfiles.d/phantasmobile.conf
Normal file
1
etc/tmpfiles.d/phantasmobile.conf
Normal file
|
@ -0,0 +1 @@
|
||||||
|
d /run/phantasmobile 0755 phantasmobile phantasmobile -
|
26
manage.py
26
manage.py
|
@ -1,11 +1,35 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
"""Django's command-line utility for administrative tasks."""
|
"""Django's command-line utility for administrative tasks."""
|
||||||
import os
|
import os
|
||||||
import sys
|
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():
|
def main():
|
||||||
"""Run administrative tasks."""
|
"""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")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings")
|
||||||
try:
|
try:
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
django
|
django
|
||||||
|
Pillow
|
||||||
libsass
|
libsass
|
||||||
django-compressor
|
django-compressor
|
||||||
django-sass-processor
|
django-sass-processor
|
||||||
requests
|
requests
|
||||||
lxml
|
lxml
|
||||||
cssselect
|
cssselect
|
||||||
|
gunicorn
|
||||||
|
celery
|
||||||
|
django-celery-results
|
||||||
|
django-celery-beat
|
||||||
|
redis
|
||||||
|
django-brake
|
||||||
|
|
Loading…
Reference in a new issue