init
This commit is contained in:
commit
6dcb020bb5
16 files changed
+488
No files matched your search
@@ -0,0 +1 @@
|
|||||||
|
__pycache__
|
||||||
+43
@@ -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
|
||||||
|
);
|
||||||
|
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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))
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"content" : "Hello, world!",
|
||||||
|
"thread" : 1
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Flask==2.2.3
|
||||||
|
Flask-Login==0.6.3
|
||||||
|
SQLAlchemy==2.0.41
|
||||||
@@ -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 = ?
|
||||||
|
;
|
||||||
@@ -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())
|
||||||
|
|
||||||
|
|
||||||
Reference in new issue
Block a user