diff --git a/tmessage/apps.py b/tmessage/apps.py new file mode 100644 index 0000000..b69a924 --- /dev/null +++ b/tmessage/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DBConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "tmessage" diff --git a/tmessage/migrations/0001_initial.py b/tmessage/migrations/0001_initial.py new file mode 100644 index 0000000..539b51a --- /dev/null +++ b/tmessage/migrations/0001_initial.py @@ -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'), + ), + ] diff --git a/tmessage/migrations/__init__.py b/tmessage/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tmessage/models.py b/tmessage/models.py new file mode 100644 index 0000000..682be24 --- /dev/null +++ b/tmessage/models.py @@ -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: + ... diff --git a/tmessage/settings.py b/tmessage/settings.py index a3cf9f4..7132d3f 100644 --- a/tmessage/settings.py +++ b/tmessage/settings.py @@ -26,7 +26,8 @@ INSTALLED_APPS = [ "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", - "django.contrib.messages" + "django.contrib.messages", + "tmessage.apps.DBConfig" ] MIDDLEWARE = [ @@ -69,10 +70,10 @@ DATABASES = { } AUTH_PASSWORD_VALIDATORS = [] - LANGUAGE_CODE = "en-us" TIME_ZONE = "UTC" USE_I18N = True USE_TZ = True - +STATIC_URL = "/static/" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" +STATIC_ROOT = BASE_DIR / "collected-static" diff --git a/tmessage/templates/index.html b/tmessage/templates/index.html new file mode 100644 index 0000000..2326b0c --- /dev/null +++ b/tmessage/templates/index.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block body %} +

Hey there, {{ username }}!

+
+

Read your messages ({{ new }} new)

+

+

Your message link: {{ config.services.message.url.pub }}/m/{{ username }}/
+
(click to copy)
+

+
+ + Log out - + Other services + + + +{% endblock %} diff --git a/tmessage/templates/message.html b/tmessage/templates/message.html new file mode 100644 index 0000000..8412010 --- /dev/null +++ b/tmessage/templates/message.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} + +{% block body %} +

Send a message to {{ username }}

+
{{ error }}
+
+
+ {% csrf_token %} +

+ {% if self_username %} + Logged in as {{ self_username }} +
+ + +
+ {% else %} + Not logged in. + {% if config.new_users %}Sign up{% else %}Log in{% endif %}? + {% endif %} +

+
+{% endblock %} diff --git a/tmessage/views/helper.py b/tmessage/views/helper.py index 85d2bcb..4cf732d 100644 --- a/tmessage/views/helper.py +++ b/tmessage/views/helper.py @@ -6,6 +6,7 @@ from django.core.handlers.wsgi import WSGIRequest from django.http import HttpResponse from django.template import loader +from tmessage.models import tMUser from tmessage.settings import config COLORS = ["rosewater", "flamingo", "pink", "mauve", "red", "maroon", "peach", "yellow", "green", "teal", "sky", "sapphire", "blue", "lavender"] @@ -21,7 +22,8 @@ def render_template( ) -> HttpResponse: c = { "accent": random.choice(COLORS), - "config": config + "config": config, + "login_token": f"/auth/?sessionid={request.COOKIES.get('sessionid')}" } for key, val in context.items(): @@ -38,6 +40,28 @@ def render_template( 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() - 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") diff --git a/tmessage/views/templates.py b/tmessage/views/templates.py index 1733ef5..c3b6604 100644 --- a/tmessage/views/templates.py +++ b/tmessage/views/templates.py @@ -3,7 +3,10 @@ from datetime import datetime from django.core.handlers.wsgi import WSGIRequest 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: @@ -16,8 +19,15 @@ def auth(request: WSGIRequest) -> HttpResponseRedirect: return resp def index(request: WSGIRequest) -> HttpResponse: - if is_logged_in(request): - return dashboard(request) + username = get_username(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( request, "noauth/index.html" @@ -27,7 +37,40 @@ def profile(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: - return render_template(request, "base.html") + error = "" + 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) + )