Merge branch 'master' into server-side-markdown

This commit is contained in:
MatthiasSaihttam 2021-10-13 23:46:40 -04:00
commit 1411762ca1
21 changed files with 1294 additions and 239 deletions

4
.gitignore vendored
View File

@ -2,6 +2,10 @@
/.idea
/db.sqlite3
/static
/media
/log
stale_outputs_checked
__pycache__
Thoughts.iml
.env
*.pages

44
jetforce_app.py Normal file
View File

@ -0,0 +1,44 @@
import os
os.environ["DJANGO_SETTINGS_MODULE"] = "whispermaphone.settings"
import django
from django.template.loader import render_to_string
from django.utils import timezone
django.setup()
from main.models import Thought
from jetforce import GeminiServer, JetforceApplication, Response, Status
from decouple import config
app = JetforceApplication()
@app.route("", strict_trailing_slash=False)
def index(request):
thoughts = Thought.objects.order_by("-posted")
rendered_text = render_to_string("whispermaphone/index.gmi", {
"thoughts": thoughts,
})
return Response(Status.SUCCESS, "text/gemini", rendered_text)
@app.route("/about", strict_trailing_slash=False)
def about(request):
return Response(Status.SUCCESS, "text/gemini", render_to_string("whispermaphone/about.gmi"))
if __name__ == "__main__":
server = GeminiServer(
app=app,
host="0.0.0.0",
hostname="localhost",
certfile=config("CERTFILE", default=None),
keyfile=config("KEYFILE", default=None),
port=1973
)
server.run()

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-04-20 16:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0003_thought_uuid'),
]
operations = [
migrations.AddField(
model_name='thought',
name='media',
field=models.FileField(blank=True, upload_to=''),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2 on 2021-05-01 20:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0004_thought_media'),
]
operations = [
migrations.AddField(
model_name='thought',
name='media_alt',
field=models.TextField(blank=True),
),
]

View File

@ -1,6 +1,13 @@
import os
import uuid
import magic
from django.db import models
from django import forms
from django.utils import timezone
from django.utils.text import normalize_newlines
from django.core.exceptions import ValidationError
class Thought(models.Model):
@ -9,3 +16,86 @@ class Thought(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, editable=False)
posted = models.DateTimeField(auto_now_add=True)
timezone_offset = models.IntegerField() # The number of minutes behind UTC we were when this was posted
media = models.FileField(upload_to="", blank=True) # A single image, video, or sound clip per post
media_alt = models.TextField(blank=True) # An optional transcription of the Thought's media
def get_media_type(self):
if not self.media:
return ""
else:
return os.path.splitext(self.media.path)[1][1:]
def get_timezone(self):
return timezone.get_fixed_timezone(-self.timezone_offset)
def get_offset_hours(self):
offset_hours = -self.timezone_offset / 60
# Convert 4.0 to 4
if offset_hours == int(offset_hours):
offset_hours = int(offset_hours)
if offset_hours > 0:
offset_hours = "+" + str(offset_hours)
return str(offset_hours)
# Honestly I'm so sick of this problem that writing out a comment here to explain why it is necessary is beyond me.
# I'm calling this CharField and not MySpecialLineNormalizingCharField
# because I think this should be the default behavior and I don't understand why it's not.
class CharField(forms.CharField):
def to_python(self, value):
# I am assuming that value is always a string and normalize_newlines isn't going to error on any string input
return super().to_python(normalize_newlines(value))
# A dict mapping allowed mime types to file extensions
ALLOWED_MEDIA_TYPES = {
"image/png": "png",
"image/jpeg": "jpeg",
"audio/x-m4a": "m4a",
"audio/mp3": "mp3",
"video/mp4": "mp4",
"video/quicktime": "mov",
}
class ThoughtForm(forms.ModelForm):
def clean_media(self):
f = self.cleaned_data["media"]
if not f:
return f
# 16 MB file size limit.
if f.size > 2**24:
raise ValidationError("Content too long", code="max_size")
chunk = next(f.chunks(chunk_size=2048))
media_type = magic.from_buffer(chunk, mime="True")
if media_type not in ALLOWED_MEDIA_TYPES:
raise ValidationError("Invalid media type", code="invalid")
return self.cleaned_data["media"]
class Meta:
model = Thought
exclude = ["posted"]
widgets = {
"text": forms.Textarea(attrs={"class": "thought", "rows": 1, "placeholder": "What are you thinking?"}),
"extended_text": forms.Textarea(attrs={"class": "thought", "rows": 1, "placeholder": "Anything else?"}),
"timezone_offset": forms.HiddenInput,
"media_alt": forms.Textarea(attrs={"placeholder": "Media alternate text, please", "rows": 1})
}
field_classes = {
"text": CharField,
"extended_text": CharField,
"media_alt": CharField,
}
error_messages = {
"text": {
# It's debatable whether this is actually a "nice" error message
# but I've grown fond of it
"required": "Need some text.",
"max_length": "Text must be at most 140 characters."
},
}

101
main/pagination.py Normal file
View File

@ -0,0 +1,101 @@
import datetime
from django.utils.text import slugify
from .models import Thought
class Page:
def __init__(self, formatted_name):
self.formatted_name = formatted_name
self.slug = slugify(formatted_name)
def get_all_entries(self):
pass
# Okay we're going to use some wack seasons here hold on
# Let's go meteorological seasons
# 1 - jan -> 1, 0
# 2 - feb -> 2, 0
# 3 - mar -> 3, 1, spring
# 4 - apl -> 4, 1
# 5 - may -> 5, 1
# 6 - jun -> 6, 2, summer
# 7 - jul -> 7, 2
# 8 - aug -> 8, 2
# 9 - sep -> 9, 3, fall
# 10 - oct -> 10, 3
# 11 - nov -> 11, 3
# 12 - dec -> 0, 0, winter
def season_for_date(date):
return (date.month % 12) // 3
def season_year_for_date(date):
return season_for_date(date), date.year - (1 if date.month in [1, 2] else 0)
def formatted_name_for_season_year(current_season, current_year):
return ["Winter", "Spring", "Summer", "Fall"][current_season] + " " + str(current_year)
def get_page_slug(thought):
return slugify(formatted_name_for_season_year(*season_year_for_date(thought.posted)))
class SeasonPage(Page):
def __init__(self, current_season, current_year):
super().__init__(formatted_name_for_season_year(current_season, current_year))
self.first_day_of_season = datetime.date(
current_year,
12 if current_season == 0 else current_season * 3,
1
)
# If the current season is winter, then the next season starts on the next year
# This is actually the first day of the next season but that's hard to type out and it's 2am
self.last_day_of_season = datetime.date(
current_year + (1 if current_season == 0 else 0),
12 if current_season == 3 else (current_season + 1) * 3,
1
)
def get_all_entries(self):
return Thought.objects.order_by("-posted").filter(
posted__gte=self.first_day_of_season, # First month of this season
posted__lt=self.last_day_of_season # First month of next season
)
# Need to loop over all thoughts? and yield a new Page for each season
# Assume that if you're using this generator, you have at least 1 thought each
# season between the first and last
# We don't take into account timezone here. Because local time isn't monotonic increasing,
# it's ill-defined how to split up a list of items ordered by server time. This is annoying
# But the alternative is to call get_season for every thought and sort them into buckets
def season_pages():
ordered_thoughts = Thought.objects.order_by("posted")
first_thought = ordered_thoughts.first()
last_thought = ordered_thoughts.last()
current_year = first_thought.posted.year
current_season = season_for_date(first_thought.posted)
while current_year < last_thought.posted.year or current_season != season_for_date(last_thought.posted):
yield SeasonPage(current_season, current_year)
if current_season == 0:
current_year += 1
current_season += 1
current_season = current_season % 4
yield SeasonPage(current_season, current_year)
def get_all_pages():
pages = list(season_pages())
pages.reverse()
return pages
# Where a Page has:
# .slug
# .formatted_name
# .get_all_entries
# return [Pages]

View File

@ -1,8 +1,3 @@
.main-content {
padding: 20px 30px;
font-size: 16px;
}
#password {
margin: 15px 0;
}

View File

@ -9,9 +9,11 @@
:root {
/*Background and text colors from Atom One Dark*/
--background-color: #23252E;
/*C9CED5*/
--text-color: #C5CBD3;
/*A lot of accent colors work with this one*/
--accent-color: #78916e;
/*Old, og: #78916e; AA compliant: #7F9875; AAA compliant: #96BD86 */
--accent-color: #7F9875;
/* Pink Official Green Forest Lime Dark theme-green Yellow Muted yellow */
/* #D787FF #5FAF87, #286546 #75a98a #78916e, #968540 #9a9168 */
}
@ -44,12 +46,12 @@ body {
}
.main-wrap {
max-width: 1000px;
max-width: 900px;
margin: 20px 30px;
}
.thought {
margin: 20px 30px;
line-height: 1.5em;
margin: 20px 0;
}
.thought.highlighted .main, .thought.highlighted .extended-text {
@ -78,7 +80,7 @@ input[type="submit"]:hover, input[type="submit"]:focus {
/* Textbox/textarea styles */
textarea, input[type="text"], input[type="password"] {
padding: 1px 5px;
padding: 0 5px;
outline: none;
resize: none;
background: none;
@ -89,20 +91,23 @@ textarea, input[type="text"], input[type="password"] {
border-bottom: 1px solid var(--accent-color);
border-radius: 0;
overflow-x: hidden;
display: block;
line-height: 1.5;
}
textarea::placeholder, input[type="text"]::placeholder {
color: var(--text-color);
opacity: 50%;
}
.text {
.text, .thought-end {
max-width: 100%;
line-height: 1.5em;
}
.main-text, .main-text p:last-child {
display: inline; /*inline so that the Show More is on the same line*/
}
.text pre {
display: inline-block; /*Code blocks need to be inline-block*/
}
a {
text-decoration: none;
}
@ -110,7 +115,7 @@ a {
.text a {
color: var(--accent-color);
}
.text p, .text pre {
.text p {
padding: 0;
margin: 0;
overflow-wrap: break-word;
@ -118,24 +123,43 @@ a {
.text > :not(:last-child) {
margin-bottom: 1em;
}
.text pre {
background: black;
/*Inline code*/
.text p > code {
border-radius: 3px;
padding: 3px;
padding: 1px 5px;
background: rgba(0, 0, 0, 0.3);
}
/*Code blocks*/
.text pre {
margin: 0;
overflow-x: auto;
display: block;
background: rgba(0, 0, 0, 0.3);
border-radius: 3px;
padding: 3px 7px;
}
.text pre > code {
/* block forces the text to the whole width, but since inline-block is buggy in Safari, this is our best option */
display: block;
}
.text img {
max-width: 100%;
.extended-media {
max-width: calc(100% - 20px);
max-height: 500px;
margin: 10px;
display: block;
}
.extended-text {
margin-top: 1em;
display: inline-block; /*Needs to be inline-block, so that margin on it works*/
display: block; /*Needs to be inline-block or block, so that margin on it works*/
}
.thought-end {
overflow: auto;
font-size: 14px;
color: var(--accent-color);
}
.thought hr {
@ -144,33 +168,87 @@ a {
border-bottom: 1px var(--accent-color) solid;
}
.timestamp {
.timestamp, .permalink {
float: right;
font-size: 14px;
color: var(--accent-color);
}
header h1 {
.permalink {
margin-right: 6px;
color: var(--text-color)
}
.permalink .button, .permalink .button:visited {
color: var(--accent-color);
margin-right: 4px;
}
.permalink .button:hover {
text-decoration: underline;
}
.permalink::after {
content: "•"
}
/* Navbar styles */
#main-nav {
padding: 4% 30px 15px;
font-size: 3em;
font-size: 3rem !important;
font-weight: normal;
margin: 0;
overflow: auto;
white-space: nowrap;
}
header .text {
#main-nav .text {
color: var(--text-color);
font-family: Georgia, Libre Baskerville, serif;
}
#main-nav h1.text {
border-bottom: 4px solid var(--accent-color);
}
/*footer {
position: fixed;
bottom: 0;
background: var(--accent-color);
color: var(--background-color);
width: 100%;
padding: 10px;
height: 20px;
}*/
#main-nav h1 {
/* Disable browser styles for h1 */
font-size: inherit;
font-weight: inherit;
display: inline;
}
.history-nav.top {
margin-top: 30px;
margin-bottom: 40px;
margin-left: 20px;
}
.history-nav.bottom {
margin-top: 50px;
margin-bottom: 30px;
margin-left: 50px;
}
.history-nav ul {
margin: 0;
padding: 0;
list-style: none;
}
.history-nav li {
display: inline;
}
.history-nav li:not(:first-child)::before {
content: "•";
}
.history-nav li:not(:first-child) > * {
margin-left: 10px;
}
.history-nav li:not(:last-child) > * {
margin-right: 5px;
}
.history-nav .current-page {
font-weight: bold;
}
.history-nav a, .history-nav a:visited {
color: var(--accent-color);
}
.history-nav a:hover {
text-decoration: underline;
}

View File

@ -1,22 +1,35 @@
textarea.thought {
display: block;
width: calc(100% - 60px);
}
textarea.thought::placeholder {
color: var(--text-color);
opacity: 50%;
}
#post-button {
border-radius: 0;
background: none;
outline: none;
border: none;
margin: 30px;
padding: 0;
font-size: 32px;
color: var(--text-color);
cursor: pointer;
font-family: Georgia, Libre Baskerville, serif;
}
.media-wrapper {
padding: 8px;
margin: 20px 0;
border-left: 2px solid var(--accent-color);
}
.media-wrapper [name="media"] {
/* Tries to make the media upload button have a cursor. Doesn't work but I blame WebKit */
cursor: pointer;
}
.media-wrapper textarea {
margin-top: 20px;
}
.error {
font-style: italic;
color: var(--accent-color);
}

View File

@ -0,0 +1,165 @@
=> / Thoughts
# About
## Welcome!
This is a website that I created to document my thoughts. It addresses many of my gripes with similar platforms, like Twitter, Tumblr, Micro.blog, and Blogger, although it takes inspiration from all of these.
Im Matthias. These are mostly my thoughts. Raw, un-distilled, thoughts. I do filter any thoughts that would be offensive or NSFW. Generally, with the exception of censored profanity, complex ideas, and links to other sites that are less principled, this site should be safe for your child to read.
This website is my own creation. It follows my own rules. It is dedicated to 3 things.
First, of course, is thoughts. The mere act of creating ideas is good, and those ideas should be shared.
Second, is words. Words still have power.
Third, is design. Design is simplicity and functionality and form and appearance.
Welcome to a website, welcome to my website. Or someones website, at least. Websites are strange like that, you can never really tell who they belong to. Maybe this website belongs to you, for the amount of time that youre here and you have it. Maybe it belongs to to no one, or all of us.
I am a very small amount of everyone. Infinitesimally small pieces of me belong to everyone, and I suppose that includes you, so I owe you at least something.
This is a website. It is not an academic paper, thesis, or dissertation. I do guarantee that all of the thoughts here are genuine—that is, I actually had them. However, I make no guarantees regarding their quality. Many of the thoughts here were had, posted, and then immediately discarded as clearly worthless. This is not disclaimed, I trust that you too can discern which of these thoughts are core values of my life and which are the merest passing fancy.
This is a website.
This is a breeding ground for ideas. The only criteria for something to be posted here is that I want it to be.
Here I work on answering the question. Everything posted here is an attempt to capture the liquid of life. Some moments are rocks, forcing you to hold on to them, weighing you down. Others are liquid, slipping out of your hands despite your best attempts. I suspect we are all frantically searching for a way to hold on to the things that are rapidly slipping away from our grasp.
This is a blog. In much the sense that Tumblr is a blogging platform, so too is this website.
If you realize that youre dying, they only thing to do is turn back toward your childhood. Oh so many people dont realize, or realize too late. I saw a man getting a transfusion of blood. Though it was necessary to keep him alive, the pain of having it injected into him incapacitated him.
This is WhisperMaPhone. Its a tool I created to share my thoughts. It is currently closed source, as it is still in active development. However, if youre interested in setting up a page similar to this, let me know.
“Our brains are dulled by the incurable mania of wanting to make the unknown known, classifiable. The desire for analysis wins out over the sentiments.”
This is the void. Theres no way for you to contact me on here. There are no guarantees when youre shouting into the void. But the void doesnt normally shout back. No one ever said the void wasnt listening. Youre listening. This is my void.
I use single quotes to indicate that Im paraphrasing, and double-quotes to indicate a direct quotation from someone else. Sometimes I will not cite my source, because the origin is less important that the words. Sometimes I dont remember the source.
The portion of this website which is hidden from prying eyes is written in Python, with Django. Although it is visible to a couple of eyes on account of their not-prying. Everyone looks but only few find. Still, you cannot find if you do not look, although whether you are more or less prepared to understand what you find is a different point entirely.
“In this day and age logical methods are applicable only to solving problems of secondary interest”
It has recently come to my attention that there is some confusion regarding the purpose of this website, or even, what it is. This page should answer both of those questions.
I can only pray you understand what I give you, but likely you will not. They never did understand the gifts they were given.
This is a surrender and a revel. This is an experiment. This is a rejection of social media and an embrace of it.
This is a circus inside a clown factory. This is a bomb.
```
All thoughts are provided with a timestamp in the timezone of the poster, to ensure proper contextualization.
Contextualization is of utmost importance.
```
```
This is different.
This is the same.
```
Im Matthias. (most of the time)
This is a Russian bot attempting to manipulate the election process. Posts here represent a subset of the result of training a GPT-3 text generation algorithm on my brain.
This is a result of not even knowing what alcohol tastes like. This is fire from the past. This is my first most prized possession. This is Researcher ████████.
```
All thoughts are provided completely free of context.
```
Gödel has disproved logic. The imperfections of the human mind are inescapable. There are perhaps corners of that 2D plane of logic that remain unexplored, but by logic alone that plane will never be fully explored, so those corners do not interest us. No, let us think in 3D.
This is an ebenezer to the Lord and the things He has done for me.
Im hungry.
Hey there, its Nate.
This website is an outlet for the many intrusive thoughts that I have, that I cannot speak out loud. These thoughts dont have meaning, they arent important. Even if it were socially acceptable for me to share them at random times, even if people were interested, theyre still not of any particular value. On the other hand, here they can be themselves. They can exist free from judgement. You, and me, come here to examine the curious thoughts of a curious boy. Here my thoughts can thrive.
This is the real track four.
This is a story about a boy.
People dont like things that are different.
The world is confusing.
This is a cult.
This is an explanation. This page answers any questions you may have about this website.
Welcome to my website! This is a micro-blogging platform for me to share my thoughts. This website breaks normal blogging and micro-blogging paradigms. I have 140 character limit on the first line, for real quick thoughts, and then a Show More button, and then unlimited characters in a second field. Ill sometimes use hundreds of characters, writing mini-essays on topics that catch my interest. This would be very impractical to do on Twitter. Although this websites solves another gripe with Twitter, the concept of “liking” Tweets. I dont ask for your feedback here. This frees me to post what Im inspired to, and allows you to experience what I post without worrying about if you should “like” it.
This is how I learned to stop worrying. This is a flea in which our two bloods mingled be.
All thoughts posted here are those of my employer.
This is the end and a new beginning. Ive stopped using Discord, Twitter, Instagram, and other social media since creating this website.
I open vim to write. I dont have a spell checker, so I struggle, catching my own errors by sight, recognizeing that the word looks wrong, but not knowing how to fix it. I shrug. Ill go over it with a spell-check at some point. But I might not. I might impuse-publish this page, one night, when Im feeling particularly lonely. Then the message that I dread (“you spelled recognizeing wrong”) might come as a welcome social interaction, and an opprotunity to improve my website. Rather than an annoyance.
This is not a test. This is considered harmful. This is mostly harmless. Fear the daemons you have created at JOKEEFUNNY.COM.
This is art. This is poetry.
This is a revolution of the proletariat. We will not sit quietly.
This is sitting loudly. This website is inaction. It exists only to let me complain.
I find it helps to imagine all of my posts here as the murmurings of someone talking in their sleep.
I bought a book earlier today. I am writing instead of reading it. The irony. I created this site to give me a space to write, and yet, often I post here merely things that I have read. The irony.
Thank you for coming to my website, and reading some of my thoughts. It really does mean a lot to me that youre interested enough to read them. I pride myself on my thoughts, and if people werent interested in them
Im tired.
Im tired of this. I want to do something interesting, something different. I want to do something different every day. I want to feel something.
This is an attempt to feel something.
This is a safe space for me to vent.
This is the end of my sanity, of normalcy, of grammaer and ru;es and smaness.
This is poetry, this is all poetry, and if you havent figured that out by now, then Im probably not that interested in listening to you, so you shouldnt feel any obligation to listen to me. This is the poetry of a broken generation. Of a generation that is as broken as any that came before it, and yet feels the pain of brokenness just as strongly as any others.
Pray with me. “The Lord is close to the brokenhearted and saves those who are crushed in spirit.” The LORD is close to me. The LORD is close to me. The LORD is close to me. The LORD is close to me. The LORD is close to me. The LORD is close to me. The LORD is close to me. The LORD is close to me. The LORD is close to me.
This website is a prayer.
This website is.
This website is an exercise in mental stimulation. This website stores Thoughts that I have, but it also encourage me to have thoughts in the first place.
This website is a part of who I am
This is a representation of my mind.
This is an influence on me
Soli Deo gloria
Thoughts posted here seem to fall into one of a few discrete categories. Firstly, analysises of topics, things that would be too long for Twitter, or else invite debate. Second, updates on my life and what Im doing and feeling. Third, expressions, of feelings. All try to be informative, humours, and rehtorically enjoyable.
Poetry is text written to convey emotion. Text is written to convey emotion. This is text.
This is not text.
This is a fan page for the Breckenridge Jazz Hands.
Im always hungry.
Please, dont try to share anything here in other places. I just dont imagine it going well. Although sharing this website as a whole I imagine would be beneficial.
I fear this is a creative writing exercise.
This is the end.
This is my last ditch effort. My guilty pleasure. My bad habit.
Good Night.
This is my late-night indulgence. Be sure to like and subscribe for more.
This is the result of not getting enough sleep.
This what Im thinking.
These are my Thoughts.

View File

@ -0,0 +1,354 @@
{% extends "whispermaphone/page.html" %}
{% block title %}About{% endblock %}
{% block navigation %}
<a href="/" class="text" style="border: none">Thoughts</a>
<h1 class="text">About</h1>{% if authenticated %}
<a href="/post" class="text" style="border: none">Post</a>
{% endif %}
{% endblock %}
{% block main %}
<h1>Welcome!</h1>
<p class="text">
This is a website that I created to document my thoughts. It addresses many of my gripes with similar platforms, like Twitter, Tumblr, Micro.blog, and Blogger, although it takes inspiration from all of these.
</p>
<p class="text">
Im Matthias. These are mostly my thoughts. Raw, un-distilled, thoughts. I do filter any thoughts that would be offensive or NSFW. Generally, with the exception of censored profanity, complex ideas, and links to other sites that are less principled, this site should be safe for your child to read.
</p>
<p class="text">
This website is my own creation. It follows my own rules. It is dedicated to 3 things.
</p>
<p class="text">
First, of course, is thoughts. The mere act of creating ideas is good, and those ideas should be shared.<br>
Second, is words. Words still have power.<br>
Third, is design. Design is simplicity and functionality and form and appearance.
</p>
<p class="text">
Welcome to a website, welcome to my website. Or someones website, at least.
Websites are strange like that, you can never really tell who they belong to. Maybe this website belongs to you,
for the amount of time that youre here and you have it. Maybe it belongs to to no one, or all of us.
</p>
<p class="text">
I am a very small amount of everyone. Infinitesimally small pieces of me belong to everyone,
and I suppose that includes you, so I owe you at least something.
</p>
<p class="text">
This is a website. It is not an academic paper, thesis, or dissertation.
I do guarantee that all of the thoughts here are genuine&mdash;that is,
I actually had them.
However, I make no guarantees regarding their quality.
Many of the thoughts here were had, posted, and then immediately discarded as clearly worthless.
This is not disclaimed, I trust that you too can discern which of these thoughts are core values of my life
and which are the merest passing fancy.
</p>
<p class="text">
This is a website.
</p>
<p class="text">
This is a breeding ground for ideas. The only criteria for something to be posted here is that I want it to be.
</p>
<p class="text">
Here I work on answering the question. Everything posted here is an attempt to capture the liquid of life.
Some moments are rocks, forcing you to hold on to them, weighing you down. Others are liquid, slipping out of your
hands despite your best attempts. I suspect we are all frantically searching for a way to hold on
to the things that are rapidly slipping away from our grasp.
</p>
<p class="text">
This is a blog. In much the sense that Tumblr is a blogging platform,
so too is this website.
</p>
<p class="text">
If you realize that youre dying, they only thing to do is turn back toward your childhood.
Oh so many people dont realize, or realize too late. I saw a man getting a transfusion of blood.
Though it was necessary to keep him alive, the pain of having it injected into him incapacitated him.
</p>
<p class="text">
This is WhisperMaPhone. Its a tool I created to share my thoughts. It is currently closed source,
as it is still in active development. However, if youre interested in setting up a page similar to this,
let me know.
</p>
<p class="text quote">
“Our brains are dulled by the incurable mania of wanting to make the unknown known, classifiable.
The desire for analysis wins out over the sentiments.”
</p>
<p class="text">
This is the void. Theres no way for you to contact me on here.
There are no guarantees when youre shouting into the void. But the void doesnt normally shout back.
No one ever said the void wasnt listening. Youre listening. This is my void.
</p>
<p class="text">
I use single quotes to indicate that Im paraphrasing, and double-quotes to indicate a direct quotation from someone else.
Sometimes I will not cite my source, because the origin is less important that the words.
Sometimes I dont remember the source.
</p>
<p class="text">
The portion of this website which is hidden from prying eyes is written in Python, with Django.
Although it is visible to a couple of eyes on account of their not-prying. Everyone looks but only
few find. Still, you cannot find if you do not look, although whether you are more or less prepared
to understand what you find is a different point entirely.
</p>
<p class="text quote">
“In this day and age logical
methods are applicable only to solving problems of secondary
interest”
</p>
<p class="text">
It has recently come to my attention that there is some confusion regarding the purpose of this website,
or even, what it is. This page should answer both of those questions.
</p>
<p class="text">
I can only pray you understand what I give you, but likely you will not. They never did understand the gifts they
were given.
</p>
<p class="text">
This is a surrender and a revel. This is an experiment.
This is a rejection of social media and an embrace of it.
</p>
<p class="text">
This is a circus inside a clown factory. This is a bomb.
</p>
<em class="text">
All thoughts are provided with a timestamp in the timezone of the poster, to ensure proper contextualization.
Contextualization is of utmost importance.
</em>
<p class="text" style="padding-left: calc(100% - 200px)">
This is different. This is the same.
</p>
<p class="text">
Im Matthias. (most of the time)
</p>
<p class="text">
This is a Russian bot attempting to manipulate the election process.
Posts here represent a subset of the result of training a GPT-3 text generation algorithm on my brain.
</p>
<p class="text">
This is a result of not even knowing what alcohol tastes like.
This is fire from the past. This is my first most prized possession.
This is <span style="text-decoration: line-through">Researcher Talloran</span>.
</p>
<em class="text">
All thoughts are provided completely free of context.
</em>
<p class="text">
Gödel has disproved logic. The imperfections of the human mind are inescapable. There are perhaps
corners of that 2D plane of logic that remain unexplored, but by logic alone that plane will never be fully
explored, so those corners do not interest us. No, let us think in 3D.
</p>
<p class="text">
This is an ebenezer to the Lord and the things He has done for me.
</p>
<p class="text">
Im hungry.
</p>
<p class="text">Hey there, its Nate.</p>
<p class="text">
This website is an outlet for the many intrusive thoughts that I have, that I cannot speak out loud. These thoughts dont have meaning, they arent important. Even if it were socially acceptable for me to share them at random times, even if people were interested, theyre still not of any particular value. On the other hand, here they can be themselves. They can exist free from judgement. You, and me, come here to examine the curious thoughts of a curious boy. Here my thoughts can thrive.
</p>
<p class="text">
This is the real track four.
</p>
<p class="text">
This is a story about a boy.<br>
People dont like things that are different.<br>
The world is confusing.<br>
This is a cult.
</p>
<p class="text">
This is an explanation. This page answers any questions you may have about this website.
</p>
<p class="text">
Welcome to my website! This is a micro-blogging platform for me to share my thoughts.
This website breaks normal blogging and micro-blogging paradigms. I have 140 character limit on the first line,
for real quick thoughts, and then a Show More button, and then unlimited characters in a second field.
Ill sometimes use hundreds of characters, writing mini-essays on topics that catch my interest.
This would be very impractical to do on Twitter. Although this websites solves another gripe with Twitter,
the concept of “liking” Tweets. I dont ask for your feedback here. This frees me to post what Im inspired to,
and allows you to experience what I post without worrying about if you should “like” it.
</p>
<p class="text">
This is how I learned to stop worrying. This is a flea in which our two bloods mingled be.
</p>
<p class="text">
All thoughts posted here are those of my employer.
</p>
<p class="text">
This is the end and a new beginning. Ive stopped using Discord, Twitter, Instagram, and other social media
since creating this website.
</p>
<p class="text">
I open vim to write. I dont have a spell checker, so I struggle, catching my own errors by sight, recognizeing that the word looks wrong, but not knowing how to fix it. I shrug. Ill go over it with a spell-check at some point. But I might not. I might impuse-publish this page, one night, when Im feeling particularly lonely. Then the message that I dread (“you spelled recognizeing wrong”) might come as a welcome social interaction, and an opprotunity to improve my website. Rather than an annoyance.
</p>
<p class="text">
This is not a test. This is considered harmful. This is mostly harmless.
Fear the daemons you have created at JOKEEFUNNY.COM.
</p>
<p class="text">
This is art. This is poetry.
</p>
<p class="text">
This is a revolution of the proletariat. We will not sit quietly.<br>
This is sitting loudly. This website is inaction. It exists only to let me complain.
</p>
<p class="text">
I find it helps to imagine all of my posts here as the murmurings of someone talking in their sleep.
</p>
<p class="text">
I bought a book earlier today. I am writing instead of reading it. The irony. I created this site to give me a space to write, and yet, often I post here merely things that I have read. The irony.
</p>
<p class="text">
Thank you for coming to my website, and reading some of my thoughts.
It really does mean a lot to me that youre interested enough to read them.
I pride myself on my thoughts, and if people werent interested in them
<p class="text">
Im tired.<br>
Im tired of this. I want to do something interesting, something different. I want to do something different every day.
I want to feel something.
</p>
<p class="text">
This is an attempt to feel something.<br>
This is a safe space for me to vent.
</p>
<p class="text">
This is the end of my sanity, of normalcy, of grammaer and ru;es and smaness.
</p>
<p class="text">
This is poetry, this is all poetry, and if you havent figured that out by now, then Im probably not that interested in listening to you, so you shouldnt feel any obligation to listen to me. This is the poetry of a broken generation. Of a generation that is as broken as any that came before it, and yet feels the pain of brokenness just as strongly as any others.
</p>
<p class="text">
Pray with me. <em>The Lord is close to the brokenhearted and saves those who are crushed in spirit.</em> The LORD is close to me. The LORD is close to me. The LORD is close to me. The LORD is close to me. The LORD is close to me. The LORD is close to me. The LORD is close to me. The LORD is close to me. The LORD is close to me.
</p>
<p class="text">
This website is a prayer.
</p>
<p class="text">
This website is.
</p>
<p class="text">
This website is an exercise in mental stimulation.
This website stores Thoughts that I have, but it also encourage me to have thoughts in the first place.
</p>
<p class="text">
This website is a part of who I am<br>
This is a representation of my mind.<br>
This is an influence on me<br>
</p>
<p class="text">
Soli Deo gloria
</p>
<p class="text">
Thoughts posted here seem to fall into one of a few discrete categories. Firstly, analysises of topics, things that would be too long for Twitter, or else invite debate. Second, updates on my life and what Im doing and feeling. Third, expressions, of feelings. All try to be informative, humours, and rehtorically enjoyable.
</p>
<p class="text">
Poetry is text written to convey emotion. Text is written to convey emotion. This is text.
</p>
<p>
This is not text.
</p>
<p class="text">
This is a fan page for the Breckenridge Jazz Hands.
</p>
<p class="text">
Im always hungry.
</p>
<p class="text">
Please, dont try to share anything here in other places. I just dont imagine it going well. Although sharing this website as a whole I imagine would be beneficial.
</p>
<p class="text">
I fear this is a creative writing exercise.
</p>
<p class="text">
This is the end.
</p>
<p class="text">
This is my last ditch effort. My guilty pleasure. My bad habit.
</p>
<p class="text">
Good Night.
</p>
<p class="text">
This is my late-night indulgence. Be sure to like and subscribe for more.
</p>
<p class="text">
This is the result of not getting enough sleep.
</p>
<p class="text">
This what Im thinking.
</p>
<p class="text">
These are my Thoughts.
</p>
{% endblock %}

View File

@ -0,0 +1,12 @@
=> /about About
# Thoughts
{% for thought in thoughts %}{{ thought.text|safe }}
{% if thought.extended_text %}
{{ thought.extended_text|safe }}
{% endif %}{% if thought.media %}
=> gemini://thoughts.learnerpages.com{{ thought.media.url }}{% endif %}{% load tz %}
```
{% timezone thought.get_timezone %}{{ thought.posted|time:"g:i a" }} {{ thought.posted|date:"M d, Y" }}, UTC{{ thought.get_offset_hours }}{% endtimezone %}
```
▔▔▔
{% endfor %}

View File

@ -1,85 +1,118 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Thoughts</title>
{% extends "whispermaphone/page.html" %}
{% load static %}
{% load static %}
<link href="{% static 'main/main.css' %}" rel="stylesheet">
<link href="{% static 'main/codehighlight.css' %}" rel="stylesheet">
{% block title %}Thoughts{% endblock %}
<link rel="icon" sizes="192x192" href="{% static 'images/favicon-192x192.png'%}">
<link rel="apple-touch-icon" href="{% static 'images/apple-touch-icon.png'%}"/>
{% block navigation %}
<h1 class="text" aria-current="page">Thoughts</h1>
<a href="/about" class="text">About</a>{% if authenticated %}
<a href="/post" class="text" style="border: none">Post</a>
{% endif %}
{% endblock %}
<link rel="alternate" href="/feed" type="application/rss+xml" title="RSS">
{% block head %}
<link rel="alternate" href="/feed" type="application/rss+xml" title="RSS">
<style>
/*Semantic styles load as part of the page*/
.hidden {
display: none;
}
</style>
</head>
<body>
<header>
<h1>
<span class="text">Thoughts</span>{% if authenticated %}
<a href="/post" class="text" style="border: none">Post</a>
{% endif %}</h1>
</header>
<link href="{% static 'main/codehighlight.css' %}" rel="stylesheet">
{% endblock %}
{% block main %}
{% if not first_page %}
<nav class="history-nav top" aria-label="History Navigation">
<ul>
{% for page in pages %}
{% if page.slug == current_page %}
<li><span class="current-page">{{ page.formatted_name }}</span></li>
{% else %}
<li><a href="?page={{ page.slug }}">{{ page.formatted_name }}</a></li>
{% endif %}
{% endfor %}
</ul>
</nav>
{% endif %}
<section class="main-wrap">
{% load tz %}
{% load markdown %}
{% for thought in thoughts %}
<div class="thought{% if thought.uuid == highlighted %} highlighted{% endif %}" id="{{thought.uuid}}">
<div class="thought{% if thought.uuid == highlighted %} highlighted{% endif %}" id="{{ thought.uuid }}">
<div class="main">
<span class="main-text text">{{ thought.text|markdown }}</span>
</div>
<div class="extended-text text">{{ thought.extended_text|markdown }}</div>
{% with file_type=thought.get_media_type %}
{% if file_type or thought.extended_text.strip %}
<div class="extended">
{% if thought.extended_text.strip %}
<span class="extended-text text">{{ thought.extended_text|markdown }}</span>
{% endif %}
{% if file_type == "png" or file_type == "jpeg" %}
<img src="{{ thought.media.url }}" class="extended-media" alt="{{ thought.media_alt }}">
{% elif file_type == "m4a" or file_type == "mp3" or file_type == "aac" %}
<audio controls src="{{ thought.media.url }}" class="extended-media"></audio>
{% elif file_type == "mov" or file_type == "mp4" %}
<video src="{{ thought.media.url }}" class="extended-media"></video>
{% endif %}
</div>
{% endif %}
{% endwith %}
<div class="thought-end">
<span class="timestamp">
{% timezone thought.timezone %}
{% timezone thought.get_timezone %}
{{ thought.posted|time:"g:i a" }}
{{ thought.posted|date:"M d, Y" }},
UTC{{ thought.offset_hours }}
UTC{{ thought.get_offset_hours }}
{{ thought.get_season }}
{% endtimezone %}
</span>
<span class="permalink">
<a class="button" href="/?show={{thought.uuid}}">Permalink</a>
</span>
</div>
<hr>
</div>
{% endfor %}
{% endblock %}
</section>
<!-- <footer>Copyright Matthias @2020{% if authenticated %}-->
<!-- <a href="/post" style="color: var(&#45;&#45;text-color); margin-left: 100px; text-decoration: none">POST</a>-->
<!-- {% endif %}</footer>-->
{% block footer %}
<nav class="history-nav bottom" aria-label="History Navigation">
<ul>
{% for page in pages %}
{% if page.slug == current_page %}
<li><span class="current-page">{{ page.formatted_name }}</span></li>
{% else %}
<li><a href="?page={{ page.slug }}">{{ page.formatted_name }}</a></li>
{% endif %}
{% endfor %}
</ul>
</nav>
{% endblock %}
{% block scripts %}
<script>
const els = document.querySelectorAll(".thought");
for (let el of els) {
const extended = el.querySelector(".extended-text");
const extended = el.querySelector(".extended");
//Hide extended text
extended.classList.add("hidden");
//Add button to show extended text
if (extended.textContent.length) {
const main = el.querySelector(".main");
const showMoreButton = document.createElement("button");
showMoreButton.appendChild(document.createTextNode("Show More"));
showMoreButton.classList.add("show-more");
showMoreButton.addEventListener("click", evt => {
// Remove ourself
showMoreButton.parentNode.removeChild(showMoreButton);
// Show the extended text
extended.classList.remove("hidden");
})
main.appendChild(showMoreButton);
if (extended) {
//Hide extended text
extended.classList.add("hidden");
//Add button to show extended text
if (extended.childNodes.length) {
const main = el.querySelector(".main");
const showMoreButton = document.createElement("button");
showMoreButton.appendChild(document.createTextNode("Show More"));
showMoreButton.classList.add("show-more");
showMoreButton.addEventListener("click", evt => {
// Remove ourself
showMoreButton.parentNode.removeChild(showMoreButton);
// Show the extended text
extended.classList.remove("hidden");
})
main.appendChild(showMoreButton);
}
}
}
@ -117,20 +150,27 @@
}
}).use(remarkable.linkify);
// Allow data: URIs for images, from https://github.com/jonschlinkert/remarkable/issues/329
const originalLinkValidator = md.inline.validateLink;
const dataLinkRegex = /^\s*data:([a-z]+\/[a-z]+(;[a-z-]+=[a-z-]+)?)?(;base64)?,[a-z0-9!$&'',()*+,;=\-._~:@/?%\s]*\s*$/i;
md.inline.validateLink = (url) => originalLinkValidator(url) || url.match(dataLinkRegex);
// I'm leaving in "references", which handles the convention of putting your links at the bottom of your post
md.core.ruler.disable(["abbr", "abbr2", "footnote_tail", "replacements"]);
md.block.ruler.disable(["hr", "footnote", "heading", "lheading", "table", "htmlblock"]);
md.inline.ruler.disable(["del", "ins", "mark", "sub", "sup", "footnote_inline", "footnote_ref", "htmltag", "entity", "autolink"]);
//Images are parsed as inline links, so we can't turn off parsing,
//but we can overwrite the renderer so they never get displayed. Not ideal
/*md.renderer.rules.image = function () {
return "";
};*/
/*
for (let el of els) {
const extended = el.querySelector(".extended-text");
const extendedText = el.querySelector(".extended-text");
const mainText = el.querySelector(".main-text");
//Markdown + highlight
extended.innerHTML = md.render(extended.textContent);
mainText.innerHTML = md.render(mainText.textContent);
}*/
if (extendedText) {
extendedText.innerHTML = md.render(extendedText.textContent);
}
}
*/
</script>
</body>
</html>
{% endblock %}

View File

@ -1,19 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Login</title>
{% extends "whispermaphone/page.html" %}
{% load static %}
{% load static %}
<link href="{% static 'main/login.css' %}" rel="stylesheet">
<link href="{% static 'main/main.css' %}" rel="stylesheet">
{% block title %}Post{% endblock %}
<link rel="icon" sizes="192x192" href="{% static 'images/favicon-192x192.png'%}">
<link rel="apple-touch-icon" href="{% static 'images/apple-touch-icon.png'%}"/>
</head>
<body>
<section class="main-content">
{% block head %}
<link href="{% static 'main/login.css' %}" rel="stylesheet">
{% endblock %}
{% block navigation %}
<a href="/" class="text" style="border: none">Thoughts</a>
<h1 class="text">Login</h1>
{% endblock %}
{% block main %}
<span class="text">
Please enter the password to access this page.
</span>
@ -22,14 +21,13 @@
<input type="password" id="password">
<input type="submit" value="Login">
</form>
</section>
{% endblock %}
{% block scripts %}
<script>
document.getElementById("password-form").addEventListener("submit", evt => {
document.cookie = `password=${document.getElementById("password").value}; max-age=15768000; samesite=strict`;
window.location.reload();
})
</script>
</body>
</html>
{% endblock %}

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{% endblock %}</title>
{% load static %}
<link href="{% static 'main/main.css' %}" rel="stylesheet">
{% block head %}{% endblock %}
<link rel="icon" sizes="192x192" href="{% static 'images/favicon-192x192.png'%}">
<link rel="apple-touch-icon" href="{% static 'images/apple-touch-icon.png'%}"/>
<style>
/*Semantic styles load as part of the page*/
.hidden {
display: none;
}
</style>
</head>
<body>
<header>
<nav id="main-nav">
{% block navigation %}{% endblock %}
</nav>
</header>
<main class="main-wrap" id="main-content">
{% block main %}
{% endblock %}
</main>
<footer>
{% block footer %}
{% endblock %}
</footer>
{% block scripts %}
{% endblock %}
</body>
</html>

View File

@ -1,41 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Post</title>
{% extends "whispermaphone/page.html" %}
{% load static %}
{% load static %}
<link href="{% static 'main/post.css' %}" rel="stylesheet">
<link href="{% static 'main/main.css' %}" rel="stylesheet">
{% block title %}Post{% endblock %}
<link rel="icon" sizes="192x192" href="{% static 'images/favicon-192x192.png'%}">
<link rel="apple-touch-icon" href="{% static 'images/apple-touch-icon.png'%}"/>
</head>
<body>
<header>
<h1>
<a href="/" class="text" style="border: none">Thoughts</a>
<span class="text">Post</span>
</h1>
</header>
{% block head %}
<link href="{% static 'main/post.css' %}" rel="stylesheet">
{% endblock %}
<section class="main-wrap">
<form action="{% url 'post'%}" method="post">
{% csrf_token %}
<textarea name="text" id="text" class="thought" rows="1" placeholder="What are you thinking?"></textarea>
<textarea name="extended_text" id="extended_text" class="thought" rows="2" placeholder="Anything else?"></textarea>
<input type="hidden" name="timezone_offset" id="timezone_offset">
{% block navigation %}
<a href="/" class="text" style="border: none">Thoughts</a>
<a href="/about" class="text" style="border: none">About</a>
<h1 class="text">Post</h1>
{% endblock %}
<input type="submit" id="post-button" value="Submit">
</form>
</section>
{% block main %}
<form action="{% url 'post'%}" method="post" enctype="multipart/form-data" id="post-form">
<div class="error">{{ form_error }}</div>
{% csrf_token %}
{{ form.text }}
{{ form.extended_text }}
{{ form.timezone_offset }}
<div class="media-wrapper">
{{ form.media }}
{{ form.media_alt }}
</div>
<input type="submit" id="post-button" value="Submit">
{{ form.submit }}
</form>
<!-- {{ form }} -->
{% endblock %}
{% block scripts %}
<script>
const textEl = document.getElementById("text");
const textExtEl = document.getElementById("extended_text");
const textEl = document.getElementById("id_text");
const textExtEl = document.getElementById("id_extended_text");
function updateBoxHeight(box) {
//style.height doesn't include padding, .scrollHeight does
box.style.height = "auto";
box.style.height = box.scrollHeight + "px";
}
// If we're handling maxlength in JS, then having the maxlength attribute is annoying
// We could (should?) also leave the property on there and then add a keydown
// listener when you're at 140 characters, but this is easier
textEl.removeAttribute("maxlength");
textEl.addEventListener("input", evt => {
// If the length is more than 120
// If the length is more than 140
const value = textEl.value;
if (value.length > 140) {
const splitAt = value.lastIndexOf(" ", 140) + 1; // Plus 1 keeps the space in the original text instead of moving it
@ -61,46 +78,50 @@
textEl.selectionStart = startPos;
textEl.selectionEnd = endPos;
}
}
// TODO: Auto-set width of short text box
//textEl.style.width = "auto";
//textEl.style.width = textEl.scrollWidth + "px";
});
//Allow pasting images into the extended text text-box
textExtEl.addEventListener("paste", (evt) => {
const items = evt.clipboardData.items;
const el = evt.target;
for (const item of items) {
//Mac clipboard at least only handles PNGs
if ("image/png" === item.type) {
const reader = new FileReader();
reader.addEventListener("load", evt => {
const newText = `![](${evt.target.result})`;
el.value = el.value.slice(0, el.selectionStart) + newText + el.value.slice(el.selectionEnd);
});
reader.readAsDataURL(item.getAsFile());
}
//Resize both text boxes
updateBoxHeight(textExtEl);
updateBoxHeight(textEl);
}
});
const mediaInput = document.getElementById("id_media");
const mediaAlt = document.getElementById("id_media_alt");
function updateMediaAlt() {
if (mediaInput.value) {
mediaAlt.classList.remove("hidden");
}else {
mediaAlt.classList.add("hidden");
}
updateBoxHeight(mediaAlt);
}
mediaAlt.addEventListener("input", () => {
updateBoxHeight(mediaAlt);
});
mediaInput.addEventListener("input", updateMediaAlt);
updateMediaAlt();
const textboxes = document.getElementsByClassName("thought");
for (const box of textboxes) {
//style.height doesn't include padding, .scrollHeight does
//This is fine as long as we reset scrollHeight before changing it
//And we have to set it this first time
//This also effectively doubles the padding value we set in CSS
box.style.height = box.scrollHeight + "px";
box.addEventListener("input", e => {
box.style.height = "auto";
box.style.height = box.scrollHeight + "px";
// Set the initial heights of the boxes
updateBoxHeight(box);
box.addEventListener("input", evt => updateBoxHeight(box));
// Allow pasting images into either text box
box.addEventListener("paste", evt => {
const files = evt.clipboardData.files;
// We can only use the first image from the pasteboard
if (files && files.length > 0) {
// This seems to only allow entry of a single file
mediaInput.files = files;
updateMediaAlt();
}
});
}
const timezoneOffsetEl = document.getElementById("timezone_offset");
const timezoneOffsetEl = document.getElementById("id_timezone_offset");
timezoneOffsetEl.value = (new Date()).getTimezoneOffset();
</script>
</body>
</html>
{% endblock %}

View File

@ -1,61 +1,121 @@
import os.path
import uuid
import subprocess
import magic
from django.http import HttpResponse
from django.shortcuts import render
from django.utils import timezone
from django.utils.crypto import constant_time_compare
from .models import Thought
from whispermaphone import settings
from .models import Thought, ThoughtForm, ALLOWED_MEDIA_TYPES
from .pagination import get_all_pages, get_page_slug
def index(request):
def check_authenticated(request):
authenticated = False
try:
if request.COOKIES["password"] == "ChromaticWave":
if constant_time_compare(request.COOKIES["password"], settings.PASSWORD):
authenticated = True
except KeyError:
pass
return authenticated
def index(request):
authenticated = check_authenticated(request)
try:
highlighted_uuid = uuid.UUID(request.GET.get("show", ""))
except ValueError:
highlighted_uuid = ""
thoughts = Thought.objects.order_by("-posted")
# Figure out what page we're viewing
pages = get_all_pages()
for thought in thoughts:
thought.timezone = timezone.get_fixed_timezone(-thought.timezone_offset)
offset_hours = -thought.timezone_offset / 60
if offset_hours == int(offset_hours):
offset_hours = int(offset_hours)
if offset_hours > 0:
offset_hours = "+" + str(offset_hours)
thought.offset_hours = offset_hours
# First item in pages should be listed first
requested_page = pages[0]
requested_slug = request.GET.get("page", default=requested_page.slug)
# show=uuid takes priority over page
if highlighted_uuid:
try:
highlighted_thought = Thought.objects.get(uuid=highlighted_uuid)
requested_slug = get_page_slug(highlighted_thought)
except Thought.DoesNotExist:
pass
if requested_page.slug != requested_slug:
for p in pages:
if p.slug == requested_slug:
requested_page = p
thoughts = requested_page.get_all_entries()
return render(request, "whispermaphone/index.html", {
"thoughts": thoughts,
"highlighted": highlighted_uuid,
"authenticated": authenticated
"authenticated": authenticated,
"pages": pages,
"current_page": requested_slug,
"first_page": requested_page == pages[0] # if you're viewing the first page
})
def post(request):
try:
if not request.COOKIES["password"] == "ChromaticWave":
return render(request, "whispermaphone/login.html", status=401)
except KeyError:
if not check_authenticated(request):
return render(request, "whispermaphone/login.html", status=401)
if request.method == "POST":
if len(request.POST["text"].strip()) == 0:
return HttpResponse("Need some text.", status=400)
thought_form = ThoughtForm(request.POST, request.FILES, instance=Thought())
if not thought_form.is_valid():
errors = thought_form.errors.as_data()
# Media formatting errors
try:
problem_field = list(errors.keys())[0]
message = list(errors.values())[0][0].messages[0]
# error_line = f"{problem_field[0].upper()}{problem_field[1:]}: {message}"
error_line = f"{message}"
except:
error_line = f"An unknown error occurred processing your request: {errors}"
if len(request.POST["text"]) > 140:
return HttpResponse("Content too long", status=400)
return render(request, "whispermaphone/post.html", {
"form": thought_form,
"form_error": error_line
}, status=400)
Thought(
text=request.POST["text"],
extended_text=request.POST["extended_text"],
timezone_offset=request.POST["timezone_offset"]
).save()
# Create a thought object we can work with
# But don't save it the DB yet
thought = thought_form.save(commit=False)
return render(request, "whispermaphone/post.html", {})
# Do media processing (already validated)
if "media" in request.FILES:
chunk = next(thought.media.chunks(chunk_size=2048))
media_type = magic.from_buffer(chunk, mime="True")
thought.media.name = f"{thought.uuid}.{ALLOWED_MEDIA_TYPES[media_type]}"
if media_type == "audio/x-m4a":
# This is a hack-fix because I want to be able to upload audio
# In the future, this should be refactored to convert file types
# using ffmpeg.js on the client side (so there are 0 security concerns)
# and then the backend just has to check a 3 item whitelist
thought.save() # Save so that we have a file to work with
subprocess.run(["ffmpeg",
"-i", thought.media.path,
"-codec:a", "aac", "-vn",
os.path.join(settings.MEDIA_ROOT, f"{thought.uuid}.aac")
], check=True)
os.remove(os.path.join(settings.MEDIA_ROOT, f"{thought.uuid}.m4a")) # Remove the original file
thought.media.name = f"{thought.uuid}.aac" # Update the file in the DB
# Save for real
thought.save()
return render(request, "whispermaphone/post.html", {"form": ThoughtForm()})
def about(request):
authenticated = check_authenticated(request)
return render(request, "whispermaphone/about.html", {"authenticated": authenticated})

View File

@ -1,3 +1,7 @@
bleach==3.3.0
Django==3.1.7
Markdown==3.3.4
Django~=3.2
python-magic~=0.4.22
bleach~=3.3.0
Pygments~=2.8.1
Markdown~=3.3.4
jetforce
python-decouple

View File

@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/3.1/ref/settings/
"""
from pathlib import Path
from decouple import config
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
@ -20,14 +21,16 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'qdm4_0b)3^)k$6r($!o^a7&0l#^6)@g2wr!x0r40ii@9otfnwo'
SECRET_KEY = config("SECRET_KEY", default="qdm4_0b)3^)k$6r($!o^a7&0l#^6)@g2wr!x0r40ii@9otfnwo")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
DEBUG = config("DEBUG", default=True, cast=bool)
PASSWORD = config("PASSWORD", default="password")
ALLOWED_HOSTS = ["thoughts.learnerpages.com"]
if DEBUG:
ALLOWED_HOSTS += ["*"]
ALLOWED_HOSTS = ["*"]
# Application definition
@ -45,6 +48,8 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
CSRF_COOKIE_SECURE = True
ROOT_URLCONF = 'whispermaphone.urls'
TEMPLATES = [
@ -74,6 +79,7 @@ DATABASES = {
}
}
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/
@ -95,3 +101,6 @@ USE_TZ = True
STATIC_URL = '/static/'
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
STATIC_ROOT = BASE_DIR / "static/"
MEDIA_ROOT = BASE_DIR / "media/"
MEDIA_URL = "/media/"

View File

@ -1,25 +1,13 @@
"""whispermaphone URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf.urls.static import static
from django.urls import path
from main import views
from main.feed import MainFeed
from whispermaphone import settings
urlpatterns = [
path("", views.index, name="index"),
path("about", views.about, name="about"),
path("post", views.post, name="post"),
path("feed", MainFeed())
]
path("feed", MainFeed()),
] + (static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) if settings.DEBUG else [])