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

+1
View File
@@ -0,0 +1 @@
__pycache__
BIN
View File
Binary file not shown.
+43
View File
@@ -0,0 +1,43 @@
CREATE TABLE IF NOT EXISTS "messages" (
"id" INTEGER NOT NULL UNIQUE,
"user" INTEGER NOT NULL,
"thread" INTEGER NOT NULL,
"content" TEXT NOT NULL,
PRIMARY KEY("id"),
FOREIGN KEY ("user") REFERENCES "users"("id")
ON UPDATE NO ACTION ON DELETE NO ACTION,
FOREIGN KEY ("thread") REFERENCES "threads"("id")
ON UPDATE NO ACTION ON DELETE NO ACTION
);
CREATE TABLE IF NOT EXISTS "users" (
"id" INTEGER NOT NULL UNIQUE,
"name" TEXT NOT NULL,
PRIMARY KEY("id")
);
CREATE TABLE IF NOT EXISTS "forums" (
"id" INTEGER NOT NULL UNIQUE,
"name" TEXT NOT NULL,
PRIMARY KEY("id")
);
CREATE TABLE IF NOT EXISTS "user_forums" (
"user_id" INTEGER NOT NULL,
"forum_id" INTEGER NOT NULL,
PRIMARY KEY("user_id", "forum_id"),
FOREIGN KEY ("forum_id") REFERENCES "forums"("id")
ON UPDATE NO ACTION ON DELETE NO ACTION,
FOREIGN KEY ("user_id") REFERENCES "users"("id")
ON UPDATE NO ACTION ON DELETE NO ACTION
);
CREATE TABLE IF NOT EXISTS "threads" (
"id" INTEGER NOT NULL UNIQUE,
"name" TEXT NOT NULL,
"forum" INTEGER NOT NULL,
PRIMARY KEY("id"),
FOREIGN KEY ("forum") REFERENCES "forums"("id")
ON UPDATE NO ACTION ON DELETE NO ACTION
);
+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 = ?
;
+18
View File
@@ -0,0 +1,18 @@
import requests
import requests.auth
session = requests.Session()
URL = 'http://127.0.0.1:5000'
r = requests.get(
URL + '/api/thread/get_messages?thread=1',
auth=requests.auth.HTTPBasicAuth(
"0x01FE",
"password"
)
)
print(r.content.decode())
+7
View File
@@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS "user_forums" (
"user_id" INTEGER NOT NULL,
"forum_id" INTEGER NOT NULL,
PRIMARY KEY ("user_id", "forum_id"),
FOREIGN KEY ("user_id") REFERENCES "users"("id"),
FOREIGN KEY ("forum_id") REFERENCES "forums"("id")
);