Initial commit

This commit is contained in:
trinkey 2024-12-31 19:56:46 -05:00
commit dc3e0bf03e
29 changed files with 991 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
__pycache__/
blog.sqlite3
.vscode/

4
config.py Normal file
View file

@ -0,0 +1,4 @@
tCOMMON_URL_INTERNAL = "http://localhost:8888"
tCOMMON_TOKEN = "Secret tCommon-specific token"
DB_PATH = "blog.sqlite3"

14
manage.py Executable file
View file

@ -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()

2
requirements.txt Normal file
View file

@ -0,0 +1,2 @@
requests
django

0
tblog/__init__.py Normal file
View file

6
tblog/apps.py Normal file
View file

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

6
tblog/asgi.py Normal file
View file

@ -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()

View file

@ -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')},
},
),
]

View file

31
tblog/models.py Normal file
View file

@ -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:
...

79
tblog/settings.py Normal file
View file

@ -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"

213
tblog/static/css/ace.css Normal file
View file

@ -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));
}

View file

@ -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;
}
}

57
tblog/static/js/write.js Normal file
View file

@ -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.";
}

9
tblog/templates/404.html Normal file
View file

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block title %}Page Not Found - {% endblock %}
{% block body %}
<h1>Hmm. That doesn't look right.</h1>
Make sure the URL is correct and try again.
<small>(Error 404 - Page not found)</small>
{% endblock %}

28
tblog/templates/base.html Normal file
View file

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}{% if title %}{{ title }} - {% endif %}{% endblock %}tBlog</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="pronouns" content="she/her">
<link id="favicon" rel="icon" href="{{ config.services.common.url.pub }}/favicon/dark/{{ accent }}/">
{% block head %}{% endblock %}
<link rel="stylesheet" href="{{ config.services.common.url.pub }}/static/css/base.css?v={{ config.version_str }}">
<style>html { --accent: var(--{{ accent }}); }</style>
<script src="{{ config.services.common.url.pub }}/static/js/theme.js?v={{ config.version_str }}"></script>
</head>
<body>
<div id="container">
<noscript>Please enable JavaScript!</noscript>
{% block body %}
Something went horribly wrong! Please contact us so we can fix this.
{% endblock %}
</div>
</body>
</html>

29
tblog/templates/blog.html Normal file
View file

@ -0,0 +1,29 @@
{% extends "base.html" %}
{% block head %}
{% include "snippets/blog-imports.html" %}
<script src="{{ config.services.common.url.pub }}/static/js/base.js?v={{ config.version_str }}"></script>
{% endblock %}
{% block body %}
<h1>{{ blog.title }}</h1>
<div><b>By <a href="/blog/{{ blog.u_by.username }}">{{ blog.u_by.username }}</a></b></div>
<small data-timestamp="{{ blog.timestamp }}"></small>
{% if username == blog.u_by.username %}
<form method="POST">
{% csrf_token %}
<p>
<input type="checkbox" required id="confirm">
<label data-fake-checkbox for="confirm">I understand this is irreversible</label><br>
<input type="submit" value="Delete this post">
</p>
</form>
{% endif %}
<hr>
<div id="blog-container" style="max-width: 1000px; width: 90vw;" class="inline-block left" data-format="{{ blog.text_format }}" data-raw="{{ blog.content }}"><i>Loading...</i></div>
<script>
const blog = document.getElementById("blog-container");
blog.innerHTML = toHTML(blog.dataset.format, blog.dataset.raw);
</script>
{% endblock %}

View file

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% block body %}
<h1>Hey there, {{ username }}!</h1>
<hr>
<p><a href="/blog/{{ username }}/">Your blog posts</a> ({{ blog_count }})</p>
<p>Write a <a href="/create/">new blog post</a></p>
<hr class="sub">
<small>
<a href="{{ config.services.auth.url.pub }}/logout/?to=blog">Log out</a> -
<a href="{{ config.services.auth.url.pub }}{{ login_token }}">Other services</a>
</small>
{% endblock %}

View file

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block body %}
<h1>tBlog</h1>
<div>Write things about... stuff</div>
<p>
<a href="{{ config.services.auth.url.pub }}/login/?to=blog">Log in</a>
</p>
{% endblock %}

View file

@ -0,0 +1,39 @@
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
{% if editor %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.37.1/ace.min.js"></script>
{% endif %}
<script>
function toHTML(format, raw) {
if (format == "markdown") {
return DOMPurify.sanitize(marked.parse(raw));
} else if (format == "html") {
return DOMPurify.sanitize(raw);
} else if (format == "mono") {
return `<pre class="no-margin">${raw.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;")}</pre>`;
} else {
return `<pre class="not-code no-margin">${raw.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;")}</pre>`;
}
}
{% if editor %}
function createEditor(element, content, format, onChange) {
let editor = ace.edit(element);
if (format == "markdown") {
editor.getSession().setMode("ace/mode/markdown");
} else if (format == "html") {
editor.getSession().setMode("ace/mode/html");
}
editor.setTheme("ace/theme/xcode");
editor.getSession().on("change", function() {
onChange(editor.getValue());
});
editor.setValue(content, 1);
return editor;
}
{% endif %}
</script>

40
tblog/templates/user.html Normal file
View file

@ -0,0 +1,40 @@
{% extends "base.html" %}
{% block head %}
<style>
.blog-post {
display: inline-block;
width: calc(90vw - 20px);
max-width: 600px;
border: 1px solid rgb(var(--accent));
border-radius: 10px;
margin-top: 18px;
padding: 20px;
text-align: left;
}
</style>
<script src="{{ config.services.common.url.pub }}/static/js/base.js?v={{ config.version_str }}"></script>
{% endblock %}
{% block body %}
<h1>{{ username }}'s Blog</h1>
<p>
{% if self_username %}
<a href="/">Your Dashboard</a>
{% else %}
Nog logged in.
{% if config.new_users %}<a href="{{ config.services.auth.url.pub }}/signup/?to=blog">Sign up</a>{% else %}<a href="{{ config.services.auth.url.pub }}/login/?to=blog">Log in</a>{% endif %}?
{% endif %}
</p>
{% for post in posts %}
<hr>
<a class="fake-link blog-post" href="/blog/{{ username }}/{{ post.url }}/">
<h2 class="no-margin">{{ post.title }}</h2>
<small data-timestamp="{{ post.timestamp }}"></small>
</a>
{% empty %}
<hr>
<i>No posts yet</i>
{% endfor %}
{% endblock %}

View file

@ -0,0 +1,55 @@
{% extends "base.html" %}
{% block head %}
{% include "snippets/blog-imports.html" with editor=1 %}
<script src="{{ config.services.common.url.pub }}/static/js/base.js?v={{ config.version_str }}"></script>
<link rel="stylesheet" href="/static/css/ace.css?v={{ config.version_str }}">
<link rel="stylesheet" href="/static/css/write.css?v={{ config.version_str }}">
{% endblock %}
{% block body %}
<form method="POST" onsubmit="window.onbeforeunload = null;">
{% csrf_token %}
<div id="editable-container">
<div id="editor-area" class="editor-object" data-editor="ace">
<div id="ace-editor" class="editor-object"></div>
<div id="plain-editor" class="editor-object"><textarea required name="raw" maxlength="500000" placeholder="Write here...">{{ repopulate.content }}</textarea></div>
</div><div class="editor-object"><div id="preview"></div></div>
</div>
{% if error %}<p class="error">{{ error }}</p>{% endif %}
<p>Characters: <b><span id="character-count" data-localize-number="0">0</span>/<span data-localize-number="500000">500000</span></b></p>
<p><b>Configuration:</b></p>
<p>
<input type="checkbox" id="use-plain-editor">
<label data-fake-checkbox for="use-plain-editor">Use alternate editor</label>
</p>
<p>
<label for="format">Format:</label>
<select id="format" name="format">
<option {% if not repopulate.format or repopulate.format == "markdown" %}selected{% endif %} value="markdown">Markdown</option>
<option {% if repopulate.format and repopulate.format == "html" %}selected{% endif %} value="html" >Raw HTML</option>
<option {% if repopulate.format and repopulate.format == "plain" %}selected{% endif %} value="plain" >Plain text (sans-serif)</option>
<option {% if repopulate.format and repopulate.format == "mono" %}selected{% endif %} value="mono" >Plain text (monospace)</option>
</select>
</p>
<p>
<label for="title">Title:</label>
<input maxlength="1000" placeholder="Title..." required name="title" id="title" value="{{ repopulate.title }}">
</p>
<p>
<label for="url">{{ config.services.blog.url.pub }}/blog/{{ username }}/</label
><input maxlength="1000" placeholder="blog-url" required name="url" id="url" value="{{ repopulate.url }}">
</p>
<p><input type="submit" value="Create Blog Post"></p>
<small>(blog posts can't be changed after posting)</small>
</form>
<script src="/static/js/write.js?v={{ config.version_str }}"></script>
{% endblock %}

14
tblog/urls.py Normal file
View file

@ -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/<str:username>/", user),
path("blog/<str:username>/<str:url>/", view_blog),
path("static/", include("tblog.views.static")),
path("django-admin/", admin.site.urls)
]

1
tblog/views/__init__.py Normal file
View file

@ -0,0 +1 @@
from .templates import auth, index, user, view_blog, write # noqa: F401

2
tblog/views/api.py Normal file
View file

@ -0,0 +1,2 @@
from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponse

67
tblog/views/helper.py Normal file
View file

@ -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")

27
tblog/views/static.py Normal file
View file

@ -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"
]]

125
tblog/views/templates.py Normal file
View file

@ -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"
)

6
tblog/wsgi.py Normal file
View file

@ -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()