From dc3e0bf03edda50cfac7ea2a70bf740b24a3da9a Mon Sep 17 00:00:00 2001 From: trinkey Date: Tue, 31 Dec 2024 19:56:46 -0500 Subject: [PATCH] Initial commit --- .gitignore | 3 + config.py | 4 + manage.py | 14 ++ requirements.txt | 2 + tblog/__init__.py | 0 tblog/apps.py | 6 + tblog/asgi.py | 6 + tblog/migrations/0001_initial.py | 36 ++++ tblog/migrations/__init__.py | 0 tblog/models.py | 31 +++ tblog/settings.py | 79 ++++++++ tblog/static/css/ace.css | 213 +++++++++++++++++++++ tblog/static/css/write.css | 76 ++++++++ tblog/static/js/write.js | 57 ++++++ tblog/templates/404.html | 9 + tblog/templates/base.html | 28 +++ tblog/templates/blog.html | 29 +++ tblog/templates/index.html | 13 ++ tblog/templates/noauth/index.html | 9 + tblog/templates/snippets/blog-imports.html | 39 ++++ tblog/templates/user.html | 40 ++++ tblog/templates/write.html | 55 ++++++ tblog/urls.py | 14 ++ tblog/views/__init__.py | 1 + tblog/views/api.py | 2 + tblog/views/helper.py | 67 +++++++ tblog/views/static.py | 27 +++ tblog/views/templates.py | 125 ++++++++++++ tblog/wsgi.py | 6 + 29 files changed, 991 insertions(+) create mode 100644 .gitignore create mode 100644 config.py create mode 100755 manage.py create mode 100644 requirements.txt create mode 100644 tblog/__init__.py create mode 100644 tblog/apps.py create mode 100644 tblog/asgi.py create mode 100644 tblog/migrations/0001_initial.py create mode 100644 tblog/migrations/__init__.py create mode 100644 tblog/models.py create mode 100644 tblog/settings.py create mode 100644 tblog/static/css/ace.css create mode 100644 tblog/static/css/write.css create mode 100644 tblog/static/js/write.js create mode 100644 tblog/templates/404.html create mode 100644 tblog/templates/base.html create mode 100644 tblog/templates/blog.html create mode 100644 tblog/templates/index.html create mode 100644 tblog/templates/noauth/index.html create mode 100644 tblog/templates/snippets/blog-imports.html create mode 100644 tblog/templates/user.html create mode 100644 tblog/templates/write.html create mode 100644 tblog/urls.py create mode 100644 tblog/views/__init__.py create mode 100644 tblog/views/api.py create mode 100644 tblog/views/helper.py create mode 100644 tblog/views/static.py create mode 100644 tblog/views/templates.py create mode 100644 tblog/wsgi.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79ce2ce --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +blog.sqlite3 +.vscode/ diff --git a/config.py b/config.py new file mode 100644 index 0000000..cdb67a7 --- /dev/null +++ b/config.py @@ -0,0 +1,4 @@ +tCOMMON_URL_INTERNAL = "http://localhost:8888" +tCOMMON_TOKEN = "Secret tCommon-specific token" + +DB_PATH = "blog.sqlite3" diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..c650d14 --- /dev/null +++ b/manage.py @@ -0,0 +1,14 @@ +import os +import sys + + +def main(): + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tblog.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError("Couldn't import Django. Are you sure it's installed and available on your PYTHONPATH environment variable? Did you forget to activate a virtual environment?") from exc + execute_from_command_line(sys.argv) + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..863c8ef --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests +django diff --git a/tblog/__init__.py b/tblog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tblog/apps.py b/tblog/apps.py new file mode 100644 index 0000000..f459d3f --- /dev/null +++ b/tblog/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DBConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "tblog" diff --git a/tblog/asgi.py b/tblog/asgi.py new file mode 100644 index 0000000..ede3d36 --- /dev/null +++ b/tblog/asgi.py @@ -0,0 +1,6 @@ +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tblog.settings") +application = get_asgi_application() diff --git a/tblog/migrations/0001_initial.py b/tblog/migrations/0001_initial.py new file mode 100644 index 0000000..dac8623 --- /dev/null +++ b/tblog/migrations/0001_initial.py @@ -0,0 +1,36 @@ +# Generated by Django 5.1.4 on 2024-12-31 23:17 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='tBUser', + fields=[ + ('username', models.CharField(max_length=30, primary_key=True, serialize=False, unique=True)), + ], + ), + migrations.CreateModel( + name='tBPost', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('url', models.CharField(max_length=250)), + ('title', models.TextField(max_length=1000)), + ('content', models.TextField(max_length=500000)), + ('timestamp', models.IntegerField()), + ('text_format', models.CharField(max_length=10)), + ('u_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='posts', to='tblog.tbuser')), + ], + options={ + 'unique_together': {('u_by', 'url')}, + }, + ), + ] diff --git a/tblog/migrations/__init__.py b/tblog/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tblog/models.py b/tblog/models.py new file mode 100644 index 0000000..4f781aa --- /dev/null +++ b/tblog/models.py @@ -0,0 +1,31 @@ +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 tBUser(models.Model): + username = models.CharField(max_length=30, unique=True, primary_key=True) + + if TYPE_CHECKING: + posts = models.Manager["tBPost"] + + def __str__(self) -> str: + return self.username + +class tBPost(models.Model): + u_by = models.ForeignKey(tBUser, on_delete=models.CASCADE, related_name="posts") + url = models.CharField(max_length=250) + title = models.TextField(max_length=1_000) + content = models.TextField(max_length=500_000) + timestamp = models.IntegerField() + text_format = models.CharField(max_length=10) # plain, mono, markdown, html ... (more to come?) + + class Meta: + unique_together = ("u_by", "url") + +try: + django_admin.site.register((tBUser, tBPost)) +except AlreadyRegistered: + ... diff --git a/tblog/settings.py b/tblog/settings.py new file mode 100644 index 0000000..488f443 --- /dev/null +++ b/tblog/settings.py @@ -0,0 +1,79 @@ +from pathlib import Path + +import requests + +from config import DB_PATH, tCOMMON_TOKEN, tCOMMON_URL_INTERNAL + +config = requests.get(f"{tCOMMON_URL_INTERNAL}/api/initialize/", params={ + "token": tCOMMON_TOKEN +}, allow_redirects=False).json() + +if not config["success"]: + raise ImportError("tCommon token doesn't match") + +if not config["services"]["message"]: + raise ImportError("tblog isn't registered in tCommon") + +DEBUG = config["debug"] +SECRET_KEY = config["services"]["message"]["token"] + +BASE_DIR = Path(__file__).resolve().parent.parent + +STATIC_DIR = BASE_DIR / "tblog/static" +ALLOWED_HOSTS = ["*"] +CSRF_TRUSTED_ORIGINS = [config["services"]["message"]["url"]["pub"]] + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "tblog.apps.DBConfig" +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware" +] + +ROOT_URLCONF = "tblog.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [ + BASE_DIR / "tblog/templates" + ], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "tblog.wsgi.application" + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / DB_PATH, + } +} + +AUTH_PASSWORD_VALIDATORS = [] +LANGUAGE_CODE = "en-us" +TIME_ZONE = "UTC" +USE_I18N = True +USE_TZ = True +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/tblog/static/css/ace.css b/tblog/static/css/ace.css new file mode 100644 index 0000000..2e095cf --- /dev/null +++ b/tblog/static/css/ace.css @@ -0,0 +1,213 @@ +.ace_editor { + background-color: rgb(var(--crust)); + color: rgb(var(--text)); +} + +.ace_editor .ace_gutter, +.ace_editor .ace_gutter-cell { + background: rgb(var(--mantle)); + color: rgb(var(--overlay1)); +} + +.ace_editor .ace_print-margin { + background: rgb(var(--mantle)); +} + +.ace_editor .ace_marker-layer .ace_active-line { + background-color: rgb(var(--mantle)); +} + +.ace_editor .ace_marker-layer .highlight-line-error { + background-color: rgba(var(--red), 20%); +} + +.ace_editor .ace_marker-layer .ace_bracket { + border-color: rgb(var(--overlay1)); +} + +.ace_editor .ace_doctype { + color: rgb(var(--mauve)); +} + +.ace_editor .ace_cursor, +.ace_editor .ace_xml.ace_text { + color: rgb(var(--text)); +} + +.ace_editor .ace_heading.ace_1, +.ace_editor .ace_heading.ace_1 + .ace_heading { + color: rgb(var(--red)); +} + +.ace_editor .ace_heading.ace_2, +.ace_editor .ace_heading.ace_2 + .ace_heading { + color: rgb(var(--peach)); +} + +.ace_editor .ace_heading.ace_3, +.ace_editor .ace_heading.ace_3 + .ace_heading { + color: rgb(var(--yellow)); +} + +.ace_editor .ace_heading.ace_4, +.ace_editor .ace_heading.ace_4 + .ace_heading { + color: rgb(var(--green)); +} + +.ace_editor .ace_heading.ace_5, +.ace_editor .ace_heading.ace_5 + .ace_heading { + color: rgb(var(--blue)); +} + +.ace_editor .ace_heading.ace_6, +.ace_editor .ace_heading.ace_6 + .ace_heading { + color: rgb(var(--mauve)); +} + +.ace_editor .ace_list { + color: rgb(var(--text)); +} + +.ace_editor .ace_list.ace_markup { + color: rgb(var(--sky)); +} + +.ace_editor .ace_marker-layer .ace_selection { + background: rgba(var(--accent), 30%); +} + +.ace-tm .ace_marker-layer .ace_selected-word { + background: none; + border: none; +} + +.ace_editor .ace_fold { + background-color: rgb(var(--surface0)); + border-color: rgb(var(--surface1)); +} + +.ace_editor .ace_constant.ace_language, +.ace_editor .ace_keyword, +.ace_editor .ace_meta { + color: rgb(var(--mauve)); +} + +.ace_editor .ace_xml, +.ace_editor .ace_support.ace_class, +.ace_editor .ace_support.ace_type { + color: rgb(var(--yellow)); +} + +.ace_editor .ace_line .ace_identifier:not(:first-of-type), +.ace_editor .ace_entity.ace_name.ace_function, +.ace_editor .ace_constant { + color: rgb(var(--blue)); +} + +.ace_editor .ace_paren, +.ace_editor .ace_variable.ace_language { + color: rgb(var(--red)); +} + +.ace_editor .ace_constant.ace_numeric { + color: rgb(var(--peach)); +} + +.ace_editor .ace_entity.ace_other.ace_attribute-name, +.ace_editor .ace_support.ace_constant, +.ace_editor .ace_support.ace_function { + color: rgb(var(--teal)); +} + +.ace_editor .ace_entity.ace_name.ace_tag, +.ace_editor .ace_variable { + color: rgb(var(--blue)); +} + +.ace_editor .ace_storage { + color: rgb(var(--peach)); +} + +.ace_editor .ace_string { + color: rgb(var(--green)); +} + +.ace_editor .ace_comment { + color: rgb(var(--overlay2)); +} + +.ace_editor .ace_indent-guide { + background-image: url("data:image/svg+xml;utf8,%3Csvg%20width%3D%221%22%20height%3D%222%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20stroke%3D%22rgb%28var%28--surface0%29%29%22%20d%3D%22M0%200v2%22%2F%3E%3C%2Fsvg%3E"); +} + +.ace_mobile-menu { + background-color: rgb(var(--base)); + box-shadow: none; + border-color: rgb(var(--surface0)); + color: rgb(var(--text)); +} + +.ace_tooltip { + background-color: rgb(var(--crust)); + color: rgb(var(--text)); + border-color: rgb(var(--surface0)); +} + +#ace_settingsmenu { + background-color: rgb(var(--base)); + box-shadow: none; + color: rgb(var(--subtext0)); +} + +#ace_settingsmenu .ace_optionsMenuEntry { + transition: 0; +} + +#ace_settingsmenu .ace_optionsMenuEntry:hover { + background-color: rgb(var(--mantle)); +} + +.ace_optionsMenuEntry button, +.ace_optionsMenuEntry button[ace_selected_button="true"], +.ace_optionsMenuEntry button:hover { + background-color: rgb(var(--crust)); + color: rgb(var(--text)); + border-color: rgb(var(--surface0)); +} + +.ace_optionsMenuEntry button[ace_selected_button="true"] { + border-color: rgb(var(--accent)); +} + +.ace_prompt_container { + background-color: rgb(var(--surface0)); +} + +.ace_editor.ace_autocomplete { + border-color: rgb(var(--surface0)); + box-shadow: none; + background-color: rgb(var(--base)); + color: rgb(var(--text)); +} + +.ace_completion-meta { + opacity: 100%; + color: rgb(var(--subtext0)); +} + +.ace_editor.ace_autocomplete .ace_line-hover { + border-color: rgb(var(--accent)); + background-color: rgb(var(--crust)); +} + +.ace_editor.ace_autocomplete .ace_marker-layer .ace_active-line { + background-color: rgb(var(--accent)); +} + +.ace_editor.ace_autocomplete .ace_line.ace_selected { + color: rgb(var(--crust)); +} + +.ace_editor.ace_autocomplete .ace_line.ace_selected .ace_completion-meta { + color: rgb(var(--surface0)); +} diff --git a/tblog/static/css/write.css b/tblog/static/css/write.css new file mode 100644 index 0000000..5a46f98 --- /dev/null +++ b/tblog/static/css/write.css @@ -0,0 +1,76 @@ +.editor-object { + display: inline-block; + width: calc(50vw - 10px); + height: calc(100vh - 120px); +} + +#editable-container { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: center; +} + +#bottom-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + gap: 20px; + margin-top: 10px; +} + +#editor-area:not([data-editor="ace"]) #ace-editor { + display: none; +} + +#editor-area:not([data-editor="plain"]) #plain-editor { + display: none; +} + +#ace-editor { + border-radius: 4px; +} + +#plain-editor textarea { + width: calc(100% - 12px); + height: calc(100% - 8px); + margin: 0; + resize: none; +} + +#preview { + outline: 1px solid rgb(var(--surface0)); + overflow: scroll; + text-align: left; + padding: 10px; + height: calc(100% - 20px); + width: calc(100% - 20px); + max-height: calc(100% - 20px); + border-radius: 4px; +} + +#preview table { + border-collapse: collapse; +} + +#preview table th, +#preview table td { + border: 1px solid rgb(var(--surface2)); + padding: 3px; +} + +#preview table tr:nth-child(even) { + background-color: rgb(var(--mantle)); +} + +@media (max-width: 100vh) { + .editor-object { + width: calc(100vw - 20px); + height: calc(50vh - 60px); + } + + #editable-container { + flex-direction: column; + } +} diff --git a/tblog/static/js/write.js b/tblog/static/js/write.js new file mode 100644 index 0000000..cc949c4 --- /dev/null +++ b/tblog/static/js/write.js @@ -0,0 +1,57 @@ +let text = document.querySelector("#plain-editor textarea").value; +let format = document.getElementById("format").value; + +const modes = { + markdown: "ace/mode/markdown", + html: "ace/mode/html", + plain: "ace/mode/text", + mono: "ace/mode/text" +} + +function updatePreview(raw) { + document.getElementById("preview").innerHTML = toHTML(format, raw); + document.getElementById("character-count").innerText = (new Intl.NumberFormat).format(raw.length); + + if (raw.length > 500_000) { + document.getElementById("character-count").classList.add("error"); + document.getElementById("character-count").classList.remove("warning"); + } else if (raw.length >= 498_000) { + document.getElementById("character-count").classList.remove("error"); + document.getElementById("character-count").classList.add("warning"); + } else { + document.getElementById("character-count").classList.remove("error"); + document.getElementById("character-count").classList.remove("warning"); + } + + text = raw; +} + +let editor = createEditor("ace-editor", text, format, (raw) => { + updatePreview(raw); + document.querySelector("#plain-editor textarea").value = raw; +}); + +document.querySelector("#plain-editor textarea").addEventListener("input", function() { + updatePreview(this.value); +}); + +document.getElementById("use-plain-editor").addEventListener("input", function() { + if (this.checked) { + document.getElementById("editor-area").dataset.editor = "plain"; + } else { + document.getElementById("editor-area").dataset.editor = "ace"; + editor.setValue(text); + } +}); + +document.getElementById("format").addEventListener("change", function() { + format = this.value; + editor.getSession().setMode(modes[format]); + updatePreview(text); +}); + +updatePreview(text); + +window.onbeforeunload = (event) => { + return "Your changes won't be saved."; +} diff --git a/tblog/templates/404.html b/tblog/templates/404.html new file mode 100644 index 0000000..43ca803 --- /dev/null +++ b/tblog/templates/404.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block title %}Page Not Found - {% endblock %} + +{% block body %} +

Hmm. That doesn't look right.

+ Make sure the URL is correct and try again. + (Error 404 - Page not found) +{% endblock %} diff --git a/tblog/templates/base.html b/tblog/templates/base.html new file mode 100644 index 0000000..f92e5b8 --- /dev/null +++ b/tblog/templates/base.html @@ -0,0 +1,28 @@ + + + + + {% block title %}{% if title %}{{ title }} - {% endif %}{% endblock %}tBlog + + + + + + + + {% block head %}{% endblock %} + + + + + + + +
+ + {% block body %} + Something went horribly wrong! Please contact us so we can fix this. + {% endblock %} +
+ + diff --git a/tblog/templates/blog.html b/tblog/templates/blog.html new file mode 100644 index 0000000..c432051 --- /dev/null +++ b/tblog/templates/blog.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} + +{% block head %} + {% include "snippets/blog-imports.html" %} + +{% endblock %} + +{% block body %} +

{{ blog.title }}

+
By {{ blog.u_by.username }}
+ + {% if username == blog.u_by.username %} +
+ {% csrf_token %} +

+ +
+ +

+
+ {% endif %} +
+
Loading...
+ + +{% endblock %} diff --git a/tblog/templates/index.html b/tblog/templates/index.html new file mode 100644 index 0000000..5f8b161 --- /dev/null +++ b/tblog/templates/index.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block body %} +

Hey there, {{ username }}!

+
+

Your blog posts ({{ blog_count }})

+

Write a new blog post

+
+ + Log out - + Other services + +{% endblock %} diff --git a/tblog/templates/noauth/index.html b/tblog/templates/noauth/index.html new file mode 100644 index 0000000..662511e --- /dev/null +++ b/tblog/templates/noauth/index.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block body %} +

tBlog

+
Write things about... stuff
+

+ Log in +

+{% endblock %} diff --git a/tblog/templates/snippets/blog-imports.html b/tblog/templates/snippets/blog-imports.html new file mode 100644 index 0000000..c19f29a --- /dev/null +++ b/tblog/templates/snippets/blog-imports.html @@ -0,0 +1,39 @@ + + +{% if editor %} + +{% endif %} + + diff --git a/tblog/templates/user.html b/tblog/templates/user.html new file mode 100644 index 0000000..cf00a4f --- /dev/null +++ b/tblog/templates/user.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} + +{% block head %} + + +{% endblock %} + +{% block body %} +

{{ username }}'s Blog

+

+ {% if self_username %} + Your Dashboard + {% else %} + Nog logged in. + {% if config.new_users %}Sign up{% else %}Log in{% endif %}? + {% endif %} +

+ + {% for post in posts %} +
+ +

{{ post.title }}

+ +
+ {% empty %} +
+ No posts yet + {% endfor %} +{% endblock %} diff --git a/tblog/templates/write.html b/tblog/templates/write.html new file mode 100644 index 0000000..4e304ae --- /dev/null +++ b/tblog/templates/write.html @@ -0,0 +1,55 @@ +{% extends "base.html" %} + +{% block head %} + {% include "snippets/blog-imports.html" with editor=1 %} + + + +{% endblock %} + +{% block body %} +
+ {% csrf_token %} +
+
+
+
+
+
+ + {% if error %}

{{ error }}

{% endif %} + +

Characters: 0/500000

+

Configuration:

+ +

+ + +

+ +

+ + +

+ +

+ + +

+ +

+ +

+ +

+ (blog posts can't be changed after posting) +
+ + +{% endblock %} diff --git a/tblog/urls.py b/tblog/urls.py new file mode 100644 index 0000000..f5e39b6 --- /dev/null +++ b/tblog/urls.py @@ -0,0 +1,14 @@ +from django.contrib import admin +from django.urls import include, path + +from .views import auth, index, user, view_blog, write + +urlpatterns = [ + path("", index), + path("auth/", auth), + path("create/", write), + path("blog//", user), + path("blog///", view_blog), + path("static/", include("tblog.views.static")), + path("django-admin/", admin.site.urls) +] diff --git a/tblog/views/__init__.py b/tblog/views/__init__.py new file mode 100644 index 0000000..684d3be --- /dev/null +++ b/tblog/views/__init__.py @@ -0,0 +1 @@ +from .templates import auth, index, user, view_blog, write # noqa: F401 diff --git a/tblog/views/api.py b/tblog/views/api.py new file mode 100644 index 0000000..9b5da91 --- /dev/null +++ b/tblog/views/api.py @@ -0,0 +1,2 @@ +from django.core.handlers.wsgi import WSGIRequest +from django.http import HttpResponse diff --git a/tblog/views/helper.py b/tblog/views/helper.py new file mode 100644 index 0000000..838dfb9 --- /dev/null +++ b/tblog/views/helper.py @@ -0,0 +1,67 @@ +import random +from urllib.parse import quote as url_escape + +import requests +from django.core.handlers.wsgi import WSGIRequest +from django.http import HttpResponse +from django.template import loader + +from tblog.models import tBUser +from tblog.settings import config + +COLORS = ["rosewater", "flamingo", "pink", "mauve", "red", "maroon", "peach", "yellow", "green", "teal", "sky", "sapphire", "blue", "lavender"] + +def render_template( + request: WSGIRequest, + template: str, + /, *, + status: int=200, + headers: dict[str, str]={}, + content_type: str="text/html", + **context +) -> HttpResponse: + c = { + "accent": random.choice(COLORS), + "config": config, + "login_token": f"/auth/?sessionid={request.COOKIES.get('session_id')}" + } + + for key, val in context.items(): + c[key] = val + + resp = HttpResponse( + loader.get_template(template).render(c, request), + status=status, + content_type=content_type + ) + + for key, val in headers.items(): + resp[key] = val + + return resp + +def get_username(request: WSGIRequest) -> str | None: + resp = requests.get(config["services"]["auth"]["url"]["int"] + f"/api/authenticated/?token={url_escape(config['services']['blog']['token'])}&service=blog", cookies={**request.COOKIES}).json() + + 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']['blog']['token'])}&service=blog&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) -> tBUser: + if i_promise_this_user_exists or username_exists(username): + try: + return tBUser.objects.get(username=username) + except tBUser.DoesNotExist: + return tBUser.objects.create(username=username) + + else: + raise tBUser.DoesNotExist(f"tAuth doesn't know who {username} is") diff --git a/tblog/views/static.py b/tblog/views/static.py new file mode 100644 index 0000000..c5c4be2 --- /dev/null +++ b/tblog/views/static.py @@ -0,0 +1,27 @@ +from django.http import HttpResponse +from django.urls import path +from django.views.decorators.cache import cache_control + +from tblog.settings import STATIC_DIR + + +def get_static_serve(path: str, content_type: str): + def x(request): + return HttpResponse( + open(STATIC_DIR / path, "rb").read(), + content_type=content_type + ) + + x.__name__ = path + return x + +file_associations = { + "css": "text/css", + "js": "text/javascript" +} + +urlpatterns = [path(i, cache_control(**{"max-age": 60 * 60 * 24 * 30})(get_static_serve(i, file_associations[i.split(".")[-1]]))) for i in [ + "css/ace.css", + "css/write.css", + "js/write.js" +]] diff --git a/tblog/views/templates.py b/tblog/views/templates.py new file mode 100644 index 0000000..b0d42cf --- /dev/null +++ b/tblog/views/templates.py @@ -0,0 +1,125 @@ +import math +import time +from urllib.parse import quote as escape_url + +from django.core.handlers.wsgi import WSGIRequest +from django.db import IntegrityError +from django.http import HttpResponse, HttpResponseRedirect + +from tblog.models import tBPost + +from .helper import get_user_object, get_username, render_template + + +def auth(request: WSGIRequest) -> HttpResponseRedirect: + resp = HttpResponseRedirect("/") + if "remove" in request.GET: + resp.set_cookie("session_id", "", max_age=0) + else: + resp.set_cookie("session_id", request.GET.get("sessionid") or "") + + return resp + +def index(request: WSGIRequest) -> HttpResponse: + 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, + blog_count=user.posts.count() # type: ignore + ) + + return render_template( + request, "noauth/index.html" + ) + +def write(request: WSGIRequest) -> HttpResponse: + username = get_username(request) + if username: + repopulate = {} + error = None + + if request.method == "POST": + url = (request.POST.get("url") or "").strip().replace(" ", "-").lower() + title = (request.POST.get("title") or "").strip() + content = (request.POST.get("raw") or "").strip() + fmt = request.POST.get("format") + + repopulate = { + "url": url, + "title": title, + "content": content, + "format": fmt + } + + if not (url and title and content) or fmt not in ["plain", "mono", "markdown", "html"] or len(url) > 250 or len(title) > 1_000 or len(content) > 500_000: + error = "Invalid input(s)" + + else: + try: + tBPost.objects.create( + u_by=get_user_object(username, i_promise_this_user_exists=True), + url=request.POST.get("url"), + title=request.POST.get("title"), + content=request.POST.get("raw"), + timestamp=math.floor(time.time()), + text_format=request.POST.get("format") + ) + except IntegrityError: + error = f"Url '{url}' already in use" + else: + return HttpResponseRedirect(f"/blog/{username}/{escape_url(url)}/") + + return render_template( + request, "write.html", + title="Writing", + username=username, + error=error, + repopulate=repopulate + ) + + return render_template( + request, "noauth/index.html" + ) + +def view_blog(request: WSGIRequest, username: str, url: str) -> HttpResponse: + try: + blog = tBPost.objects.get( + u_by=get_user_object(username), + url=url + ) + except tBPost.DoesNotExist: + return render_template( + request, "404.html" + ) + + self_username = get_username(request) + + if request.method == "POST" and username == self_username: + blog.delete() + return HttpResponseRedirect("/") + + return render_template( + request, "blog.html", + blog=blog, + title=blog.title + " by " + blog.u_by.username, + username=self_username + ) + +def user(request: WSGIRequest, username: str) -> HttpResponse: + user = get_user_object(username) + + if user: + return render_template( + request, "user.html", + username=user.username, + self_username=get_username(request), + posts=user.posts.all() # type: ignore + ) + + return render_template( + request, "404.html" + ) diff --git a/tblog/wsgi.py b/tblog/wsgi.py new file mode 100644 index 0000000..541a6e1 --- /dev/null +++ b/tblog/wsgi.py @@ -0,0 +1,6 @@ +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tblog.settings") +application = get_wsgi_application()