you can send messages
This commit is contained in:
parent
aeb052f26e
commit
103998c0e6
9 changed files with 236 additions and 12 deletions
6
tmessage/apps.py
Normal file
6
tmessage/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class DBConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "tmessage"
|
56
tmessage/migrations/0001_initial.py
Normal file
56
tmessage/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
# Generated by Django 5.0.7 on 2024-12-23 15:14
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='tMMessage',
|
||||||
|
fields=[
|
||||||
|
('message_id', models.IntegerField(primary_key=True, serialize=False)),
|
||||||
|
('content', models.CharField(max_length=10000)),
|
||||||
|
('response', models.CharField(blank=True, max_length=10000, null=True)),
|
||||||
|
('anonymous', models.BooleanField()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='tMUser',
|
||||||
|
fields=[
|
||||||
|
('username', models.CharField(max_length=30, primary_key=True, serialize=False, unique=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='tMM2MLike',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='likes_obj', to='tmessage.tmmessage')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='liked_obj', to='tmessage.tmuser')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('user', 'message')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='tmmessage',
|
||||||
|
name='likes',
|
||||||
|
field=models.ManyToManyField(related_name='liked', through='tmessage.tMM2MLike', to='tmessage.tmuser'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='tmmessage',
|
||||||
|
name='u_from',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sent', to='tmessage.tmuser'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='tmmessage',
|
||||||
|
name='u_to',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received', to='tmessage.tmuser'),
|
||||||
|
),
|
||||||
|
]
|
0
tmessage/migrations/__init__.py
Normal file
0
tmessage/migrations/__init__.py
Normal file
48
tmessage/models.py
Normal file
48
tmessage/models.py
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from django.contrib import admin as django_admin
|
||||||
|
from django.contrib.admin.exceptions import AlreadyRegistered # type: ignore
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class tMUser(models.Model):
|
||||||
|
username = models.CharField(max_length=30, unique=True, primary_key=True)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
liked = models.Manager["tMMessage"]
|
||||||
|
sent = models.Manager["tMMessage"]
|
||||||
|
received = models.Manager["tMMessage"]
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.username
|
||||||
|
|
||||||
|
class tMMessage(models.Model):
|
||||||
|
message_id = models.IntegerField(primary_key=True)
|
||||||
|
content = models.CharField(max_length=10_000)
|
||||||
|
response = models.CharField(max_length=10_000, blank=True, null=True)
|
||||||
|
anonymous = models.BooleanField()
|
||||||
|
likes = models.ManyToManyField(tMUser, through="tMM2MLike", related_name="liked")
|
||||||
|
u_to = models.ForeignKey(tMUser, on_delete=models.CASCADE, related_name="received")
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
u_from: tMUser | None
|
||||||
|
else:
|
||||||
|
u_from = models.ForeignKey(tMUser, on_delete=models.SET_NULL, blank=True, null=True, related_name="sent")
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"({self.message_id}) {self.u_from.username if self.u_from else 'Anonymous'} messaged {self.u_to.username}"
|
||||||
|
|
||||||
|
class tMM2MLike(models.Model):
|
||||||
|
user = models.ForeignKey(tMUser, on_delete=models.CASCADE, related_name="liked_obj")
|
||||||
|
message = models.ForeignKey(tMMessage, on_delete=models.CASCADE, related_name="likes_obj")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ("user", "message")
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.user.username} liked message {self.message.message_id}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
django_admin.site.register((tMUser, tMMessage, tMM2MLike))
|
||||||
|
except AlreadyRegistered:
|
||||||
|
...
|
|
@ -26,7 +26,8 @@ INSTALLED_APPS = [
|
||||||
"django.contrib.auth",
|
"django.contrib.auth",
|
||||||
"django.contrib.contenttypes",
|
"django.contrib.contenttypes",
|
||||||
"django.contrib.sessions",
|
"django.contrib.sessions",
|
||||||
"django.contrib.messages"
|
"django.contrib.messages",
|
||||||
|
"tmessage.apps.DBConfig"
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
@ -69,10 +70,10 @@ DATABASES = {
|
||||||
}
|
}
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = []
|
AUTH_PASSWORD_VALIDATORS = []
|
||||||
|
|
||||||
LANGUAGE_CODE = "en-us"
|
LANGUAGE_CODE = "en-us"
|
||||||
TIME_ZONE = "UTC"
|
TIME_ZONE = "UTC"
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
STATIC_URL = "/static/"
|
||||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
STATIC_ROOT = BASE_DIR / "collected-static"
|
||||||
|
|
24
tmessage/templates/index.html
Normal file
24
tmessage/templates/index.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<h1>Hey there, {{ username }}!</h1>
|
||||||
|
<hr>
|
||||||
|
<p><a href="messages/">Read your messages</a> ({{ new }} new)</p>
|
||||||
|
<p>
|
||||||
|
<div>Your message link: <code class="cursor-pointer" id="message-link">{{ config.services.message.url.pub }}/m/{{ username }}/</code></div>
|
||||||
|
<div><small>(click to copy)</small></div>
|
||||||
|
</p>
|
||||||
|
<hr class="sub">
|
||||||
|
<small>
|
||||||
|
<a href="{{ config.services.auth.url.pub }}/logout/?to=message">Log out</a> -
|
||||||
|
<a href="{{ config.services.auth.url.pub }}{{ login_token }}">Other services</a>
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById("message-link").addEventListener("click", function() {
|
||||||
|
navigator.clipboard.writeText(this.innerText);
|
||||||
|
this.classList.remove("success-anim");
|
||||||
|
setTimeout(() => { this.classList.add("success-anim"); }, 10);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
22
tmessage/templates/message.html
Normal file
22
tmessage/templates/message.html
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<h1>Send a message to {{ username }}</h1>
|
||||||
|
<div id="error">{{ error }}</div>
|
||||||
|
<hr>
|
||||||
|
<form method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<p><textarea name="message" id="message" required maxlength="10000" placeholder="Your message"></textarea></p>
|
||||||
|
{% if self_username %}
|
||||||
|
Logged in as <b>{{ self_username }}</b>
|
||||||
|
<div>
|
||||||
|
<input name="anonymous" id="anonymous" type="checkbox">
|
||||||
|
<label data-fake-checkbox for="anonymous">Don't attach your username</label>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
Not logged in.
|
||||||
|
{% if config.new_users %}<a href="{{ config.services.auth.url.pub }}/signup/">Sign up</a>{% else %}<a href="{{ config.services.auth.url.pub }}/login/">Log in</a>{% endif %}?
|
||||||
|
{% endif %}
|
||||||
|
<p><input type="submit" value="Send message"></p>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
|
@ -6,6 +6,7 @@ from django.core.handlers.wsgi import WSGIRequest
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.template import loader
|
from django.template import loader
|
||||||
|
|
||||||
|
from tmessage.models import tMUser
|
||||||
from tmessage.settings import config
|
from tmessage.settings import config
|
||||||
|
|
||||||
COLORS = ["rosewater", "flamingo", "pink", "mauve", "red", "maroon", "peach", "yellow", "green", "teal", "sky", "sapphire", "blue", "lavender"]
|
COLORS = ["rosewater", "flamingo", "pink", "mauve", "red", "maroon", "peach", "yellow", "green", "teal", "sky", "sapphire", "blue", "lavender"]
|
||||||
|
@ -21,7 +22,8 @@ def render_template(
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
c = {
|
c = {
|
||||||
"accent": random.choice(COLORS),
|
"accent": random.choice(COLORS),
|
||||||
"config": config
|
"config": config,
|
||||||
|
"login_token": f"/auth/?sessionid={request.COOKIES.get('sessionid')}"
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, val in context.items():
|
for key, val in context.items():
|
||||||
|
@ -38,6 +40,28 @@ def render_template(
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
def is_logged_in(request: WSGIRequest) -> dict | None:
|
def get_username(request: WSGIRequest) -> str | None:
|
||||||
resp = requests.get(config["services"]["auth"]["url"]["int"] + f"/api/authenticated/?token={url_escape(config['services']['message']['token'])}&service=message", cookies={**request.COOKIES}).json()
|
resp = requests.get(config["services"]["auth"]["url"]["int"] + f"/api/authenticated/?token={url_escape(config['services']['message']['token'])}&service=message", cookies={**request.COOKIES}).json()
|
||||||
return resp["success"] and resp["auth"]
|
|
||||||
|
if not resp["success"]:
|
||||||
|
raise Exception("Unable to communicate with tAuth")
|
||||||
|
|
||||||
|
return resp["username"]
|
||||||
|
|
||||||
|
def username_exists(username: str) -> bool:
|
||||||
|
resp = requests.get(config["services"]["auth"]["url"]["int"] + f"/api/username/?token={url_escape(config['services']['message']['token'])}&service=message&username={url_escape(username)}").json()
|
||||||
|
|
||||||
|
if not resp["success"]:
|
||||||
|
raise Exception("Unable to communicate with tAuth")
|
||||||
|
|
||||||
|
return resp["exists"]
|
||||||
|
|
||||||
|
def get_user_object(username: str, *, i_promise_this_user_exists: bool=False) -> tMUser:
|
||||||
|
if i_promise_this_user_exists or username_exists(username):
|
||||||
|
try:
|
||||||
|
return tMUser.objects.get(username=username)
|
||||||
|
except tMUser.DoesNotExist:
|
||||||
|
return tMUser.objects.create(username=username)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise tMUser.DoesNotExist(f"tAuth doesn't know who {username} is")
|
||||||
|
|
|
@ -3,7 +3,10 @@ from datetime import datetime
|
||||||
from django.core.handlers.wsgi import WSGIRequest
|
from django.core.handlers.wsgi import WSGIRequest
|
||||||
from django.http import HttpResponse, HttpResponseRedirect
|
from django.http import HttpResponse, HttpResponseRedirect
|
||||||
|
|
||||||
from .helper import is_logged_in, render_template
|
from tmessage.models import tMMessage
|
||||||
|
|
||||||
|
from .helper import (get_user_object, get_username, render_template,
|
||||||
|
username_exists)
|
||||||
|
|
||||||
|
|
||||||
def auth(request: WSGIRequest) -> HttpResponseRedirect:
|
def auth(request: WSGIRequest) -> HttpResponseRedirect:
|
||||||
|
@ -16,8 +19,15 @@ def auth(request: WSGIRequest) -> HttpResponseRedirect:
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
def index(request: WSGIRequest) -> HttpResponse:
|
def index(request: WSGIRequest) -> HttpResponse:
|
||||||
if is_logged_in(request):
|
username = get_username(request)
|
||||||
return dashboard(request)
|
if username:
|
||||||
|
user = get_user_object(username, i_promise_this_user_exists=True)
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
request, "index.html",
|
||||||
|
username=username,
|
||||||
|
new=user.received.filter(response=None).count() # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
request, "noauth/index.html"
|
request, "noauth/index.html"
|
||||||
|
@ -27,7 +37,40 @@ def profile(request: WSGIRequest, username: str) -> HttpResponse:
|
||||||
...
|
...
|
||||||
|
|
||||||
def message(request: WSGIRequest, username: str) -> HttpResponse:
|
def message(request: WSGIRequest, username: str) -> HttpResponse:
|
||||||
...
|
if not username_exists(username):
|
||||||
|
return render_template(
|
||||||
|
request, "404.html"
|
||||||
|
)
|
||||||
|
|
||||||
def dashboard(request: WSGIRequest) -> HttpResponse:
|
error = ""
|
||||||
return render_template(request, "base.html")
|
if request.method == "POST":
|
||||||
|
content = (request.POST.get("message") or "").strip()
|
||||||
|
if len(content) > 10000:
|
||||||
|
error = "Invalid message"
|
||||||
|
|
||||||
|
else:
|
||||||
|
self_username = get_username(request)
|
||||||
|
|
||||||
|
if self_username is None:
|
||||||
|
anonymous = True
|
||||||
|
u_from = None
|
||||||
|
else:
|
||||||
|
anonymous = request.POST.get("anonymous") is not None
|
||||||
|
u_from = get_user_object(self_username, i_promise_this_user_exists=True)
|
||||||
|
|
||||||
|
tMMessage.objects.create(
|
||||||
|
content=content,
|
||||||
|
response=None,
|
||||||
|
anonymous=anonymous,
|
||||||
|
u_to=get_user_object(username, i_promise_this_user_exists=True),
|
||||||
|
u_from=u_from
|
||||||
|
)
|
||||||
|
|
||||||
|
error = "Sent!"
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
request, "message.html",
|
||||||
|
username=username,
|
||||||
|
error=error,
|
||||||
|
self_username=get_username(request)
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in a new issue