This commit is contained in:
0x01FE committed 2025-07-09 09:50:01 -05:00
commit 6dcb020bb5
16 files changed
+488

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
import flask
from . import user_api
from . import forum_api
from . import thread_api
api = flask.Blueprint(
name='api',
import_name=__name__,
url_prefix='/api/'
)
api.register_blueprint(user_api.user)
api.register_blueprint(forum_api.forum)
api.register_blueprint(thread_api.thread)
+47
View File
@@ -0,0 +1,47 @@
import time
import flask
import flask_login
from . import helpers
import database
forum = flask.Blueprint(
name='forum_api',
import_name=__name__,
url_prefix='/forum/'
)
@forum.route('/forum/send_message', methods=['POST'])
@flask_login.login_required
def send_message() -> tuple[str, int]:
data = flask.request.json
if not data:
return '', 400
if any([not p in data for p in ['content', 'thread']]):
return '', 400
new_message = database.MessageORM(
user=flask_login.current_user.get_id(),
content=data['content'],
thread=data['thread'],
time=int(time.time())
)
if not helpers.user_in_thread(flask_login.current_user.get_id(), data['thread']):
return '', 401
database.insert_message(new_message)
return '', 201
@forum.route('/forum/get_threads')
@flask_login.login_required
def get_forum_threads() -> tuple[str, int]:
return '', 200
+26
View File
@@ -0,0 +1,26 @@
import logging
import database
def user_in_thread(user_id: int, thread_id: int) -> bool:
forums = database.get_user_forums(user_id)
if not forums:
return False
for forum in forums:
if not forum.id:
logging.error(f'Forum {forum.name} has no id but user is in it?')
continue
threads = database.get_forum_threads(forum.id)
for thread in threads:
if not thread.id:
logging.error(f'Thread {thread.name} has no id?')
continue
if thread.id == thread_id:
return True
return False
+24
View File
@@ -0,0 +1,24 @@
import dataclasses
import flask
import flask_login
import database
thread = flask.Blueprint(
name='thread_api',
import_name=__name__,
url_prefix='/thread/'
)
@thread.route('/get_messages')
@flask_login.login_required
def get_thread_messages() -> tuple[flask.Response, int]:
thread_id = flask.request.args.get("thread", type=int)
if thread_id is None:
return flask.jsonify({"error": "Missing thread parameter"}), 400
messages = database.get_messages_by_thread(thread_id)
messages_dicts = {"messages" : [dataclasses.asdict(msg) for msg in messages]}
return flask.jsonify(messages_dicts), 200
+25
View File
@@ -0,0 +1,25 @@
import dataclasses
import flask
import flask_login
import mgU
import database
user = flask.Blueprint(
name='user_api',
import_name=__name__,
url_prefix='/user/'
)
@user.route('/get_forums')
@flask_login.login_required
def get_user_forums() -> tuple[flask.Response, int]:
user_id: str = flask_login.current_user.get_id()
forums: list[mgU.Forum] = database.get_user_forums(int(user_id))
forums_dict = {"forums" : [dataclasses.asdict(forum) for forum in forums]}
return flask.jsonify(forums_dict), 200
+41
View File
@@ -0,0 +1,41 @@
import base64
import secrets
import logging
import flask
import flask_login
import api.api as api
import database
app = flask.Flask(__name__)
app.secret_key = secrets.token_hex(16)
app.register_blueprint(api.api)
logging.basicConfig(
format="%(asctime)s [%(levelname)s] %(name)s:%(lineno)d - %(message)s",
level=logging.DEBUG
)
login_manager = flask_login.LoginManager()
login_manager.session_protection = "strong"
login_manager.init_app(app)
@login_manager.request_loader
def load_user(request: flask.Request) -> database.User | None:
api_key = request.headers.get('Authorization')
if api_key:
api_key = api_key.replace('Basic ', '', 1)
try:
api_key = base64.b64decode(api_key).decode()
except TypeError:
pass
username, password = api_key.split(':')
user = database.get_user(username, password)
return user
return None
if __name__ == "__main__":
app.run(debug=True)
+193
View File
@@ -0,0 +1,193 @@
import sqlite3
import logging
import dataclasses
import argon2
import flask_login
from sqlalchemy import create_engine, Column, Integer, ForeignKey, Text, select
from sqlalchemy.orm import sessionmaker, DeclarativeBase
from sqlalchemy.exc import SQLAlchemyError
import mgU
DATABASE = "./main2.db"
ph = argon2.PasswordHasher()
class User(flask_login.UserMixin):
def __init__(self, id: str, name: str):
self.name = name
self.id = id
def get_id(self) -> str:
return self.id
# My awful sqlite boilerplate because i hate orms
def load_query(name: str) -> str:
path = f'server/sql/{name}.sql'
return open(path).read()
class Opener():
def __init__(self):
self.con = sqlite3.connect(DATABASE)
def __enter__(self) -> tuple[sqlite3.Connection, sqlite3.Cursor]:
return self.con, self.con.cursor()
def __exit__(self, type, value, traceback):
self.con.commit()
self.con.close()
# orm garbage
class Base(DeclarativeBase):
pass
class MessageORM(Base):
__tablename__ = "messages"
id = Column(Integer, primary_key=True, autoincrement=True)
user = Column(Integer, ForeignKey("users.id"), nullable=False)
thread = Column(Integer, ForeignKey("threads.id"), nullable=False)
content = Column(Text, nullable=False)
time = Column(Integer, nullable=False)
class UserORM(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(Text, nullable=False, unique=True)
passhash = Column(Text, nullable=False)
class ForumORM(Base):
__tablename__ = "forums"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(Text, nullable=False)
class UserForumsORM(Base):
__tablename__ = "user_forums"
user_id = Column(Integer, ForeignKey("users.id"), primary_key=True, nullable=False)
forum_id = Column(Integer, ForeignKey("forums.id"), primary_key=True, nullable=False)
class ThreadORM(Base):
__tablename__ = "threads"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(Text, nullable=False)
forum = Column(Integer, ForeignKey("forums.id"), nullable=False)
# Raw SQL DTOs
@dataclasses.dataclass
class UserDTO:
name: str
id: int | None = None
@dataclasses.dataclass
class MessageDTO:
user: UserDTO
content: str
time: int
thread_id: int
id: int | None = None
engine = create_engine(f"sqlite:///{DATABASE}", echo=False, future=True)
SessionLocal = sessionmaker(bind=engine)
def insert_message(message: MessageORM) -> None:
"""
Insert a new message (from mgU.Message dataclass) and return a new mgU.Message with id, or None on failure.
"""
session = SessionLocal()
try:
session.add(message)
session.commit()
except SQLAlchemyError:
session.rollback()
return None
finally:
session.close()
def get_messages_by_thread(thread_id: int) -> list[mgU.Message]:
"""
Return a list of mgU.Message objects for a given thread_id, ordered by id ascending.
"""
with Opener() as (conn, cur):
q = load_query("get_messages_by_thread")
cur.execute(q, [thread_id])
results = cur.fetchall()
return [
mgU.Message(
user=mgU.User(
name=msg[0],
id=msg[1]
),
thread=msg[4],
content=msg[2],
time=msg[3],
id=msg[5]
)
for msg in results
]
def get_user_forums(user_id: int) -> list[mgU.Forum]:
session = SessionLocal()
try:
q = (
select(ForumORM.id, ForumORM.name)
.join_from(ForumORM, UserForumsORM)
.where(UserForumsORM.user_id == user_id)
)
results = session.execute(q)
return [
mgU.Forum(
name=forum.name,
id=forum.id
)
for forum in results
]
finally:
session.close()
def get_forum_threads(forum_id: int) -> list[mgU.Thread]:
session = SessionLocal()
try:
q = (
select(ThreadORM.id, ThreadORM.name, ThreadORM.forum)
.where(ThreadORM.forum == forum_id)
)
results = session.execute(q)
return [
mgU.Thread(
name=thread.name,
forum=thread.forum,
id=thread.id
)
for thread in results
]
finally:
session.close()
# For flask_login
def get_user(username: str, api_key: str) -> User | None:
session = SessionLocal()
try:
results = session.query(UserORM).where(UserORM.name == username)
finally:
session.close()
if not results:
return None
user_row = results[0]
try:
ph.verify(user_row.passhash, api_key)
except argon2.exceptions.VerifyMismatchError:
return None
return User(str(user_row.id), str(user_row.name))
+4
View File
@@ -0,0 +1,4 @@
{
"content" : "Hello, world!",
"thread" : 1
}
+31
View File
@@ -0,0 +1,31 @@
import typing
import dataclasses
@dataclasses.dataclass(kw_only=True)
class User:
id: int | None = None
name: str
@dataclasses.dataclass(kw_only=True)
class Forum:
id: int | None = None
name: str
@dataclasses.dataclass(kw_only=True)
class Thread:
id: int | None = None
name: str
forum: int
@dataclasses.dataclass(kw_only=True)
class UserForum:
user_id: int
forum_id: int
@dataclasses.dataclass(kw_only=True)
class Message:
user: User
content: str
thread: int
time: int
id: int | None = None
+3
View File
@@ -0,0 +1,3 @@
Flask==2.2.3
Flask-Login==0.6.3
SQLAlchemy==2.0.41
+11
View File
@@ -0,0 +1,11 @@
select
users.name as user_name,
users.id as user_id,
messages.content,
messages.time,
messages.thread,
messages.id as message_id
from messages
inner join users on messages.user = users.id
where messages.thread = ?
;