Compare commits

..

No commits in common. "main" and "v1" have entirely different histories.
main ... v1

28 changed files with 1206 additions and 1555 deletions

10
.gitignore vendored
View File

@ -1,7 +1,5 @@
.venv
app.log
__pycache__/
database.db
.env .env
flask_session .venv
temp app.db
__pycache__
flask_session

View File

@ -8,16 +8,12 @@ COPY requirements.txt .
# Install the required packages # Install the required packages
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
RUN pip install gunicorn
# Copy the rest of the application code into the container # Copy the rest of the application code into the container
COPY src . COPY src/ ./src
# Expose the port the app runs on # Expose the port the app runs on
EXPOSE 5000 EXPOSE 5000
# Set environment variables
ENV FLASK_APP=main.py
# run the application # run the application
ENTRYPOINT [ "gunicorn", "-b", ":5000", "--access-logfile", "-", "--error-logfile", "-", "main:app" ] CMD ["python", "src/main.py"]

View File

@ -2,6 +2,6 @@
prismic is a simple messageboard made in python prismic is a simple messageboard made in python
## Planned features ## Planned features
- [x] user board creation - [ ] user board creation
- [x] custom profiles - [ ] custom profiles
- [x] moderation tools - [x] moderation tools

View File

@ -1,5 +1,2 @@
mysql-connector-python # only required for external mysql connection flask
python-dotenv # only required if using .env file flask-session
python-dateutil
flask-session
flask

View File

@ -1,185 +1,581 @@
import os, hashlib, sqlite3, logging import sqlite3, logging, os
from os import getenv as env
import mysql.connector
log = logging.getLogger(__name__) # Configure logging
FETCHALL = 0 logger = logging.getLogger(__name__)
FETCHONE = 1
class Database: class Database:
def __init__(self): def __init__(self, db_name):
self.connection = None # Initialize the database connection
self.cursor = None logger.info("Initializing database connection...")
self.connection = sqlite3.connect(db_name, check_same_thread=False)
self.cursor = self.connection.cursor()
logger.info("Database connection established.")
def connect_sqlite(self, db_path): # Create users table if it doesn't exist
try: logger.info("Creating users table if it doesn't exist...")
self.connection = sqlite3.connect(db_path, check_same_thread=False) self.cursor.execute('''
self.cursor = self.connection.cursor() CREATE TABLE IF NOT EXISTS users (
log.info("Connected to SQLite database") id INTEGER PRIMARY KEY AUTOINCREMENT,
except sqlite3.Error as e: username TEXT NOT NULL UNIQUE,
log.error(f"SQLite connection error: {e}") password TEXT NOT NULL,
raise session TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
def connect_mysql(self):
try:
self.connection = mysql.connector.connect(
host=env('MYSQL_HOST'),
user=env('MYSQL_USER'),
password=env('MYSQL_PASSWORD'),
database=env('MYSQL_DATABASE')
) )
self.cursor = self.connection.cursor() ''')
log.info("Connected to MySQL database") logger.info("Users table created.")
except mysql.connector.Error as e:
log.error(f"MySQL connection error: {e}") # Create boards table if it doesn't exist
raise logger.info("Creating boards table if it doesn't exist...")
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS boards (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
owner_id INTEGER NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (owner_id) REFERENCES users (id) ON DELETE SET NULL
)
''')
logger.info("Boards table created.")
# Create posts table if it doesn't exist
logger.info("Creating posts table if it doesn't exist...")
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
board_id INTEGER NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
refence INTEGER,
FOREIGN KEY (refence) REFERENCES posts (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL,
FOREIGN KEY (board_id) REFERENCES boards (id) ON DELETE CASCADE
)
''')
logger.info("Posts table created.")
def close(self): def close(self):
if self.cursor: # Close the database connection
self.cursor.close() logger.info("Closing database connection...")
if self.connection: self.connection.commit()
self.connection.close() self.connection.close()
log.warning("Database connection closed") logger.info("Database connection closed.")
def execute_query(self, query, params=None, fetch_type=FETCHALL):
try:
if params:
self.cursor.execute(query, params)
else:
self.cursor.execute(query)
self.connection.commit()
log.debug(f"Executed query: {query}")
if fetch_type == FETCHALL:
return self.fetchall()
elif fetch_type == FETCHONE:
return self.fetchone()
elif fetch_type is None:
log.debug("No fetch type specified, returning None")
return None
else:
log.warning("Invalid fetch type, returning None")
return None
except (sqlite3.Error, mysql.connector.Error) as e:
log.critical(f"Query execution error: {e}")
raise
def fetchall(self):
try:
result = self.cursor.fetchall()
log.debug(f"Fetched all results")
return result
except (sqlite3.Error, mysql.connector.Error) as e:
log.error(f"Fetchall error: {e}")
raise
def fetchone(self):
try:
result = self.cursor.fetchone()
log.debug(f"Fetched one result")
return result
except (sqlite3.Error, mysql.connector.Error) as e:
log.critical(f"Fetchone error: {e}")
raise
def first_time_run(db:Database): def create_user(self, username, password):
log.info("First time run detected, initializing database") logger.info(f"Creating user: {username}")
# Create users table # Check if the user already exists
log.info("Creating users table") logger.info(f"Checking if user {username} already exists...")
db.execute_query(""" self.cursor.execute('''
CREATE TABLE users ( SELECT * FROM users WHERE username = ?
id INTEGER PRIMARY KEY AUTOINCREMENT, ''', (username,))
username TEXT NOT NULL, user = self.cursor.fetchone()
password TEXT NOT NULL,
about TEXT DEFAULT 'No description',
permissions TEXT DEFAULT 'user',
avatar MEDIUMBLOB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
token TEXT,
avatar_type TEXT
)
""")
# Create posts table if user:
log.info("Creating posts table") logger.warning(f"User {username} already exists.")
db.execute_query(""" return False
CREATE TABLE posts ( logger.info(f"User {username} does not exist.")
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
board_id INTEGER NOT NULL,
content TEXT NOT NULL,
reference INTREGER,
type TEXT DEFAULT 'post',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (board_id) REFERENCES boards (id) ON DELETE CASCADE,
FOREIGN KEY (reference) REFERENCES posts (id) ON DELETE SET NULL
)
""")
# Create boards table # Create a new user
log.info("Creating boards table") self.cursor.execute('''
db.execute_query(""" INSERT INTO users (username, password)
CREATE TABLE boards ( VALUES (?, ?)
id INTEGER PRIMARY KEY AUTOINCREMENT, ''', (username, password))
name TEXT NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
owner_id INTEGER NOT NULL,
FOREIGN KEY (owner_id) REFERENCES users (id)
)
""")
# Create attachments table self.connection.commit()
db.execute_query(""" logger.info(f"User {username} created.")
CREATE TABLE attachments ( return True
id INTEGER PRIMARY KEY AUTOINCREMENT,
post_id INTEGER NOT NULL,
file_data MEDIUMBLOB NOT NULL,
file_name TEXT NOT NULL,
file_type TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES posts (id) ON DELETE CASCADE
)
""")
# Create system user
if not env('SYSTEM_PASSWORD'):
log.warning("SYSTEM_PASSWORD is empty, generating random password")
password = os.urandom(16).hex()
log.info(f"Generated system user password: {password}")
else:
password = env('SYSTEM_PASSWORD')
log.info("Creating system user")
db.execute_query("""
INSERT INTO users (username, password, about, permissions)
VALUES (?, ?, ?, ?)
""", (
env('SYSTEM_USER', default='system'),
env('SYSTEM_PASSWORD', default=hashlib.sha256(password.encode()).hexdigest()),
env('SYSTEM_ABOUT', default='System User'),
env('SYSTEM_PERMISSIONS', default='admin')
))
# Create system boards def get_user(self, username):
boards_names = env('SYSTEM_BOARDS', default='General,Random,System').split(',') # Get user by username
if "System" not in boards_names: logger.info(f"Getting user {username}...")
boards_names.append("System") self.cursor.execute('''
SELECT * FROM users WHERE username = ?
''', (username,))
user = self.cursor.fetchone()
log.info(f"Creating system boards: {', '.join(boards_names)}") if user:
for board_name in boards_names: logger.info(f"User {username} found.")
db.execute_query(""" return user
INSERT INTO boards (name, description, owner_id) logger.warning(f"User {username} not found.")
return None
def create_board(self, name, description, owner_id):
logger.info(f"Creating board: {name}")
# Check if the board already exists
logger.info(f"Checking if board {name} already exists...")
self.cursor.execute('''
SELECT * FROM boards WHERE name = ?
''', (name,))
board = self.cursor.fetchone()
if board:
logger.warning(f"Board {name} already exists.")
return False
logger.info(f"Board {name} does not exist.")
# Check if the owner exists
logger.info(f"Checking if owner {owner_id} exists...")
self.cursor.execute('''
SELECT * FROM users WHERE id = ?
''', (owner_id,))
owner = self.cursor.fetchone()
if not owner:
logger.warning(f"Owner {owner_id} does not exist.")
return False
logger.info(f"Owner {owner_id} exists.")
# Create a new board
self.cursor.execute('''
INSERT INTO boards (name, owner_id, description)
VALUES (?, ?, ?) VALUES (?, ?, ?)
""", ( ''', (name, owner_id, description))
board_name,
f"This is a automatically created board for {board_name}",
1
))
log.info("First time run completed") self.connection.commit()
logger.info(f"Board {name} created.")
return True
def get_board(self, name):
# Get board by name
logger.info(f"Getting board {name}...")
self.cursor.execute('''
SELECT * FROM boards WHERE name = ?
''', (name,))
board = self.cursor.fetchone()
if board:
logger.info(f"Board {name} found.")
return board
logger.warning(f"Board {name} not found.")
return None
def create_post(self, user_id, board_id, content, ref=None):
logger.info(f"Creating post for user {user_id} in board {board_id}...")
# Check if the user exists
logger.info(f"Checking if user {user_id} exists...")
self.cursor.execute('''
SELECT * FROM users WHERE id = ?
''', (user_id,))
user = self.cursor.fetchone()
if not user:
logger.warning(f"User {user_id} does not exist.")
return False
logger.info(f"User {user_id} exists.")
# Check if the board exists
logger.info(f"Checking if board {board_id} exists...")
self.cursor.execute('''
SELECT * FROM boards WHERE id = ?
''', (board_id,))
board = self.cursor.fetchone()
if not board:
logger.warning(f"Board {board_id} does not exist.")
return False
logger.info(f"Board {board_id} exists.")
# Check if the reference post exists
if ref is not None:
logger.info(f"Checking if reference post {ref} exists...")
self.cursor.execute('''
SELECT * FROM posts WHERE id = ?
''', (ref,))
reference_post = self.cursor.fetchone()
if not reference_post:
logger.warning(f"Reference post {ref} does not exist.")
return False
logger.info(f"Reference post {ref} exists.")
else:
logger.info("No reference post provided.")
# Create a new post
self.cursor.execute('''
INSERT INTO posts (user_id, board_id, content, refence)
VALUES (?, ?, ?, ?)
''', (user_id, board_id, content, ref))
self.connection.commit()
logger.info(f"Post created for user {user_id} in board {board_id}.")
post_id = self.cursor.lastrowid
logger.info(f"Post ID: {post_id}")
return post_id
def get_post(self, post_id):
# Get post by ID
logger.info(f"Getting post {post_id}...")
self.cursor.execute('''
SELECT * FROM posts WHERE id = ?
''', (post_id,))
post = self.cursor.fetchone()
if post:
logger.info(f"Post {post_id} found.")
return post
logger.warning(f"Post {post_id} not found.")
return None
def get_post_references(self, post_id, limit=10, offset=0):
# Get references for a post
logger.info(f"Getting references for post {post_id}...")
self.cursor.execute('''
SELECT * FROM posts WHERE refence = ? LIMIT ? OFFSET ?
''', (post_id, limit, offset))
references = self.cursor.fetchall()
if references:
logger.info(f"References found for post {post_id}.")
return references
logger.warning(f"No references found for post {post_id}.")
return None
def get_posts(self, board_id, limit, offset):
if board_id:
logger.info(f"Getting posts for board {board_id}...")
# Check if the board exists
logger.info(f"Checking if board {board_id} exists...")
self.cursor.execute('''
SELECT * FROM boards WHERE id = ?
''', (board_id,))
board = self.cursor.fetchone()
if not board:
logger.warning(f"Board {board_id} does not exist.")
return None
logger.info(f"Board {board_id} exists.")
# Get posts for the board
logger.info(f"Getting posts for board {board_id}...")
self.cursor.execute('''
SELECT * FROM posts WHERE board_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?
''', (board_id, limit, offset))
posts = self.cursor.fetchall()
if posts:
logger.info(f"Posts found for board {board_id}.")
return posts
logger.warning(f"No posts found for board {board_id}.")
return None
else:
# Get all posts
logger.info(f"Getting all posts...")
self.cursor.execute('''
SELECT * FROM posts ORDER BY created_at DESC LIMIT ? OFFSET ?
''', (limit, offset))
posts = self.cursor.fetchall()
if posts:
logger.info(f"Posts found.")
return posts
logger.warning(f"No posts found.")
return None
def get_latest_posts(self, limit):
logger.info(f"Getting latest posts...")
# Get latest posts
logger.info(f"Getting latest posts...")
self.cursor.execute('''
SELECT * FROM posts ORDER BY created_at DESC LIMIT ?
''', (limit,))
posts = self.cursor.fetchall()
if posts:
logger.info(f"Latest posts found.")
return posts
logger.warning(f"No latest posts found.")
return None
def get_boards(self, limit, offset):
logger.info(f"Listing boards with limit {limit} and offset {offset}...")
# Get boards with pagination
self.cursor.execute('''
SELECT * FROM boards ORDER BY created_at DESC LIMIT ? OFFSET ?
''', (limit, offset))
boards = self.cursor.fetchall()
if boards:
logger.info(f"Boards found.")
return boards
logger.warning(f"No boards found.")
return None
def post_to_dict(self, post):
# Convert post to dictionary
logger.info(f"Converting post to dictionary...")
data = {
'id': post[0],
'content': post[3],
'short_content': post[3][:40] + '...' if len(post[3]) > 40 else post[3],
'created_at': post[4],
'url': f"/post/{post[0]}",
}
# Get user information
logger.info(f"Getting user information for post {post[0]}...")
self.cursor.execute('''
SELECT * FROM users WHERE id = ?
''', (post[1],))
user = self.cursor.fetchone()
if user:
data['user'] = {
'id': user[0],
'name': user[1],
'created_at': user[3]
}
logger.info(f"User information found for post {post[0]}.")
else:
logger.warning(f"User information not found for post {post[0]}.")
data['user'] = {
'id': None,
'name': None,
'created_at': None
}
# Get board information
logger.info(f"Getting board information for post {post[0]}...")
self.cursor.execute('''
SELECT * FROM boards WHERE id = ?
''', (post[2],))
board = self.cursor.fetchone()
if board:
data['board'] = {
'id': board[0],
'name': board[1],
'description': board[3],
'created_at': board[4]
}
logger.info(f"Board information found for post {post[0]}.")
else:
logger.warning(f"Board information not found for post {post[0]}.")
data['board'] = {
'id': None,
'name': None,
'description': None,
'created_at': None
}
# Get reference post information
if post[5] is not None:
logger.info(f"Getting reference post information for post {post[0]}...")
self.cursor.execute('''
SELECT * FROM posts WHERE id = ?
''', (post[5],))
reference_post = self.cursor.fetchone()
if reference_post:
data['reference'] = {
'id': reference_post[0],
'content': reference_post[3],
'short_content': reference_post[3][:20] + '...' if len(reference_post[3]) > 20 else reference_post[3],
'created_at': reference_post[4]
}
logger.info(f"Reference post information found for post {post[0]}.")
else:
logger.warning(f"Reference post information not found for post {post[0]}.")
data['reference'] = {
'id': None,
'content': None,
'created_at': None
}
else:
logger.info("No reference post.")
data['reference'] = None
# get post references
logger.info(f"Getting post references for post {post[0]}...")
self.cursor.execute('''
SELECT * FROM posts WHERE refence = ?
''', (post[0],))
references = self.cursor.fetchall()
if references:
data['replies'] = []
for reference in references:
reference_data = {
'id': reference[0],
'content': reference[3],
'short_content': reference[3][:20] + '...' if len(reference[3]) > 20 else reference[3],
'created_at': reference[4]
}
data['replies'].append(reference_data)
logger.info(f"Post references found for post {post[0]}.")
else:
logger.warning(f"No post references found for post {post[0]}.")
data['replies'] = []
logger.info(f"Post converted to dictionary.")
return data
def create_session(self, username, password):
logger.info(f"Creating session for user {username}...")
# Check if the user exists
logger.info(f"Checking if user {username} exists...")
self.cursor.execute('''
SELECT * FROM users WHERE username = ?
''', (username,))
user = self.cursor.fetchone()
if not user:
logger.warning(f"User {username} does not exist.")
return None
logger.info(f"User {username} exists.")
# Check if the password is correct
logger.info(f"Checking password for user {username}...")
if user[2] != password:
logger.warning(f"Incorrect password for user {username}.")
return None
logger.info(f"Password for user {username} is correct.")
# Create a new session and overwrite the old one if it exists
session = os.urandom(16).hex()
self.cursor.execute('''
UPDATE users SET session = ? WHERE username = ?
''', (session, username))
self.connection.commit()
logger.info(f"Session created for user {username}.")
return session
def get_session(self, session):
logger.info(f"Getting session {session}...")
# Get session information
self.cursor.execute('''
SELECT * FROM users WHERE session = ?
''', (session,))
user = self.cursor.fetchone()
if user:
logger.info(f"Session {session} found.")
return user
logger.warning(f"Session {session} not found.")
return None
def delete_session(self, session):
logger.info(f"Deleting session {session}...")
# Delete session
self.cursor.execute('''
UPDATE users SET session = NULL WHERE session = ?
''', (session,))
self.connection.commit()
logger.info(f"Session {session} deleted.")
return True
def user_to_dict(self, user):
# Convert user to dictionary
logger.info(f"Converting user to dictionary...")
data = {
'id': user[0],
'name': user[1],
'created_at': user[4]
}
logger.info(f"User converted to dictionary.")
return data
def get_user_posts(self, user_id, limit=10, offset=0):
logger.info(f"Getting posts for user {user_id}...")
# Get posts for the user
self.cursor.execute('''
SELECT * FROM posts WHERE user_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?
''', (user_id, limit, offset))
posts = self.cursor.fetchall()
if posts:
logger.info(f"Posts found for user {user_id}.")
return posts
logger.warning(f"No posts found for user {user_id}.")
return None
def board_to_dict(self, board):
# Convert board to dictionary
logger.info(f"Converting board to dictionary...")
data = {
'id': board[0],
'name': board[1],
'description': board[3],
'created_at': board[4]
}
logger.info(f"Board converted to dictionary.")
return data
def get_board_posts(self, board_id, limit=10, offset=0):
logger.info(f"Getting posts for board {board_id}...")
# Get posts for the board
self.cursor.execute('''
SELECT * FROM posts WHERE board_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?
''', (board_id, limit, offset))
posts = self.cursor.fetchall()
if posts:
logger.info(f"Posts found for board {board_id}.")
return posts
logger.warning(f"No posts found for board {board_id}.")
return None
def get_all_boards_for_post(self):
logger.info(f"Getting all boards...")
# Get all boards
self.cursor.execute('''
SELECT * FROM boards
''')
boards = self.cursor.fetchall()
if boards:
logger.info(f"Boards found.")
# Convert boards to dictionary
boards = [{"id": board[0], "name": board[1]} for board in boards]
boards.sort(key=lambda x: x['name'])
return boards
logger.warning(f"No boards found.")
return None
def delete_post(self, post_id):
logger.info(f"Deleting post {post_id}...")
# Delete post
self.cursor.execute('''
DELETE FROM posts WHERE id = ?
''', (post_id,))
self.connection.commit()
logger.info(f"Post {post_id} deleted.")
return True

View File

@ -4,24 +4,24 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prismic</title> <title>Prismic</title>
<meta name="author" content="Alfie King"> <link rel="stylesheet" href="../static/base.css">
<link rel="stylesheet" href="../../static/css/base.css">
{% block head %}{% endblock %}
</head> </head>
<body> <body>
<header> <header>
<div id="title"> <div id="title">
<h1>Prismic</h1> <h1>Prismic</h1>
<h2>ver: 2.0</h2> <h2>ver: 1.0</h2>
</div> </div>
<nav> <nav>
<ul> <ul>
<li><a href="/">Home</a></li> <li><a href="/">Home</a></li>
<li><a href="/boards">Boards</a></li> <li><a href="/boards">Boards</a></li>
<li><a href="/posts">Posts</a></li> <li><a href="/posts">Posts</a></li>
<li><a href="/users">Users</a></li> {% if session.name %}
<li><a href="/post">Post</a></li>
{% endif %}
<li>{% if session.name %} <li>{% if session.name %}
<a class="user-{{session.perms}}" href="/users/{{ session.name }}">{{ session.name }}</a> <a href="/user/{{ session.name }}">{{ session.name }}</a>
{% else %} {% else %}
<a href="/login">Login</a> <a href="/login">Login</a>
{% endif %}</li> {% endif %}</li>
@ -30,10 +30,10 @@
</nav> </nav>
</header> </header>
<main> <main>
{% block main_content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>
<footer> <footer>
<p>Created by <a href="https://alfieking.dev">Alfie King</a>{% if session.name %} | <a href="/settings">Settings</a> | <a href="/logout">Logout</a>{% endif %}</p> <p>Created by <a href="https://alfieking.dev">Alfie King</a>{% if session.name %} | <a href="/logout">Logout</a>{% endif %}</p>
</footer> </footer>
</body> </body>
</html> </html>

View File

@ -1,44 +1,44 @@
{% extends "templates/base.html" %} {% extends "base.html" %}
{% block head %} {% block content %}
<link rel="stylesheet" href="../static/css/form.css"> <h1>{{ board.name }}</h1>
{% endblock %} <h6>Created at: {{ board.created_at }}</h6>
{% block main_content %}
<h1 class="board-name">/{{ board.name }}/</h1>
<p>{{ board.description }}</p> <p>{{ board.description }}</p>
{% if board.owner_id == session.user_id %}
<h6><a href="/boards/delete/{{ board.id }}">Delete Board</a></h6>
{% elif session.perms == "admin" %}
<h6><a href="/boards/delete/{{ board.id }}">Delete Board</a></h6>
{% endif %}
{% if session.user_id %}
<br>
<h3>Post to this board</h3>
<form action="/post" method="POST" enctype="multipart/form-data">
<input type="hidden" name="board_id" value="{{ board.id }}">
<textarea name="content" placeholder="Content" required></textarea>
<input type="file" name="attachments" multiple>
<button type="submit">Post</button>
</form>
{% endif %}
<br> <br>
<ul class="post-list"> <h1>Posts:</h1>
{% for post in posts %} <ul class="posts">
{% for post in board.posts %}
<li> <li>
{% include "templates/short_post.html" %} <h3>From {{ post.user.name }}</h3>
<h6>posted at {{ post.created_at }}</h6>
{% if post.reference %}
<h6><b>ref post:</b> <a href="/posts/{{ post.reference.id }}">{{ post.reference.short_content }}</a></h6>
{% endif %}
<p>{{ post.short_content }}</p>
<h6><a href="/posts/{{ post.id }}">View Post</a>
{% if post.replies|length > 0 %}
({{ post.replies|length }} replies)
{% endif %}
{% if session.name == "SYSTEM" %}
| <a href="/delete/post/{{ post.id }}">Delete</a>
{% elif session.name == post.user.name %}
| <a href="/delete/post/{{ post.id }}">Delete</a>
{% endif %}
</h6>
</li> </li>
{% endfor %} {% endfor %}
{% if board.posts|length == 0 %}
<li>No posts found.</li>
{% endif %}
</ul> </ul>
{% if total_pages > 0 %} <div id="nav">
<div id="nav"> <h5>Page {{ page }}</h5>
<h5>Page {{ page }} of {{ total_pages }}</h5> {% if page > 1 %}
{% if page > 1 %} <h5><a href="/boards/{{ board.name }}?page={{ page - 1 }}">Previous Page</a></h5>
<h5><a href="/boards/{{ board.name }}?page={{ page - 1 }}">Previous Page</a></h5> {% endif %}
{% endif %}
{% if posts|length == 10 %} {% if board.posts|length == 10 %}
<h5><a href="/boards/{{ board.name }}?page={{ page + 1 }}">Next Page</a></h5> <h5><a href="/boards/{{ board.name }}?page={{ page + 1 }}">Next Page</a></h5>
{% endif %} {% endif %}
</div> </div>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -1,30 +1,30 @@
{% extends "templates/base.html" %} {% extends "base.html" %}
{% block head %} {% block content %}
<link rel="stylesheet" href="../static/css/boards.css">
{% endblock %}
{% block main_content %}
<h1>Boards</h1> <h1>Boards</h1>
<p>Here are the boards available on this site. To create a new board, click <a href="/boards/new">here</a>.</p> <h5>Page {{ page }}</h5>
<br> <br>
<ul class="board-list"> <ul class="posts">
{% for board in boards %} {% for board in boards %}
<li> <li>
<h3><a class="board-name" href="/boards/{{ board.name }}">/{{ board.name }}/</a></h3> <h3>/{{ board.name }}/</h3>
<h6>created at {{ board.created_at }}</h6>
<p>{{ board.description }}</p> <p>{{ board.description }}</p>
<h6><a href="/boards/{{ board.name }}">View Board</a></h6>
</li> </li>
{% endfor %} {% endfor %}
{% if boards|length == 0 %}
<li>No boards found.</li>
{% endif %}
</ul> </ul>
{% if total_pages > 0 %} <div id="nav">
<div id="nav"> <h5>Page {{ page }}</h5>
<h5>Page {{ page }} of {{ total_pages }}</h5> {% if page > 1 %}
{% if page > 1 %} <h5><a href="/boards?page={{ page - 1 }}">Previous Page</a></h5>
<h5><a href="/boards?page={{ page - 1 }}">Previous Page</a></h5> {% endif %}
{% endif %}
{% if boards|length == 10 %} {% if boards|length == 10 %}
<h5><a href="/boards?page={{ page + 1 }}">Next Page</a></h5> <h5><a href="/boards?page={{ page + 1 }}">Next Page</a></h5>
{% endif %} {% endif %}
</div> </div>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -1,9 +0,0 @@
{% extends "templates/base.html" %}
{% block main_content %}
<h1>OOPSIE WOOPSIE!!</h1>
<h3>UwU you did a slight fucky wucky >w<</h3>
<br>
<h3>Thwe bwunder in qwestion :3</h3>
<p>{{error}}</p>
{% endblock %}

View File

@ -1,24 +1,33 @@
{% extends "templates/base.html" %} {% extends "base.html" %}
{% block head %} {% block content %}
<link rel="stylesheet" href="../static/css/index.css">
{% endblock %}
{% block main_content %}
<h1>Welcome to Prismic</h1> <h1>Welcome to Prismic</h1>
<p>Prismic is a simple, fast, and secure way to share your thoughts with the world. Join us today and start sharing your ideas!</p> <p>Prismic is a simple textboard</p>
<br> <br>
{% if session.name %} <h1>Latest Posts:</h1>
<p>Hello <a class="user-{{session.perms}}" href="/users/{{session.name}}">{{session.name}}</a>! You are logged in.</p> <ul class="posts">
{% else %} {% for post in posts %}
<p><a href="/login">Login</a> or <a href="/register">Register</a></p> <li>
{% endif %} <h3>From {{ post.user.name }} in /{{ post.board.name }}/</h3>
<br> <h6>posted at {{ post.created_at }}</h6>
<h3>Stats:</h3> {% if post.reference %}
<ul class="stats"> <h6><b>ref post:</b> <a href="/posts/{{ post.reference.id }}">{{ post.reference.short_content if post.reference.id is not none else '[NOT FOUND]' }}</a></h6>
<li>Total Posts: {{ total_posts }}</li> {% endif %}
<li>Total Boards: {{ total_boards }}</li> <p>{{ post.short_content }}</p>
<li>Total Users: {{ total_users }}</li> <h6><a href="/posts/{{ post.id }}">View Post</a>
<li>Total Attachments: {{ total_attachments }}</li> {% if post.replies|length > 0 %}
({{ post.replies|length }} replies)
{% endif %}
{% if session.name == "SYSTEM" %}
| <a href="/delete/post/{{ post.id }}">Delete</a>
{% elif session.name == post.user.name %}
| <a href="/delete/post/{{ post.id }}">Delete</a>
{% endif %}
</h6>
</li>
{% endfor %}
{% if posts|length == 0 %}
<li>No posts found.</li>
{% endif %}
</ul> </ul>
{% endblock %} {% endblock %}

View File

@ -1,10 +1,6 @@
{% extends "templates/base.html" %} {% extends "base.html" %}
{% block head %} {% block content %}
<link rel="stylesheet" href="../static/css/form.css">
{% endblock %}
{% block main_content %}
<h1>Login</h1> <h1>Login</h1>
<p><a href="/register">Register</a> if you don't have an account.</p> <p><a href="/register">Register</a> if you don't have an account.</p>
<br> <br>

View File

@ -1,22 +0,0 @@
{% extends "templates/base.html" %}
{% block head %}
<link rel="stylesheet" href="../static/css/form.css">
{% endblock %}
{% block main_content %}
<h1>New Board</h1>
<br>
<form method="POST" action="/boards/new">
<label for="board_name">Board Name:</label>
<input type="text" id="name" name="name" required>
<br>
<label for="board_description">Board Description:</label>
<input type="text" id="description" name="description" required>
<br>
<button type="submit">Create Board</button>
</form>
{% if error %}
<p style="color: red;">{{ error }}</p>
{% endif %}
{% endblock %}

20
src/html/newpost.html Normal file
View File

@ -0,0 +1,20 @@
{% extends "base.html" %}
{% block content %}
<form method="POST" action="/post">
<label for="board">Board:</label>
<select id="board" name="board">
{% for board in boards %}
<option value="{{ board.id }}">{{ board.name }}</option>
{% endfor %}
</select>
<br>
<label for="content">Content:</label>
<textarea id="content" name="content" rows="4" cols="50" required></textarea>
<br>
<button type="submit">Post</button>
</form>
{% if error %}
<p style="color: red;">{{ error }}</p>
{% endif %}
{% endblock %}

View File

@ -1,75 +1,61 @@
{% extends "templates/base.html" %} {% extends "base.html" %}
{% block head %} {% block content %}
<link rel="stylesheet" href="../static/css/form.css"> <h2>From {{ post.user.name }} in /{{ post.board.name }}/</h2>
{% endblock %} <h6>posted at {{ post.created_at }}</h6>
{% if post.reference %}
{% block main_content %} <h6><b>ref post:</b> <a href="/posts/{{ post.reference.id }}">{{ post.reference.content }}</a></h6>
<h1><a href="/users/{{post.user.name}}" class="user-{{post.user.perms}}">{{post.user.name}}</a> posted in <a href="/boards/{{post.board.name}}" class="board-name">/{{post.board.name}}/</a></h1>
<h4>posted at <span class="time">{{post.created_at}}</span></h4>
{% if post.reference and post.reference.show %}
<h4>ref post: <a href="/posts/{{post.reference.id}}">{{post.reference.short_content}}</a></h4>
{% endif %} {% endif %}
{% if post.attachments %} <p>{{ post.content }}</p>
<h4>{{post.attachments|length}} attachments</h4> {% if session.name == "SYSTEM" %}
<div class="attachments"> <h6><a href="/delete/post/{{ post.id }}">Delete</a></h6>
{% for attachment in post.attachments %} {% elif session.name == post.user.name %}
{% if attachment.type == "image" %} <h6><a href="/delete/post/{{ post.id }}">Delete</a></h6>
<a href="{{attachment.url}}"><img src="{{attachment.url}}" alt="{{attachment.file_name}}" width="350px"></a>
{% elif attachment.type == "video" %}
<video width="350px" controls>
<source src="{{attachment.url}}" type="video/mp4">
Your browser does not support the video tag.
</video>
{% elif attachment.type == "audio" %}
<audio controls>
<source src="{{attachment.url}}" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
{% else %}
<a href="{{attachment.url}}">{{attachment.file_name}}</a>
{% endif %}
{% endfor %}
</div>
{% endif %}
<p>{{post.content}}</p>
<h6>
{% if session.perms == "admin" %}
<a href="/delete/post/{{ post.id }}">Delete</a>
{% elif session.name == post.user.name %}
<a href="/delete/post/{{ post.id }}">Delete</a>
{% endif %}
</h6>
{% if session.user_id %}
<br>
<h3>Reply to this post</h3>
<form action="/post" method="POST" enctype="multipart/form-data">
<input type="hidden" name="board_id" value="{{ post.board.id }}">
<input type="hidden" name="reference" value="{{ post.id }}">
<input type="hidden" name="type" value="comment">
<textarea name="content" placeholder="Content" required></textarea>
<input type="file" name="attachments" multiple>
<button type="submit">Post</button>
</form>
{% endif %} {% endif %}
<br> <br>
<h1>{{ post.replies }} Replies</h1>
<ul class="post-list"> <h3>Replies:</h3>
{% for post in replies %}
<ul class="posts">
{% for reply in post.replies %}
<li> <li>
{% include "templates/post.html" %} <h3>From {{ reply.user.name }} in /{{ reply.board.name }}/</h3>
<h6>posted at {{ reply.created_at }}</h6>
<p>{{ reply.short_content }}</p>
<h6><a href="/posts/{{ reply.id }}">View Reply</a>
{% if session.name == "SYSTEM" %}
| <a href="/delete/post/{{ reply.id }}">Delete</a>
{% elif session.name == post.user.name %}
| <a href="/delete/post/{{ reply.id }}">Delete</a>
{% endif %}
</h6>
</li> </li>
{% endfor %} {% endfor %}
{% if post.replies|length == 0 %}
<li>No replies found.</li>
{% endif %}
</ul> </ul>
{% if total_pages > 0 %} <div id="nav">
<div id="nav"> <h5>Page {{ page }}</h5>
<h5>Page {{ page }} of {{ total_pages }}</h5> {% if page > 1 %}
{% if page > 1 %} <h5><a href="/posts/{{ post.id }}?page={{ page - 1 }}">Previous Page</a></h5>
<h5><a href="/posts/{{ post.id }}?page={{ page - 1 }}">Previous Page</a></h5> {% endif %}
{% endif %}
{% if replies|length == 10 %} {% if post.replies|length == 10 %}
<h5><a href="/posts/{{ post.id }}?page={{ page + 1 }}">Next Page</a></h5> <h5><a href="/posts/{{ post.id }}?page={{ page + 1 }}">Next Page</a></h5>
{% endif %} {% endif %}
</div> </div>
<br>
<form method="POST" action="/post">
<input type="hidden" name="ref" value="{{ post.id }}">
<input type="hidden" name="board" value="{{ post.board.id }}">
<input type="hidden" name="redirect" value="/posts/{{ post.id }}">
<label for="content">Reply:</label>
<textarea id="content" name="content" rows="4" cols="50" required></textarea>
<br>
<button type="submit">Post</button>
</form>
{% if error %}
<p style="color: red;">{{ error }}</p>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -1,25 +1,42 @@
{% extends "templates/base.html" %} {% extends "base.html" %}
{% block main_content %} {% block content %}
<h1>Posts</h1> <h1>Posts</h1>
<p>Here are the latest posts from all boards.</p> <h5>Page {{ page }}</h5>
<br> <br>
<ul class="post-list"> <ul class="posts">
{% for post in posts %} {% for post in posts %}
<li> <li>
{% include "templates/short_post.html" %} <h3>From {{ post.user.name }} in /{{ post.board.name }}/</h3>
<h6>posted at {{ post.created_at }}</h6>
{% if post.reference %}
<h6><b>ref post:</b> <a href="/posts/{{ post.reference.id }}">{{ post.reference.short_content }}</a></h6>
{% endif %}
<p>{{ post.short_content }}</p>
<h6><a href="/posts/{{ post.id }}">View post</a>
{% if post.replies|length > 0 %}
({{ post.replies|length }} replies)
{% endif %}
{% if session.name == "SYSTEM" %}
| <a href="/delete/post/{{ post.id }}">Delete</a>
{% elif session.name == post.user.name %}
| <a href="/delete/post/{{ post.id }}">Delete</a>
{% endif %}
</h6>
</li> </li>
{% endfor %} {% endfor %}
{% if posts|length == 0 %}
<li>No replies found.</li>
{% endif %}
</ul> </ul>
{% if total_pages > 0 %} <div id="nav">
<div id="nav"> <h5>Page {{ page }}</h5>
<h5>Page {{ page }} of {{ total_pages }}</h5> {% if page > 1 %}
{% if page > 1 %} <h5><a href="/posts?page={{ page - 1 }}">Previous Page</a></h5>
<h5><a href="/posts?page={{ page - 1 }}">Previous Page</a></h5> {% endif %}
{% endif %}
{% if posts|length == 10 %} {% if posts|length == 10 %}
<h5><a href="/posts?page={{ page + 1 }}">Next Page</a></h5> <h5><a href="/posts?page={{ page + 1 }}">Next Page</a></h5>
{% endif %} {% endif %}
</div> </div>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -1,10 +1,6 @@
{% extends "templates/base.html" %} {% extends "base.html" %}
{% block head %} {% block content %}
<link rel="stylesheet" href="../static/css/form.css">
{% endblock %}
{% block main_content %}
<h1>Register</h1> <h1>Register</h1>
<br> <br>
<form method="POST" action="/register"> <form method="POST" action="/register">

View File

@ -1,52 +0,0 @@
{% extends "templates/base.html" %}
{% block head %}
<link rel="stylesheet" href="../static/css/form.css">
{% endblock %}
{% block main_content %}
<h1>Settings</h1>
<br>
<form method="POST" action="/settings">
<input type="hidden" name="type" value="username">
<label for="username">Change username:</label>
<input type="text" id="username" name="username" value="{{ session.name }}" required>
<button type="submit">Change username</button>
</form>
<br>
<form method="POST" action="/settings">
<input type="hidden" name="type" value="password">
<label for="old_password">Old password:</label>
<input type="password" id="old_password" name="old_password" required>
<label for="new_password">New password:</label>
<input type="password" id="new_password" name="new_password" required>
<button type="submit">Change password</button>
</form>
<br>
<form method="POST" action="/settings">
<input type="hidden" name="type" value="about">
<label for="about">About me:</label>
<textarea id="about" name="about" required>{{ session.about }}</textarea>
<button type="submit">Change about me</button>
</form>
<br>
<form method="POST" action="/settings" enctype="multipart/form-data">
<input type="hidden" name="type" value="avatar">
<label for="avatar">Change avatar:</label>
<input type="file" id="avatar" name="avatar" accept="image/png, image/jpeg, image/jpg, image/gif" required>
<button type="submit">Change avatar</button>
</form>
<br>
<form method="POST" action="/settings">
<input type="hidden" name="type" value="delete">
<label for="delete_account">Delete account:</label>
<input type="password" id="delete_account" name="delete_account" placeholder="Enter your password to confirm" required>
<button type="submit">Delete account</button>
</form>
{% if error %}
<p style="color: red;">{{ error }}</p>
{% endif %}
{% if success %}
<p style="color: green;">{{ success }}</p>
{% endif %}
{% endblock %}

View File

@ -1,29 +0,0 @@
<div class="post">
<h3><a href="/users/{{post.user.name}}" class="user-{{post.user.perms}}">{{post.user.name}}</a> posted in <a href="/boards/{{post.board.name}}" class="board-name">/{{post.board.name}}/</a></h3>
<h6>posted at <span class="time">{{post.created_at}}</span></h6>
{% if post.reference and post.reference.show %}
<h6>ref post: <a href="/posts/{{post.reference.id}}">{{post.reference.short_content}}</a></h6>
{% endif %}
{% if post.attachments %}
<h6>{{post.attachments|length}} attachments</h6>
<div class="attachments">
{% for attachment in post.attachments %}
{% if attachment.type == "image" %}
<a href="{{attachment.url}}"><img src="{{attachment.url}}" alt="{{attachment.file_name}}" width="100px"></a>
{% endif %}
{% endfor %}
</div>
{% endif %}
<p>{{post.short_content}}</p>
<h6>
<a href="/posts/{{ post.id }}">View Post</a>
{% if post.replies > 0 %}
({{ post.replies }} replies)
{% endif %}
{% if session.perms == "admin" %}
| <a href="/delete/post/{{ post.id }}">Delete</a>
{% elif session.name == post.user.name %}
| <a href="/delete/post/{{ post.id }}">Delete</a>
{% endif %}
</h6>
</div>

View File

@ -1,29 +0,0 @@
<div class="post">
<h3><a href="/users/{{post.user.name}}" class="user-{{post.user.perms}}">{{post.user.name}}</a> posted in <a href="/boards/{{post.board.name}}" class="board-name">/{{post.board.name}}/</a></h3>
<h6>posted at <span class="time">{{post.created_at}}</span></h6>
{% if post.reference and post.reference.show %}
<h6>ref post: <a href="/post/{{post.reference.url}}">{{post.reference.short_content}}</a></h6>
{% endif %}
{% if post.attachments %}
<h6>{{post.attachments|length}} attachments</h6>
<div class="attachments">
{% for attachment in post.attachments %}
{% if attachment.type == "image" %}
<a href="{{attachment.url}}"><img src="{{attachment.url}}" alt="{{attachment.file_name}}" width="100px"></a>
{% endif %}
{% endfor %}
</div>
{% endif %}
<p>{{post.short_content}}</p>
<h6>
<a href="/posts/{{ post.id }}">View Post</a>
{% if post.replies > 0 %}
({{ post.replies }} replies)
{% endif %}
{% if session.perms == "admin" %}
| <a href="/delete/post/{{ post.id }}">Delete</a>
{% elif session.name == post.user.name %}
| <a href="/delete/post/{{ post.id }}">Delete</a>
{% endif %}
</h6>
</div>

View File

@ -1,40 +1,43 @@
{% extends "templates/base.html" %} {% extends "base.html" %}
{% block head %} {% block content %}
<link rel="stylesheet" href="../static/css/user.css"> <h1>{{ user.name }}</h1>
{% endblock %} <h6>Joined at: {{ user.created_at }}</h6>
{% block main_content %}
<div class="user">
{% if user.avatar_url %}
<img src="{{user.avatar_url}}" alt="Avatar">
{% else %}
<img src="/static/content/default_avatar.png" alt="Default Avatar">
{% endif %}
<div class="info">
<h1 class="user-{{user.perms}}">{{user.name}}</h1>
<h4>Joined at <span class="time">{{user.created_at}}</span></h4>
</div>
</div>
<p>{{user.about}}</p>
<br> <br>
<h2><span class="user-{{user.perms}}">{{ user.name }}'s</span> Posts</h2> <h1>Posts:</h1>
<ul class="post-list"> <ul class="posts">
{% for post in posts %} {% for post in user.posts %}
<li> <li>
{% include "templates/short_post.html" %} <h3>From {{ post.user.name }} in /{{ post.board.name }}/</h3>
<h6>posted at {{ post.created_at }}</h6>
{% if post.reference %}
<h6><b>ref post:</b> <a href="/posts/{{ post.reference.id }}">{{ post.reference.content }}</a></h6>
{% endif %}
<p>{{ post.content }}</p>
<h6><a href="/posts/{{ post.id }}">View Post</a>
{% if post.replies|length > 0 %}
({{ post.replies|length }} replies)
{% endif %}
{% if session.name == "SYSTEM" %}
| <a href="/delete/post/{{ post.id }}">Delete</a>
{% elif session.name == post.user.name %}
| <a href="/delete/post/{{ post.id }}">Delete</a>
{% endif %}
</h6>
</li> </li>
{% endfor %} {% endfor %}
{% if user.posts|length == 0 %}
<li>No posts found.</li>
{% endif %}
</ul> </ul>
{% if total_pages > 0 %} <div id="nav">
<div id="nav"> <h5>Page {{ page }}</h5>
<h5>Page {{ page }} of {{ total_pages }}</h5> {% if page > 1 %}
{% if page > 1 %} <h5><a href="/user/{{ user.name }}?page={{ page - 1 }}">Previous Page</a></h5>
<h5><a href="/users/{{user.d}}?page={{ page - 1 }}">Previous Page</a></h5> {% endif %}
{% endif %}
{% if posts|length == 10 %} {% if user.posts|length == 10 %}
<h5><a href="/users/{{user.id}}?page={{ page + 1 }}">Next Page</a></h5> <h5><a href="/user/{{ user.name }}?page={{ page + 1 }}">Next Page</a></h5>
{% endif %} {% endif %}
</div> </div>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -1,39 +0,0 @@
{% extends "templates/base.html" %}
{% block head %}
<link rel="stylesheet" href="../static/css/user.css">
{% endblock %}
{% block main_content %}
<h1>Users</h1>
<p>Here are the boards users on this site.</p>
<br>
<ul class="board-list">
{% for user in users %}
<li>
<a href="/users/{{user.name}}" class="user">
{% if user.avatar_url %}
<img src="{{user.avatar_url}}" alt="Avatar">
{% else %}
<img src="/static/content/default_avatar.png" alt="Default Avatar">
{% endif %}
<div class="info">
<h1 class="user-{{user.perms}}">{{user.name}}</h1>
<h4>Joined at <span class="time">{{user.created_at}}</span></h4>
</div>
</a>
</li>
{% endfor %}
</ul>
{% if total_pages > 0 %}
<div id="nav">
<h5>Page {{ page }} of {{ total_pages }}</h5>
{% if page > 1 %}
<h5><a href="/users?page={{ page - 1 }}">Previous Page</a></h5>
{% endif %}
{% if boards|length == 10 %}
<h5><a href="/users?page={{ page + 1 }}">Next Page</a></h5>
{% endif %}
</div>
{% endif %}
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@ -2,10 +2,6 @@
--bg: #000; --bg: #000;
--fg: #fff; --fg: #fff;
--accent: #7139f3; --accent: #7139f3;
--admin: #39f3da;
--user: #5b6dd4;
--board: #515699;
--time: #30597a;
} }
body { body {
@ -87,9 +83,96 @@ a {
text-decoration: none; text-decoration: none;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
a:hover { a:hover {
opacity: 0.6; color: var(--fg);
}
ul.posts {
list-style: none;
padding: 0;
margin: 0;
margin-left: 10px;
display: flex;
flex-direction: column;
gap: 10px;
}
form {
display: flex;
flex-direction: column;
gap: 10px;
}
input[type="text"],
input[type="password"] {
padding: 10px;
border: 1px solid var(--fg);
border-radius: 5px;
box-sizing: border-box;
background-color: var(--bg);
color: var(--fg);
font-size: 1rem;
width: 300px;
}
input[type="text"]:focus,
input[type="password"]:focus {
outline: none;
border-color: var(--accent);
}
button[type="submit"] {
padding: 10px 20px;
border: none;
border-radius: 5px;
background-color: var(--accent);
color: var(--bg);
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s ease;
width: 300px;
font-weight: 600;
}
button[type="submit"]:hover {
background-color: var(--fg);
color: var(--bg);
}
button[type="submit"]:disabled {
background-color: var(--bg);
color: var(--fg);
cursor: not-allowed;
}
select {
padding: 10px;
border: 1px solid var(--fg);
border-radius: 5px;
background-color: var(--bg);
color: var(--fg);
font-size: 1rem;
width: 300px;
}
select:focus {
outline: none;
border-color: var(--accent);
}
textarea {
padding: 10px;
border: 1px solid var(--fg);
border-radius: 5px;
background-color: var(--bg);
color: var(--fg);
font-size: 1rem;
width: 300px;
height: 100px;
}
textarea:focus {
outline: none;
border-color: var(--accent);
} }
div#nav { div#nav {
@ -98,51 +181,6 @@ div#nav {
margin-top: 20px; margin-top: 20px;
} }
ul.board-list { p {
list-style: none; white-space:pre;
padding: 0;
margin: 0;
margin-left: 10px;
display: flex;
flex-direction: column;
gap: 10px;
}
ul.post-list {
list-style: none;
padding: 0;
margin: 0;
margin-left: 10px;
display: flex;
flex-direction: column;
gap: 10px;
}
.user-admin {
color: var(--admin);
}
.user-user {
color: var(--user);
}
.board-name {
color: var(--board);
}
.time {
color: var(--time);
}
#title {
background: linear-gradient(90deg, var(--accent), var(--admin));
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.attachments {
display: flex;
gap: 10px;
align-items: center;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 387 KiB

View File

@ -1,89 +0,0 @@
form {
display: flex;
flex-direction: column;
gap: 10px;
}
input[type="text"],
input[type="password"] {
padding: 10px;
border: 1px solid var(--fg);
border-radius: 5px;
box-sizing: border-box;
background-color: var(--bg);
color: var(--fg);
font-size: 1rem;
width: 300px;
}
input[type="text"]:focus,
input[type="password"]:focus {
outline: none;
border-color: var(--accent);
}
button[type="submit"] {
padding: 10px 20px;
border: none;
border-radius: 5px;
background: linear-gradient(90deg, var(--accent), var(--admin));
color: var(--bg);
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
width: 300px;
font-weight: 600;
}
button[type="submit"]:hover {
transform: scale(1.05);
}
button[type="submit"]:disabled {
background-color: var(--bg);
color: var(--fg);
cursor: not-allowed;
}
select {
padding: 10px;
border: 1px solid var(--fg);
border-radius: 5px;
background-color: var(--bg);
color: var(--fg);
font-size: 1rem;
width: 300px;
}
select:focus {
outline: none;
border-color: var(--accent);
}
textarea {
padding: 10px;
border: 1px solid var(--fg);
border-radius: 5px;
background-color: var(--bg);
color: var(--fg);
font-size: 1rem;
width: 300px;
height: 150px;
box-sizing: border-box;
}
textarea:focus {
outline: none;
border-color: var(--accent);
}
input[type="file"] {
padding: 10px;
border: 1px solid var(--fg);
border-radius: 5px;
background-color: var(--bg);
color: var(--fg);
font-size: 1rem;
width: 300px;
cursor: pointer;
box-sizing: border-box;
}

View File

@ -1,7 +0,0 @@
ul.stats {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
}

View File

@ -1,27 +0,0 @@
.user {
display: flex;
gap: 20px;
align-items: center;
border: 1px solid var(--fg);
width: fit-content;
max-width: 100%;
padding: 10px;
margin-bottom: 10px;
}
.user img {
height: 80px;
max-width: 300px;
}
a.user {
text-decoration: none;
color: var(--fg);
transition: all 0.3s ease;
}
a.user:hover {
opacity: 1;
transform: scale(1.05);
border-color: var(--accent);
}

View File

@ -1,145 +0,0 @@
import logging, re, database, datetime, os
from dateutil import tz
log = logging.getLogger(__name__)
STRING = re.compile(r'^[\w ]+$')
STRING_NO_SPACE = re.compile(r'^[\w]+$')
TZ = os.getenv('TZ', 'UTC')
def convert_utc_to_local(utc_dt:str, local_tz:str) -> str:
utc_dt = datetime.datetime.strptime(utc_dt, '%Y-%m-%d %H:%M:%S')
log.debug(f"Converting UTC datetime {utc_dt} to local timezone {local_tz}")
if not isinstance(utc_dt, datetime.datetime):
raise ValueError("utc_dt must be a datetime object")
if not isinstance(local_tz, str):
raise ValueError("local_tz must be a string")
local_tz = tz.gettz(local_tz)
if local_tz is None:
raise ValueError(f"Invalid timezone: {local_tz}")
local_dt = utc_dt.replace(tzinfo=tz.tzutc()).astimezone(local_tz)
log.debug(f"Converted datetime: {local_dt}")
return local_dt.strftime('%Y-%m-%d %H:%M:%S')
def validate_string(input_string:str, allowed_regex:re.Pattern=STRING, max_length:int=None, min_length:int=None) -> bool:
if max_length and len(input_string) > max_length:
return False
if min_length and len(input_string) < min_length:
return False
if not re.match(allowed_regex, input_string):
return False
return True
def validate_board_name(board_name:str) -> bool:
log.debug(f"Validating board name: {board_name}")
return validate_string(board_name, allowed_regex=STRING_NO_SPACE, max_length=20, min_length=3)
def validate_username(username:str) -> bool:
log.debug(f"Validating username: {username}")
return validate_string(username, allowed_regex=STRING_NO_SPACE, max_length=20, min_length=3)
class data_converter:
def __init__(self, db:database.Database):
self.db = db
def board_to_dict(self, board):
return {
'id': board[0],
'name': board[1],
'description': board[2],
'created_at': convert_utc_to_local(board[3], 'Europe/London'),
'owner_id': board[4]
}
def post_to_dict(self, post, show_reference:bool=True):
log.debug(f"Converting post with id ({post[0]}) to dict")
data = {
'id': post[0],
'content': post[3],
'short_content': post[3][:50] + '...' if len(post[3]) > 50 else post[3],
'created_at': convert_utc_to_local(post[6], 'Europe/London'),
'type': post[5],
}
# Get the user who created the post
user = self.db.execute_query("SELECT * FROM users WHERE id = ?", (post[1],), fetch_type=database.FETCHONE)
if user:
data['user'] = {
'id': user[0],
'name': user[1],
'about': user[3],
'avatar_url': '/users/' + str(user[0]) + '/avatar' if user[5] else None,
'perms': user[4],
}
else:
data['user'] = None
# Get the board where the post was created
board = self.db.execute_query("SELECT * FROM boards WHERE id = ?", (post[2],), fetch_type=database.FETCHONE)
if board:
data['board'] = {
'id': board[0],
'name': board[1],
'description': board[2],
'created_at': convert_utc_to_local(board[3], 'Europe/London'),
'owner_id': board[4]
}
else:
data['board'] = None
# Get the reference post if it exists
if post[4]:
reference_post = self.db.execute_query("SELECT * FROM posts WHERE id = ?", (post[4],), fetch_type=database.FETCHONE)
if reference_post:
data['reference'] = {
'id': reference_post[0],
'content': reference_post[3],
'short_content': reference_post[3][:25] + '...' if len(reference_post[3]) > 25 else reference_post[3],
'created_at': convert_utc_to_local(reference_post[6], 'Europe/London'),
'type': reference_post[5],
'show': show_reference
}
else:
data['reference'] = None
else:
data['reference'] = None
# Get the attachments for the post
attachments = self.db.execute_query("SELECT * FROM attachments WHERE post_id = ?", (post[0],), fetch_type=database.FETCHALL)
data['attachments'] = []
for attachment in attachments:
data['attachments'].append({
'id': attachment[0],
'file_name': attachment[3],
'created_at': convert_utc_to_local(attachment[5], 'Europe/London'),
'url': '/attachment/' + str(attachment[0]),
'type': attachment[4].split('/')[0],
'mime_type': attachment[4]
})
# Get the number of replies to the post
replies = self.db.execute_query("SELECT COUNT(*) FROM posts WHERE reference = ?", (post[0],), fetch_type=database.FETCHONE)
data['replies'] = replies[0] if replies else 0
log.debug(f"Post converted to dict")
return data
def user_to_dict(self, user):
return {
'id': user[0],
'name': user[1],
'about': user[3],
'avatar_url': '/users/' + str(user[0]) + '/avatar' if user[5] else None,
'perms': user[4],
'created_at': convert_utc_to_local(user[6], 'Europe/London'),
'avatar_type': user[8],
}