v2 overhaul

This commit is contained in:
Alfie King 2025-04-25 00:33:14 +01:00
parent e9dc5e5056
commit ea2e8b75cc
27 changed files with 1513 additions and 1202 deletions

7
.gitignore vendored
View File

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

View File

@ -10,7 +10,7 @@ COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# Copy the rest of the application code into the container # Copy the rest of the application code into the container
COPY src/ ./src COPY src src
# Expose the port the app runs on # Expose the port the app runs on
EXPOSE 5000 EXPOSE 5000

View File

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

View File

@ -1,581 +1,185 @@
import sqlite3, logging, os import os, hashlib, sqlite3, logging
from os import getenv as env
import mysql.connector
# Configure logging log = logging.getLogger(__name__)
logger = logging.getLogger(__name__) FETCHALL = 0
FETCHONE = 1
class Database: class Database:
def __init__(self, db_name): def __init__(self):
# Initialize the database connection self.connection = None
logger.info("Initializing database connection...") self.cursor = None
self.connection = sqlite3.connect(db_name, check_same_thread=False)
def connect_sqlite(self, db_path):
try:
self.connection = sqlite3.connect(db_path, check_same_thread=False)
self.cursor = self.connection.cursor() self.cursor = self.connection.cursor()
logger.info("Database connection established.") log.info("Connected to SQLite database")
except sqlite3.Error as e:
log.error(f"SQLite connection error: {e}")
raise
# Create users table if it doesn't exist def connect_mysql(self):
logger.info("Creating users table if it doesn't exist...") try:
self.cursor.execute(''' self.connection = mysql.connector.connect(
CREATE TABLE IF NOT EXISTS users ( 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")
except mysql.connector.Error as e:
log.error(f"MySQL connection error: {e}")
raise
def close(self):
if self.cursor:
self.cursor.close()
if self.connection:
self.connection.close()
log.warning("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):
log.info("First time run detected, initializing database")
# Create users table
log.info("Creating users table")
db.execute_query("""
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE, username TEXT NOT NULL,
password TEXT NOT NULL, password TEXT NOT NULL,
session TEXT, about TEXT DEFAULT 'No description',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP permissions TEXT DEFAULT 'user',
) avatar MEDIUMBLOB,
''')
logger.info("Users table created.")
# Create boards table if it doesn't exist
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, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (owner_id) REFERENCES users (id) ON DELETE SET NULL token TEXT,
avatar_type TEXT
) )
''') """)
logger.info("Boards table created.")
# Create posts table if it doesn't exist # Create posts table
logger.info("Creating posts table if it doesn't exist...") log.info("Creating posts table")
self.cursor.execute(''' db.execute_query("""
CREATE TABLE IF NOT EXISTS posts ( CREATE TABLE posts (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
board_id INTEGER NOT NULL, board_id INTEGER NOT NULL,
content TEXT NOT NULL, content TEXT NOT NULL,
reference INTREGER,
type TEXT DEFAULT 'post',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
refence INTEGER, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (refence) REFERENCES posts (id) ON DELETE CASCADE, FOREIGN KEY (board_id) REFERENCES boards (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL, FOREIGN KEY (reference) REFERENCES posts (id) ON DELETE SET NULL
FOREIGN KEY (board_id) REFERENCES boards (id) ON DELETE CASCADE
) )
''') """)
logger.info("Posts table created.")
# Create boards table
log.info("Creating boards table")
db.execute_query("""
CREATE TABLE boards (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
owner_id INTEGER NOT NULL,
FOREIGN KEY (owner_id) REFERENCES users (id)
)
""")
def close(self): # Create attachments table
# Close the database connection db.execute_query("""
logger.info("Closing database connection...") CREATE TABLE attachments (
self.connection.commit() id INTEGER PRIMARY KEY AUTOINCREMENT,
self.connection.close() post_id INTEGER NOT NULL,
logger.info("Database connection closed.") 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
def create_user(self, username, password): if not env('SYSTEM_PASSWORD'):
logger.info(f"Creating user: {username}") log.warning("SYSTEM_PASSWORD is empty, generating random password")
password = os.urandom(16).hex()
# Check if the user already exists log.info(f"Generated system user password: {password}")
logger.info(f"Checking if user {username} already exists...")
self.cursor.execute('''
SELECT * FROM users WHERE username = ?
''', (username,))
user = self.cursor.fetchone()
if user:
logger.warning(f"User {username} already exists.")
return False
logger.info(f"User {username} does not exist.")
# Create a new user
self.cursor.execute('''
INSERT INTO users (username, password)
VALUES (?, ?)
''', (username, password))
self.connection.commit()
logger.info(f"User {username} created.")
return True
def get_user(self, username):
# Get user by username
logger.info(f"Getting user {username}...")
self.cursor.execute('''
SELECT * FROM users WHERE username = ?
''', (username,))
user = self.cursor.fetchone()
if user:
logger.info(f"User {username} found.")
return user
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 (?, ?, ?)
''', (name, owner_id, description))
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: else:
logger.info("No reference post provided.") password = env('SYSTEM_PASSWORD')
# Create a new post log.info("Creating system user")
self.cursor.execute(''' db.execute_query("""
INSERT INTO posts (user_id, board_id, content, refence) INSERT INTO users (username, password, about, permissions)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?)
''', (user_id, board_id, content, ref)) """, (
self.connection.commit() env('SYSTEM_USER', default='system'),
logger.info(f"Post created for user {user_id} in board {board_id}.") env('SYSTEM_PASSWORD', default=hashlib.sha256(password.encode()).hexdigest()),
post_id = self.cursor.lastrowid env('SYSTEM_ABOUT', default='System User'),
logger.info(f"Post ID: {post_id}") env('SYSTEM_PERMISSIONS', default='admin')
return post_id ))
# Create system boards
def get_post(self, post_id): boards_names = env('SYSTEM_BOARDS', default='General,Random,System').split(',')
# Get post by ID if "System" not in boards_names:
logger.info(f"Getting post {post_id}...") boards_names.append("System")
self.cursor.execute('''
SELECT * FROM posts WHERE id = ? log.info(f"Creating system boards: {', '.join(boards_names)}")
''', (post_id,)) for board_name in boards_names:
post = self.cursor.fetchone() db.execute_query("""
INSERT INTO boards (name, description, owner_id)
if post: VALUES (?, ?, ?)
logger.info(f"Post {post_id} found.") """, (
return post board_name,
logger.warning(f"Post {post_id} not found.") f"This is a automatically created board for {board_name}",
return None 1
))
def get_post_references(self, post_id, limit=10, offset=0): log.info("First time run completed")
# 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

@ -1,44 +1,42 @@
{% extends "base.html" %} {% extends "templates/base.html" %}
{% block content %} {% block head %}
<h1>{{ board.name }}</h1> <link rel="stylesheet" href="../static/css/form.css">
<h6>Created at: {{ board.created_at }}</h6> {% endblock %}
{% 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>
{% endif %}
{% if session.user_id %}
<br> <br>
<h1>Posts:</h1> <h3>Post to this board</h3>
<ul class="posts"> <form action="/post" method="POST" enctype="multipart/form-data">
{% for post in board.posts %} <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>
<ul class="post-list">
{% for post in posts %}
<li> <li>
<h3>From {{ post.user.name }}</h3> {% include "templates/short_post.html" %}
<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 "base.html" %} {% extends "templates/base.html" %}
{% block content %} {% block head %}
<link rel="stylesheet" href="../static/css/boards.css">
{% endblock %}
{% block main_content %}
<h1>Boards</h1> <h1>Boards</h1>
<h5>Page {{ page }}</h5> <p>Here are the boards available on this site. To create a new board, click <a href="/boards/new">here</a>.</p>
<br> <br>
<ul class="posts"> <ul class="board-list">
{% for board in boards %} {% for board in boards %}
<li> <li>
<h3>/{{ board.name }}/</h3> <h3><a class="board-name" href="/boards/{{ board.name }}">/{{ board.name }}/</a></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 %}

9
src/html/error.html Normal file
View File

@ -0,0 +1,9 @@
{% 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,33 +1,24 @@
{% extends "base.html" %} {% extends "templates/base.html" %}
{% block content %} {% block head %}
<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 textboard</p> <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>
<br> <br>
<h1>Latest Posts:</h1> {% if session.name %}
<ul class="posts"> <p>Hello <a class="user-{{session.perms}}" href="/users/{{session.name}}">{{session.name}}</a>! You are logged in.</p>
{% for post in posts %} {% else %}
<li> <p><a href="/login">Login</a> or <a href="/register">Register</a></p>
<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 if post.reference.id is not none else '[NOT FOUND]' }}</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>
{% endfor %}
{% if posts|length == 0 %}
<li>No posts found.</li>
{% endif %} {% endif %}
<br>
<h3>Stats:</h3>
<ul class="stats">
<li>Total Posts: {{ total_posts }}</li>
<li>Total Boards: {{ total_boards }}</li>
<li>Total Users: {{ total_users }}</li>
<li>Total Attachments: {{ total_attachments }}</li>
</ul> </ul>
{% endblock %} {% endblock %}

View File

@ -1,6 +1,10 @@
{% extends "base.html" %} {% extends "templates/base.html" %}
{% block content %} {% block head %}
<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>

22
src/html/new_board.html Normal file
View File

@ -0,0 +1,22 @@
{% 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 %}

View File

@ -1,20 +0,0 @@
{% 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,61 +1,75 @@
{% extends "base.html" %} {% extends "templates/base.html" %}
{% block content %} {% block head %}
<h2>From {{ post.user.name }} in /{{ post.board.name }}/</h2> <link rel="stylesheet" href="../static/css/form.css">
<h6>posted at {{ post.created_at }}</h6> {% endblock %}
{% if post.reference %}
<h6><b>ref post:</b> <a href="/posts/{{ post.reference.id }}">{{ post.reference.content }}</a></h6> {% block main_content %}
<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 %}
{% if post.attachments %}
<h4>{{post.attachments|length}} attachments</h4>
<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="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 %} {% endif %}
<p>{{post.content}}</p> <p>{{post.content}}</p>
<h6>
{% if session.name == "SYSTEM" %} {% if session.name == "SYSTEM" %}
<h6><a href="/delete/post/{{ post.id }}">Delete</a></h6> <a href="/delete/post/{{ post.id }}">Delete</a>
{% elif session.name == post.user.name %} {% elif session.name == post.user.name %}
<h6><a href="/delete/post/{{ post.id }}">Delete</a></h6> <a href="/delete/post/{{ post.id }}">Delete</a>
{% endif %}
<br>
<h3>Replies:</h3>
<ul class="posts">
{% for reply in post.replies %}
<li>
<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 %} {% endif %}
</h6> </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 %}
<br>
<h1>{{ post.replies }} Replies</h1>
<ul class="post-list">
{% for post in replies %}
<li>
{% include "templates/post.html" %}
</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,42 +1,25 @@
{% extends "base.html" %} {% extends "templates/base.html" %}
{% block content %} {% block main_content %}
<h1>Posts</h1> <h1>Posts</h1>
<h5>Page {{ page }}</h5> <p>Here are the latest posts from all boards.</p>
<br> <br>
<ul class="posts"> <ul class="post-list">
{% for post in posts %} {% for post in posts %}
<li> <li>
<h3>From {{ post.user.name }} in /{{ post.board.name }}/</h3> {% include "templates/short_post.html" %}
<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,6 +1,10 @@
{% extends "base.html" %} {% extends "templates/base.html" %}
{% block content %} {% block head %}
<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">

52
src/html/settings.html Normal file
View File

@ -0,0 +1,52 @@
{% 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

@ -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>
<link rel="stylesheet" href="../static/base.css"> <meta name="author" content="Alfie King">
<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: 1.0</h2> <h2>ver: 2.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>
{% if session.name %} <li><a href="/users">Users</a></li>
<li><a href="/post">Post</a></li>
{% endif %}
<li>{% if session.name %} <li>{% if session.name %}
<a href="/user/{{ session.name }}">{{ session.name }}</a> <a class="user-{{session.perms}}" href="/users/{{ 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 content %}{% endblock %} {% block main_content %}{% endblock %}
</main> </main>
<footer> <footer>
<p>Created by <a href="https://alfieking.dev">Alfie King</a>{% if session.name %} | <a href="/logout">Logout</a>{% endif %}</p> <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>
</footer> </footer>
</body> </body>
</html> </html>

View File

@ -0,0 +1,29 @@
<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.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>
</div>

View File

@ -0,0 +1,29 @@
<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.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>
</div>

View File

@ -1,43 +1,40 @@
{% extends "base.html" %} {% extends "templates/base.html" %}
{% block content %} {% block head %}
<h1>{{ user.name }}</h1> <link rel="stylesheet" href="../static/css/user.css">
<h6>Joined at: {{ user.created_at }}</h6> {% endblock %}
{% 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>
<h1>Posts:</h1> <h2><span class="user-{{user.perms}}">{{ user.name }}'s</span> Posts</h2>
<ul class="posts"> <ul class="post-list">
{% for post in user.posts %} {% for post in posts %}
<li> <li>
<h3>From {{ post.user.name }} in /{{ post.board.name }}/</h3> {% include "templates/short_post.html" %}
<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 %}

39
src/html/users.html Normal file
View File

@ -0,0 +1,39 @@
{% 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

View File

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

89
src/static/css/form.css Normal file
View File

@ -0,0 +1,89 @@
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;
}

7
src/static/css/index.css Normal file
View File

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

27
src/static/css/user.css Normal file
View File

@ -0,0 +1,27 @@
.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);
}

145
src/utils.py Normal file
View File

@ -0,0 +1,145 @@
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],
}