Merge pull request 'v2-overhaul' (#1) from v2-overhaul into main

Reviewed-on: #1
This commit is contained in:
Alfie King 2025-04-24 23:41:09 +00:00
commit 439975ce96
28 changed files with 1515 additions and 1204 deletions

7
.gitignore vendored
View File

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

View File

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

View File

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

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

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
logger = logging.getLogger(__name__)
log = logging.getLogger(__name__)
FETCHALL = 0
FETCHONE = 1
class Database:
def __init__(self, db_name):
# Initialize the database connection
logger.info("Initializing database connection...")
self.connection = sqlite3.connect(db_name, check_same_thread=False)
def __init__(self):
self.connection = None
self.cursor = None
def connect_sqlite(self, db_path):
try:
self.connection = sqlite3.connect(db_path, check_same_thread=False)
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
logger.info("Creating users table if it doesn't exist...")
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
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")
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,
username TEXT NOT NULL UNIQUE,
username TEXT NOT NULL,
password TEXT NOT NULL,
session TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
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,
about TEXT DEFAULT 'No description',
permissions TEXT DEFAULT 'user',
avatar MEDIUMBLOB,
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
logger.info("Creating posts table if it doesn't exist...")
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS posts (
# Create posts table
log.info("Creating posts table")
db.execute_query("""
CREATE TABLE posts (
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,
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
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
)
''')
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):
# Close the database connection
logger.info("Closing database connection...")
self.connection.commit()
self.connection.close()
logger.info("Database connection closed.")
# Create attachments table
db.execute_query("""
CREATE TABLE attachments (
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
def create_user(self, username, password):
logger.info(f"Creating user: {username}")
# Check if the user already exists
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.")
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:
logger.info("No reference post provided.")
password = env('SYSTEM_PASSWORD')
# Create a new post
self.cursor.execute('''
INSERT INTO posts (user_id, board_id, content, refence)
log.info("Creating system user")
db.execute_query("""
INSERT INTO users (username, password, about, permissions)
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
""", (
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
boards_names = env('SYSTEM_BOARDS', default='General,Random,System').split(',')
if "System" not in boards_names:
boards_names.append("System")
log.info(f"Creating system boards: {', '.join(boards_names)}")
for board_name in boards_names:
db.execute_query("""
INSERT INTO boards (name, description, owner_id)
VALUES (?, ?, ?)
""", (
board_name,
f"This is a automatically created board for {board_name}",
1
))
log.info("First time run completed")

View File

@ -1,44 +1,42 @@
{% extends "base.html" %}
{% extends "templates/base.html" %}
{% block content %}
<h1>{{ board.name }}</h1>
<h6>Created at: {{ board.created_at }}</h6>
{% block head %}
<link rel="stylesheet" href="../static/css/form.css">
{% endblock %}
{% block main_content %}
<h1 class="board-name">/{{ board.name }}/</h1>
<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>
<h1>Posts:</h1>
<ul class="posts">
{% for post in board.posts %}
<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>
<ul class="post-list">
{% for post in posts %}
<li>
<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>
{% include "templates/short_post.html" %}
</li>
{% endfor %}
{% if board.posts|length == 0 %}
<li>No posts found.</li>
{% endif %}
</ul>
{% if total_pages > 0 %}
<div id="nav">
<h5>Page {{ page }}</h5>
<h5>Page {{ page }} of {{ total_pages }}</h5>
{% if page > 1 %}
<h5><a href="/boards/{{ board.name }}?page={{ page - 1 }}">Previous Page</a></h5>
{% endif %}
{% if board.posts|length == 10 %}
{% if posts|length == 10 %}
<h5><a href="/boards/{{ board.name }}?page={{ page + 1 }}">Next Page</a></h5>
{% endif %}
</div>
{% endif %}
{% 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>
<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>
<ul class="posts">
<ul class="board-list">
{% for board in boards %}
<li>
<h3>/{{ board.name }}/</h3>
<h6>created at {{ board.created_at }}</h6>
<h3><a class="board-name" href="/boards/{{ board.name }}">/{{ board.name }}/</a></h3>
<p>{{ board.description }}</p>
<h6><a href="/boards/{{ board.name }}">View Board</a></h6>
</li>
{% endfor %}
{% if boards|length == 0 %}
<li>No boards found.</li>
{% endif %}
</ul>
{% if total_pages > 0 %}
<div id="nav">
<h5>Page {{ page }}</h5>
<h5>Page {{ page }} of {{ total_pages }}</h5>
{% if page > 1 %}
<h5><a href="/boards?page={{ page - 1 }}">Previous Page</a></h5>
{% endif %}
{% if boards|length == 10 %}
<h5><a href="/boards?page={{ page + 1 }}">Next Page</a></h5>
{% endif %}
</div>
{% endif %}
{% 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>
<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>
<h1>Latest Posts:</h1>
<ul class="posts">
{% for post in posts %}
<li>
<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>
{% if session.name %}
<p>Hello <a class="user-{{session.perms}}" href="/users/{{session.name}}">{{session.name}}</a>! You are logged in.</p>
{% else %}
<p><a href="/login">Login</a> or <a href="/register">Register</a></p>
{% 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>
{% 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>
<p><a href="/register">Register</a> if you don't have an account.</p>
<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 %}
<h2>From {{ post.user.name }} in /{{ post.board.name }}/</h2>
<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>
{% block head %}
<link rel="stylesheet" href="../static/css/form.css">
{% endblock %}
{% 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 %}
<p>{{ post.content }}</p>
{% if session.name == "SYSTEM" %}
<h6><a href="/delete/post/{{ post.id }}">Delete</a></h6>
{% elif session.name == post.user.name %}
<h6><a href="/delete/post/{{ post.id }}">Delete</a></h6>
{% 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 %}
<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>
{% endfor %}
</div>
{% endif %}
<p>{{post.content}}</p>
<h6>
{% if session.name == "SYSTEM" %}
| <a href="/delete/post/{{ reply.id }}">Delete</a>
<a href="/delete/post/{{ post.id }}">Delete</a>
{% elif session.name == post.user.name %}
| <a href="/delete/post/{{ reply.id }}">Delete</a>
<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 %}
<br>
<h1>{{ post.replies }} Replies</h1>
<ul class="post-list">
{% for post in replies %}
<li>
{% include "templates/post.html" %}
</li>
{% endfor %}
{% if post.replies|length == 0 %}
<li>No replies found.</li>
{% endif %}
</ul>
{% if total_pages > 0 %}
<div id="nav">
<h5>Page {{ page }}</h5>
<h5>Page {{ page }} of {{ total_pages }}</h5>
{% if page > 1 %}
<h5><a href="/posts/{{ post.id }}?page={{ page - 1 }}">Previous Page</a></h5>
{% endif %}
{% if post.replies|length == 10 %}
{% if replies|length == 10 %}
<h5><a href="/posts/{{ post.id }}?page={{ page + 1 }}">Next Page</a></h5>
{% endif %}
</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 %}
{% endblock %}

View File

@ -1,42 +1,25 @@
{% extends "base.html" %}
{% extends "templates/base.html" %}
{% block content %}
{% block main_content %}
<h1>Posts</h1>
<h5>Page {{ page }}</h5>
<p>Here are the latest posts from all boards.</p>
<br>
<ul class="posts">
<ul class="post-list">
{% for post in posts %}
<li>
<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>
{% include "templates/short_post.html" %}
</li>
{% endfor %}
{% if posts|length == 0 %}
<li>No replies found.</li>
{% endif %}
</ul>
{% if total_pages > 0 %}
<div id="nav">
<h5>Page {{ page }}</h5>
<h5>Page {{ page }} of {{ total_pages }}</h5>
{% if page > 1 %}
<h5><a href="/posts?page={{ page - 1 }}">Previous Page</a></h5>
{% endif %}
{% if posts|length == 10 %}
<h5><a href="/posts?page={{ page + 1 }}">Next Page</a></h5>
{% endif %}
</div>
{% endif %}
{% 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>
<br>
<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 name="viewport" content="width=device-width, initial-scale=1.0">
<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>
<body>
<header>
<div id="title">
<h1>Prismic</h1>
<h2>ver: 1.0</h2>
<h2>ver: 2.0</h2>
</div>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/boards">Boards</a></li>
<li><a href="/posts">Posts</a></li>
{% if session.name %}
<li><a href="/post">Post</a></li>
{% endif %}
<li><a href="/users">Users</a></li>
<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 %}
<a href="/login">Login</a>
{% endif %}</li>
@ -30,10 +30,10 @@
</nav>
</header>
<main>
{% block content %}{% endblock %}
{% block main_content %}{% endblock %}
</main>
<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>
</body>
</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 %}
<h1>{{ user.name }}</h1>
<h6>Joined at: {{ user.created_at }}</h6>
{% block head %}
<link rel="stylesheet" href="../static/css/user.css">
{% 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>
<h1>Posts:</h1>
<ul class="posts">
{% for post in user.posts %}
<h2><span class="user-{{user.perms}}">{{ user.name }}'s</span> Posts</h2>
<ul class="post-list">
{% for post in posts %}
<li>
<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>
{% include "templates/short_post.html" %}
</li>
{% endfor %}
{% if user.posts|length == 0 %}
<li>No posts found.</li>
{% endif %}
</ul>
{% if total_pages > 0 %}
<div id="nav">
<h5>Page {{ page }}</h5>
<h5>Page {{ page }} of {{ total_pages }}</h5>
{% 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 %}
{% if user.posts|length == 10 %}
<h5><a href="/user/{{ user.name }}?page={{ page + 1 }}">Next Page</a></h5>
{% if posts|length == 10 %}
<h5><a href="/users/{{user.id}}?page={{ page + 1 }}">Next Page</a></h5>
{% endif %}
</div>
{% endif %}
{% 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;
--fg: #fff;
--accent: #7139f3;
--admin: #39f3da;
--user: #5b6dd4;
--board: #515699;
--time: #30597a;
}
body {
@ -83,11 +87,18 @@ a {
text-decoration: none;
transition: all 0.3s ease;
}
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;
padding: 0;
margin: 0;
@ -97,90 +108,41 @@ ul.posts {
gap: 10px;
}
form {
ul.post-list {
list-style: none;
padding: 0;
margin: 0;
margin-left: 10px;
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;
.user-admin {
color: var(--admin);
}
input[type="text"]:focus,
input[type="password"]:focus {
outline: none;
border-color: var(--accent);
.user-user {
color: var(--user);
}
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;
.board-name {
color: var(--board);
}
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;
.time {
color: var(--time);
}
select {
padding: 10px;
border: 1px solid var(--fg);
border-radius: 5px;
background-color: var(--bg);
color: var(--fg);
font-size: 1rem;
width: 300px;
#title {
background: linear-gradient(90deg, var(--accent), var(--admin));
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
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 {
.attachments {
display: flex;
gap: 10px;
margin-top: 20px;
}
p {
white-space:pre;
align-items: center;
}

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],
}