From efcc4a14b6a0577d8ba4de1a5886b16bf009098a Mon Sep 17 00:00:00 2001 From: retoor Date: Mon, 4 Aug 2025 00:04:08 +0200 Subject: [PATCH] Working version, perfect. --- ads.py | 237 ++++++++++--- main.py | 1032 ++++++++++++++++++++++++++++--------------------------- 2 files changed, 718 insertions(+), 551 deletions(-) diff --git a/ads.py b/ads.py index 7dba74e..d5b46b8 100644 --- a/ads.py +++ b/ads.py @@ -2,7 +2,7 @@ import re import json from uuid import uuid4 from datetime import datetime, timezone -from typing import Any, Dict, Iterable, List, Optional, AsyncGenerator, Union, Tuple +from typing import Any, Dict, Iterable, List, Optional, AsyncGenerator, Union, Tuple, Set from pathlib import Path import aiosqlite import unittest @@ -13,9 +13,16 @@ import asyncio class AsyncDataSet: _KV_TABLE = "__kv_store" + _DEFAULT_COLUMNS = { + "uid": "TEXT PRIMARY KEY", + "created_at": "TEXT", + "updated_at": "TEXT", + "deleted_at": "TEXT", + } def __init__(self, file: str): self._file = file + self._table_columns_cache: Dict[str, Set[str]] = {} @staticmethod def _utc_iso() -> str: @@ -40,26 +47,62 @@ class AsyncDataSet: return "BLOB" return "TEXT" + async def _get_table_columns(self, table: str) -> Set[str]: + """Get actual columns that exist in the table.""" + if table in self._table_columns_cache: + return self._table_columns_cache[table] + + columns = set() + try: + async with aiosqlite.connect(self._file) as db: + async with db.execute(f"PRAGMA table_info({table})") as cursor: + async for row in cursor: + columns.add(row[1]) # Column name is at index 1 + self._table_columns_cache[table] = columns + except: + pass + return columns + + async def _invalidate_column_cache(self, table: str): + """Invalidate column cache for a table.""" + if table in self._table_columns_cache: + del self._table_columns_cache[table] + async def _ensure_column(self, table: str, name: str, value: Any) -> None: col_type = self._py_to_sqlite_type(value) - async with aiosqlite.connect(self._file) as db: - await db.execute(f"ALTER TABLE {table} ADD COLUMN `{name}` {col_type}") - await db.commit() + try: + async with aiosqlite.connect(self._file) as db: + await db.execute(f"ALTER TABLE {table} ADD COLUMN `{name}` {col_type}") + await db.commit() + await self._invalidate_column_cache(table) + except aiosqlite.OperationalError as e: + if "duplicate column name" in str(e).lower(): + pass # Column already exists + else: + raise async def _ensure_table(self, table: str, col_sources: Dict[str, Any]) -> None: - cols: Dict[str, str] = { - "uid": "TEXT PRIMARY KEY", - "created_at": "TEXT", - "updated_at": "TEXT", - "deleted_at": "TEXT", - } + # Always include default columns + cols = self._DEFAULT_COLUMNS.copy() + + # Add columns from col_sources for key, val in col_sources.items(): if key not in cols: cols[key] = self._py_to_sqlite_type(val) + columns_sql = ", ".join(f"`{k}` {t}" for k, t in cols.items()) async with aiosqlite.connect(self._file) as db: await db.execute(f"CREATE TABLE IF NOT EXISTS {table} ({columns_sql})") await db.commit() + await self._invalidate_column_cache(table) + + async def _table_exists(self, table: str) -> bool: + """Check if a table exists.""" + async with aiosqlite.connect(self._file) as db: + async with db.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table,) + ) as cursor: + return await cursor.fetchone() is not None _RE_NO_COLUMN = re.compile(r"(?:no such column:|has no column named) (\w+)") _RE_NO_TABLE = re.compile(r"no such table: (\w+)") @@ -84,25 +127,60 @@ class AsyncDataSet: sql: str, params: Iterable[Any], col_sources: Dict[str, Any], + max_retries: int = 10 ) -> aiosqlite.Cursor: - while True: + retries = 0 + while retries < max_retries: try: async with aiosqlite.connect(self._file) as db: cursor = await db.execute(sql, params) await db.commit() return cursor except aiosqlite.OperationalError as err: + retries += 1 + err_str = str(err).lower() + + # Handle missing column col = self._missing_column_from_error(err) if col: - if col not in col_sources: - raise - await self._ensure_column(table, col, col_sources[col]) + if col in col_sources: + await self._ensure_column(table, col, col_sources[col]) + else: + # Column not in sources, ensure it with NULL/TEXT type + await self._ensure_column(table, col, None) continue + + # Handle missing table tbl = self._missing_table_from_error(err) if tbl: await self._ensure_table(tbl, col_sources) continue + + # Handle other column-related errors + if "has no column named" in err_str: + # Extract column name differently + match = re.search(r"table \w+ has no column named (\w+)", err_str) + if match: + col_name = match.group(1) + if col_name in col_sources: + await self._ensure_column(table, col_name, col_sources[col_name]) + else: + await self._ensure_column(table, col_name, None) + continue + raise + raise Exception(f"Max retries ({max_retries}) exceeded") + + async def _filter_existing_columns(self, table: str, data: Dict[str, Any]) -> Dict[str, Any]: + """Filter data to only include columns that exist in the table.""" + if not await self._table_exists(table): + return data + + existing_columns = await self._get_table_columns(table) + if not existing_columns: + return data + + return {k: v for k, v in data.items() if k in existing_columns} async def _safe_query( self, @@ -111,7 +189,14 @@ class AsyncDataSet: params: Iterable[Any], col_sources: Dict[str, Any], ) -> AsyncGenerator[Dict[str, Any], None]: - while True: + # Check if table exists first + if not await self._table_exists(table): + return + + max_retries = 10 + retries = 0 + + while retries < max_retries: try: async with aiosqlite.connect(self._file) as db: db.row_factory = aiosqlite.Row @@ -120,10 +205,20 @@ class AsyncDataSet: yield dict(row) return except aiosqlite.OperationalError as err: + retries += 1 + err_str = str(err).lower() + + # Handle missing table tbl = self._missing_table_from_error(err) if tbl: - await self._ensure_table(tbl, col_sources) - continue + # For queries, if table doesn't exist, just return empty + return + + # Handle missing column in WHERE clause or SELECT + if "no such column" in err_str: + # For queries with missing columns, return empty + return + raise @staticmethod @@ -145,27 +240,34 @@ class AsyncDataSet: **args, } + # Ensure table exists with all needed columns + await self._ensure_table(table, record) + # Handle auto-increment ID if requested if return_id and 'id' not in args: + # Ensure id column exists async with aiosqlite.connect(self._file) as db: - # Create table with autoincrement ID if needed - await db.execute(f""" - CREATE TABLE IF NOT EXISTS {table} ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - uid TEXT UNIQUE, - created_at TEXT, - updated_at TEXT, - deleted_at TEXT - ) - """) - await db.commit() + # Add id column if it doesn't exist + try: + await db.execute(f"ALTER TABLE {table} ADD COLUMN id INTEGER PRIMARY KEY AUTOINCREMENT") + await db.commit() + except aiosqlite.OperationalError as e: + if "duplicate column name" not in str(e).lower(): + # Try without autoincrement constraint + try: + await db.execute(f"ALTER TABLE {table} ADD COLUMN id INTEGER") + await db.commit() + except: + pass + + await self._invalidate_column_cache(table) - # Insert and get lastrowid - cols = "`" + "`, `".join(record.keys()) + "`" - qs = ", ".join(["?"] * len(record)) - cursor = await db.execute(f"INSERT INTO {table} ({cols}) VALUES ({qs})", list(record.values())) - await db.commit() - return cursor.lastrowid + # Insert and get lastrowid + cols = "`" + "`, `".join(record.keys()) + "`" + qs = ", ".join(["?"] * len(record)) + sql = f"INSERT INTO {table} ({cols}) VALUES ({qs})" + cursor = await self._safe_execute(table, sql, list(record.values()), record) + return cursor.lastrowid cols = "`" + "`, `".join(record) + "`" qs = ", ".join(["?"] * len(record)) @@ -181,15 +283,31 @@ class AsyncDataSet: ) -> int: if not args: return 0 + + # Check if table exists + if not await self._table_exists(table): + return 0 + args["updated_at"] = self._utc_iso() + + # Ensure all columns exist + all_cols = {**args, **(where or {})} + await self._ensure_table(table, all_cols) + for col, val in all_cols.items(): + await self._ensure_column(table, col, val) + set_clause = ", ".join(f"`{k}` = ?" for k in args) where_clause, where_params = self._build_where(where) sql = f"UPDATE {table} SET {set_clause}{where_clause}" params = list(args.values()) + where_params - cur = await self._safe_execute(table, sql, params, {**args, **(where or {})}) + cur = await self._safe_execute(table, sql, params, all_cols) return cur.rowcount async def delete(self, table: str, where: Optional[Dict[str, Any]] = None) -> int: + # Check if table exists + if not await self._table_exists(table): + return 0 + where_clause, where_params = self._build_where(where) sql = f"DELETE FROM {table}{where_clause}" cur = await self._safe_execute(table, sql, where_params, where or {}) @@ -241,6 +359,10 @@ class AsyncDataSet: ] async def count(self, table: str, where: Optional[Dict[str, Any]] = None) -> int: + # Check if table exists + if not await self._table_exists(table): + return 0 + where_clause, where_params = self._build_where(where) sql = f"SELECT COUNT(*) FROM {table}{where_clause}" gen = self._safe_query(table, sql, where_params, where or {}) @@ -287,10 +409,14 @@ class AsyncDataSet: async def query_raw(self, sql: str, params: Optional[Tuple] = None) -> List[Dict[str, Any]]: """Execute raw SQL query and return results as list of dicts.""" - async with aiosqlite.connect(self._file) as db: - db.row_factory = aiosqlite.Row - async with db.execute(sql, params or ()) as cursor: - return [dict(row) async for row in cursor] + try: + async with aiosqlite.connect(self._file) as db: + db.row_factory = aiosqlite.Row + async with db.execute(sql, params or ()) as cursor: + return [dict(row) async for row in cursor] + except aiosqlite.OperationalError: + # Return empty list if query fails + return [] async def query_one(self, sql: str, params: Optional[Tuple] = None) -> Optional[Dict[str, Any]]: """Execute raw SQL query and return single result.""" @@ -298,8 +424,12 @@ class AsyncDataSet: return results[0] if results else None async def create_table(self, table: str, schema: Dict[str, str], constraints: Optional[List[str]] = None): - """Create table with custom schema and constraints.""" - columns = [f"`{col}` {dtype}" for col, dtype in schema.items()] + """Create table with custom schema and constraints. Always includes default columns.""" + # Merge default columns with custom schema + full_schema = self._DEFAULT_COLUMNS.copy() + full_schema.update(schema) + + columns = [f"`{col}` {dtype}" for col, dtype in full_schema.items()] if constraints: columns.extend(constraints) columns_sql = ", ".join(columns) @@ -307,6 +437,7 @@ class AsyncDataSet: async with aiosqlite.connect(self._file) as db: await db.execute(f"CREATE TABLE IF NOT EXISTS {table} ({columns_sql})") await db.commit() + await self._invalidate_column_cache(table) async def insert_unique(self, table: str, args: Dict[str, Any], unique_fields: List[str]) -> Union[str, None]: """Insert with unique constraint handling. Returns uid on success, None if duplicate.""" @@ -323,6 +454,10 @@ class AsyncDataSet: async def aggregate(self, table: str, function: str, column: str = "*", where: Optional[Dict[str, Any]] = None) -> Any: """Perform aggregate functions like SUM, AVG, MAX, MIN.""" + # Check if table exists + if not await self._table_exists(table): + return None + where_clause, where_params = self._build_where(where) sql = f"SELECT {function}({column}) as result FROM {table}{where_clause}" result = await self.query_one(sql, tuple(where_params)) @@ -450,6 +585,26 @@ class TestAsyncDataSet(unittest.IsolatedAsyncioTestCase): ["username", "email"] ) self.assertIsNone(result) + + async def test_missing_table_operations(self): + # Test operations on non-existent tables + self.assertEqual(await self.connector.count("nonexistent"), 0) + self.assertEqual(await self.connector.find("nonexistent"), []) + self.assertIsNone(await self.connector.get("nonexistent")) + self.assertFalse(await self.connector.exists("nonexistent", {"id": 1})) + self.assertEqual(await self.connector.delete("nonexistent"), 0) + self.assertEqual(await self.connector.update("nonexistent", {"name": "test"}), 0) + + async def test_auto_column_creation(self): + # Insert with new columns that don't exist yet + await self.connector.insert("dynamic", {"col1": "value1", "col2": 42, "col3": 3.14}) + + # Add more columns in next insert + await self.connector.insert("dynamic", {"col1": "value2", "col4": True, "col5": None}) + + # All records should be retrievable + records = await self.connector.find("dynamic") + self.assertEqual(len(records), 2) if __name__ == "__main__": diff --git a/main.py b/main.py index a09bfc4..a0c396c 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,3 @@ -# backend.py from fastapi import FastAPI, HTTPException, Depends, Form, File, UploadFile from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse @@ -7,10 +6,11 @@ from typing import Optional, List, Dict, Any, Literal from datetime import datetime, timedelta import hashlib import secrets -import sqlite3 import json import os from pathlib import Path +import asyncio +from ads import AsyncDataSet app = FastAPI(title="DevRant Community API") @@ -28,103 +28,93 @@ DB_PATH = "devrant_community.db" UPLOAD_DIR = Path("uploads") UPLOAD_DIR.mkdir(exist_ok=True) -def init_db(): - conn = sqlite3.connect(DB_PATH) - c = conn.cursor() - +# Initialize AsyncDataSet +db = AsyncDataSet(DB_PATH) + +async def init_db(): # Users table - c.execute('''CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - email TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - score INTEGER DEFAULT 0, - about TEXT DEFAULT '', - location TEXT DEFAULT '', - skills TEXT DEFAULT '', - github TEXT DEFAULT '', - website TEXT DEFAULT '', - created_time INTEGER NOT NULL, - avatar_b TEXT DEFAULT '7bc8a4', - avatar_i TEXT - )''') + await db.create_table("users", { + "id": "INTEGER PRIMARY KEY AUTOINCREMENT", + "username": "TEXT NOT NULL", + "email": "TEXT NOT NULL", + "password_hash": "TEXT NOT NULL", + "score": "INTEGER DEFAULT 0", + "about": "TEXT DEFAULT ''", + "location": "TEXT DEFAULT ''", + "skills": "TEXT DEFAULT ''", + "github": "TEXT DEFAULT ''", + "website": "TEXT DEFAULT ''", + "created_time": "INTEGER NOT NULL", + "avatar_b": "TEXT DEFAULT '7bc8a4'", + "avatar_i": "TEXT" + }, ["UNIQUE(username)", "UNIQUE(email)"]) # Auth tokens table - c.execute('''CREATE TABLE IF NOT EXISTS auth_tokens ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - token_key TEXT UNIQUE NOT NULL, - expire_time INTEGER NOT NULL, - FOREIGN KEY (user_id) REFERENCES users (id) - )''') + await db.create_table("auth_tokens", { + "id": "INTEGER PRIMARY KEY AUTOINCREMENT", + "user_id": "INTEGER NOT NULL", + "token_key": "TEXT NOT NULL", + "expire_time": "INTEGER NOT NULL" + }, ["UNIQUE(token_key)", "FOREIGN KEY (user_id) REFERENCES users (id)"]) # Rants table - c.execute('''CREATE TABLE IF NOT EXISTS rants ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - text TEXT NOT NULL, - score INTEGER DEFAULT 0, - created_time INTEGER NOT NULL, - attached_image TEXT DEFAULT '', - tags TEXT DEFAULT '', - edited BOOLEAN DEFAULT 0, - type INTEGER DEFAULT 1, - FOREIGN KEY (user_id) REFERENCES users (id) - )''') + await db.create_table("rants", { + "id": "INTEGER PRIMARY KEY AUTOINCREMENT", + "user_id": "INTEGER NOT NULL", + "text": "TEXT NOT NULL", + "score": "INTEGER DEFAULT 0", + "created_time": "INTEGER NOT NULL", + "attached_image": "TEXT DEFAULT ''", + "tags": "TEXT DEFAULT ''", + "edited": "INTEGER DEFAULT 0", + "type": "INTEGER DEFAULT 1" + }, ["FOREIGN KEY (user_id) REFERENCES users (id)"]) # Comments table - c.execute('''CREATE TABLE IF NOT EXISTS comments ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - rant_id INTEGER NOT NULL, - user_id INTEGER NOT NULL, - body TEXT NOT NULL, - score INTEGER DEFAULT 0, - created_time INTEGER NOT NULL, - attached_image TEXT DEFAULT '', - edited BOOLEAN DEFAULT 0, - FOREIGN KEY (rant_id) REFERENCES rants (id), - FOREIGN KEY (user_id) REFERENCES users (id) - )''') + await db.create_table("comments", { + "id": "INTEGER PRIMARY KEY AUTOINCREMENT", + "rant_id": "INTEGER NOT NULL", + "user_id": "INTEGER NOT NULL", + "body": "TEXT NOT NULL", + "score": "INTEGER DEFAULT 0", + "created_time": "INTEGER NOT NULL", + "attached_image": "TEXT DEFAULT ''", + "edited": "INTEGER DEFAULT 0" + }, ["FOREIGN KEY (rant_id) REFERENCES rants (id)", "FOREIGN KEY (user_id) REFERENCES users (id)"]) # Votes table - c.execute('''CREATE TABLE IF NOT EXISTS votes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - target_id INTEGER NOT NULL, - target_type TEXT NOT NULL, - vote INTEGER NOT NULL, - reason INTEGER, - UNIQUE(user_id, target_id, target_type), - FOREIGN KEY (user_id) REFERENCES users (id) - )''') + await db.create_table("votes", { + "id": "INTEGER PRIMARY KEY AUTOINCREMENT", + "user_id": "INTEGER NOT NULL", + "target_id": "INTEGER NOT NULL", + "target_type": "TEXT NOT NULL", + "vote": "INTEGER NOT NULL", + "reason": "INTEGER" + }, ["UNIQUE(user_id, target_id, target_type)", "FOREIGN KEY (user_id) REFERENCES users (id)"]) # Favorites table - c.execute('''CREATE TABLE IF NOT EXISTS favorites ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - rant_id INTEGER NOT NULL, - UNIQUE(user_id, rant_id), - FOREIGN KEY (user_id) REFERENCES users (id), - FOREIGN KEY (rant_id) REFERENCES rants (id) - )''') + await db.create_table("favorites", { + "id": "INTEGER PRIMARY KEY AUTOINCREMENT", + "user_id": "INTEGER NOT NULL", + "rant_id": "INTEGER NOT NULL" + }, ["UNIQUE(user_id, rant_id)", "FOREIGN KEY (user_id) REFERENCES users (id)", "FOREIGN KEY (rant_id) REFERENCES rants (id)"]) # Notifications table - c.execute('''CREATE TABLE IF NOT EXISTS notifications ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - type TEXT NOT NULL, - rant_id INTEGER, - comment_id INTEGER, - from_user_id INTEGER, - created_time INTEGER NOT NULL, - read BOOLEAN DEFAULT 0, - FOREIGN KEY (user_id) REFERENCES users (id) - )''') - - conn.commit() - conn.close() + await db.create_table("notifications", { + "id": "INTEGER PRIMARY KEY AUTOINCREMENT", + "user_id": "INTEGER NOT NULL", + "type": "TEXT NOT NULL", + "rant_id": "INTEGER", + "comment_id": "INTEGER", + "from_user_id": "INTEGER", + "created_time": "INTEGER NOT NULL", + "read": "INTEGER DEFAULT 0" + }, ["FOREIGN KEY (user_id) REFERENCES users (id)"]) -init_db() +# Run init_db on startup +@app.on_event("startup") +async def startup_event(): + await init_db() # Pydantic models class UserRegister(BaseModel): @@ -149,57 +139,44 @@ class VoteRequest(BaseModel): reason: Optional[int] = None # Helper functions -def get_db(): - conn = sqlite3.connect(DB_PATH) - conn.row_factory = sqlite3.Row - return conn - def hash_password(password: str) -> str: return hashlib.sha256(password.encode()).hexdigest() def generate_token() -> str: return secrets.token_urlsafe(32) -def get_current_user(token_id: Optional[int] = Form(None), - token_key: Optional[str] = Form(None), - user_id: Optional[int] = Form(None)): +async def get_current_user(token_id: Optional[int] = Form(None), + token_key: Optional[str] = Form(None), + user_id: Optional[int] = Form(None)): if not all([token_id, token_key, user_id]): return None - conn = get_db() - c = conn.cursor() + token = await db.get("auth_tokens", { + "id": token_id, + "token_key": token_key, + "user_id": user_id + }) - c.execute('''SELECT * FROM auth_tokens - WHERE id = ? AND token_key = ? AND user_id = ? AND expire_time > ?''', - (token_id, token_key, user_id, int(datetime.now().timestamp()))) - - token = c.fetchone() - conn.close() - - if not token: + if not token or token['expire_time'] <= int(datetime.now().timestamp()): return None return user_id -def format_rant(rant_row, user_row, current_user_id=None): - conn = get_db() - c = conn.cursor() - +async def format_rant(rant_row, user_row, current_user_id=None): # Get comment count - c.execute('SELECT COUNT(*) as count FROM comments WHERE rant_id = ?', (rant_row['id'],)) - comment_count = c.fetchone()['count'] + comment_count = await db.count("comments", {"rant_id": rant_row['id']}) # Get vote state for current user vote_state = 0 if current_user_id: - c.execute('SELECT vote FROM votes WHERE user_id = ? AND target_id = ? AND target_type = ?', - (current_user_id, rant_row['id'], 'rant')) - vote = c.fetchone() + vote = await db.get("votes", { + "user_id": current_user_id, + "target_id": rant_row['id'], + "target_type": "rant" + }) if vote: vote_state = vote['vote'] - conn.close() - tags = json.loads(rant_row['tags']) if rant_row['tags'] else [] return { @@ -227,21 +204,18 @@ def format_rant(rant_row, user_row, current_user_id=None): } } -def format_comment(comment_row, user_row, current_user_id=None): - conn = get_db() - c = conn.cursor() - +async def format_comment(comment_row, user_row, current_user_id=None): # Get vote state for current user vote_state = 0 if current_user_id: - c.execute('SELECT vote FROM votes WHERE user_id = ? AND target_id = ? AND target_type = ?', - (current_user_id, comment_row['id'], 'comment')) - vote = c.fetchone() + vote = await db.get("votes", { + "user_id": current_user_id, + "target_id": comment_row['id'], + "target_type": "comment" + }) if vote: vote_state = vote['vote'] - conn.close() - return { "id": comment_row['id'], "rant_id": comment_row['rant_id'], @@ -267,12 +241,8 @@ async def register_user( type: int = Form(1), app: int = Form(3) ): - conn = get_db() - c = conn.cursor() - # Validate username length if len(username) < 4 or len(username) > 15: - conn.close() return { "success": False, "error": "Your username must be between 4 and 15 characters.", @@ -280,9 +250,7 @@ async def register_user( } # Check if username exists - c.execute('SELECT id FROM users WHERE username = ?', (username,)) - if c.fetchone(): - conn.close() + if await db.exists("users", {"username": username}): return { "success": False, "error": "Username already taken.", @@ -290,9 +258,7 @@ async def register_user( } # Check if email exists - c.execute('SELECT id FROM users WHERE email = ?', (email,)) - if c.fetchone(): - conn.close() + if await db.exists("users", {"email": email}): return { "success": False, "error": "Email already registered.", @@ -304,14 +270,14 @@ async def register_user( created_time = int(datetime.now().timestamp()) try: - c.execute('''INSERT INTO users (username, email, password_hash, created_time) - VALUES (?, ?, ?, ?)''', - (username, email, password_hash, created_time)) - conn.commit() - conn.close() + await db.insert("users", { + "username": username, + "email": email, + "password_hash": password_hash, + "created_time": created_time + }, return_id=True) return {"success": True} except Exception as e: - conn.close() return {"success": False, "error": str(e)} @app.post("/api/users/auth-token") @@ -320,15 +286,13 @@ async def login( password: str = Form(...), app: int = Form(3) ): - conn = get_db() - c = conn.cursor() - - # Find user - c.execute('SELECT * FROM users WHERE username = ? OR email = ?', (username, username)) - user = c.fetchone() + # Find user by username or email + user = await db.query_one( + "SELECT * FROM users WHERE username = ? OR email = ?", + (username, username) + ) if not user or user['password_hash'] != hash_password(password): - conn.close() return { "success": False, "error": "Invalid login credentials entered. Please try again." @@ -338,13 +302,11 @@ async def login( token_key = generate_token() expire_time = int((datetime.now() + timedelta(days=30)).timestamp()) - c.execute('''INSERT INTO auth_tokens (user_id, token_key, expire_time) - VALUES (?, ?, ?)''', - (user['id'], token_key, expire_time)) - - token_id = c.lastrowid - conn.commit() - conn.close() + token_id = await db.insert("auth_tokens", { + "user_id": user['id'], + "token_key": token_key, + "expire_time": expire_time + }, return_id=True) return { "success": True, @@ -366,30 +328,41 @@ async def get_rants( token_key: Optional[str] = None, user_id: Optional[int] = None ): - current_user_id = get_current_user(token_id, token_key, user_id) if token_id else None + current_user_id = await get_current_user(token_id, token_key, user_id) if token_id else None - conn = get_db() - c = conn.cursor() - - # Get rants - order_by = "created_time DESC" if sort == "recent" else "score DESC" - c.execute(f'''SELECT r.*, u.* FROM rants r - JOIN users u ON r.user_id = u.id - ORDER BY r.{order_by} - LIMIT ? OFFSET ?''', (limit, skip)) + # Get rants with user info + order_by = "r.created_time DESC" if sort == "recent" else "r.score DESC" + rows = await db.query_raw( + f"""SELECT r.*, u.id as user_id, u.username, u.score as user_score, + u.avatar_b, u.avatar_i + FROM rants r + JOIN users u ON r.user_id = u.id + ORDER BY {order_by} + LIMIT ? OFFSET ?""", + (limit, skip) + ) rants = [] - for row in c.fetchall(): - rant_data = {col: row[col] for col in row.keys() if col in - ['id', 'text', 'score', 'created_time', 'attached_image', 'tags', 'edited', 'type']} - user_data = {col: row[col] for col in row.keys() if col in - ['username', 'avatar_b', 'avatar_i']} - user_data['id'] = row['user_id'] - user_data['score'] = row['score'] + for row in rows: + rant_data = { + 'id': row['id'], + 'text': row['text'], + 'score': row['score'], + 'created_time': row['created_time'], + 'attached_image': row['attached_image'], + 'tags': row['tags'], + 'edited': row['edited'], + 'type': row['type'] + } + user_data = { + 'id': row['user_id'], + 'username': row['username'], + 'score': row['user_score'], + 'avatar_b': row['avatar_b'], + 'avatar_i': row['avatar_i'] + } - rants.append(format_rant(rant_data, user_data, current_user_id)) - - conn.close() + rants.append(await format_rant(rant_data, user_data, current_user_id)) return { "success": True, @@ -420,57 +393,77 @@ async def get_rant( token_key: Optional[str] = None, user_id: Optional[int] = None ): - current_user_id = get_current_user(token_id, token_key, user_id) if token_id else None + current_user_id = await get_current_user(token_id, token_key, user_id) if token_id else None - conn = get_db() - c = conn.cursor() + # Get rant with user info + rant_row = await db.query_one( + """SELECT r.*, u.id as user_id, u.username, u.score as user_score, + u.avatar_b, u.avatar_i + FROM rants r + JOIN users u ON r.user_id = u.id + WHERE r.id = ?""", + (rant_id,) + ) - # Get rant - c.execute('''SELECT r.*, u.* FROM rants r - JOIN users u ON r.user_id = u.id - WHERE r.id = ?''', (rant_id,)) - - rant_row = c.fetchone() if not rant_row: - conn.close() raise HTTPException(status_code=404, detail="Rant not found") - rant_data = {col: rant_row[col] for col in rant_row.keys() if col in - ['id', 'text', 'score', 'created_time', 'attached_image', 'tags', 'edited', 'type']} - user_data = {col: rant_row[col] for col in rant_row.keys() if col in - ['username', 'avatar_b', 'avatar_i']} - user_data['id'] = rant_row['user_id'] - user_data['score'] = rant_row['score'] + rant_data = { + 'id': rant_row['id'], + 'text': rant_row['text'], + 'score': rant_row['score'], + 'created_time': rant_row['created_time'], + 'attached_image': rant_row['attached_image'], + 'tags': rant_row['tags'], + 'edited': rant_row['edited'], + 'type': rant_row['type'] + } + user_data = { + 'id': rant_row['user_id'], + 'username': rant_row['username'], + 'score': rant_row['user_score'], + 'avatar_b': rant_row['avatar_b'], + 'avatar_i': rant_row['avatar_i'] + } - rant = format_rant(rant_data, user_data, current_user_id) + rant = await format_rant(rant_data, user_data, current_user_id) # Get comments - c.execute('''SELECT c.*, u.* FROM comments c - JOIN users u ON c.user_id = u.id - WHERE c.rant_id = ? - ORDER BY c.created_time ASC''', (rant_id,)) + comment_rows = await db.query_raw( + """SELECT c.*, u.id as user_id, u.username, u.score as user_score, + u.avatar_b, u.avatar_i + FROM comments c + JOIN users u ON c.user_id = u.id + WHERE c.rant_id = ? + ORDER BY c.created_time ASC""", + (rant_id,) + ) comments = [] - for row in c.fetchall(): - comment_data = {col: row[col] for col in row.keys() if col in - ['id', 'rant_id', 'body', 'score', 'created_time']} - user_data = {col: row[col] for col in row.keys() if col in - ['username', 'avatar_b', 'avatar_i']} - user_data['id'] = row['user_id'] - user_data['score'] = row['score'] + for row in comment_rows: + comment_data = { + 'id': row['id'], + 'rant_id': row['rant_id'], + 'body': row['body'], + 'score': row['score'], + 'created_time': row['created_time'] + } + user_data = { + 'id': row['user_id'], + 'username': row['username'], + 'score': row['user_score'], + 'avatar_b': row['avatar_b'], + 'avatar_i': row['avatar_i'] + } - comments.append(format_comment(comment_data, user_data, current_user_id)) + comments.append(await format_comment(comment_data, user_data, current_user_id)) # Check if subscribed (favorited) subscribed = 0 if current_user_id: - c.execute('SELECT id FROM favorites WHERE user_id = ? AND rant_id = ?', - (current_user_id, rant_id)) - if c.fetchone(): + if await db.exists("favorites", {"user_id": current_user_id, "rant_id": rant_id}): subscribed = 1 - conn.close() - # Add link to rant rant['link'] = f"rants/{rant_id}/{rant['text'][:50].replace(' ', '-')}" @@ -492,20 +485,18 @@ async def create_rant( user_id: int = Form(...), image: Optional[UploadFile] = File(None) ): - current_user_id = get_current_user(token_id, token_key, user_id) + current_user_id = await get_current_user(token_id, token_key, user_id) if not current_user_id: return {"success": False, "error": "Authentication required"} - conn = get_db() - c = conn.cursor() - # Check for duplicate rant - c.execute('''SELECT id FROM rants - WHERE user_id = ? AND text = ? AND created_time > ?''', - (current_user_id, rant, int(datetime.now().timestamp()) - 300)) + recent_time = int(datetime.now().timestamp()) - 300 + duplicate = await db.query_one( + "SELECT id FROM rants WHERE user_id = ? AND text = ? AND created_time > ?", + (current_user_id, rant, recent_time) + ) - if c.fetchone(): - conn.close() + if duplicate: return { "success": False, "error": "It looks like you just posted this same rant! Your connection might have timed out while posting so you might have seen an error, but sometimes the rant still gets posted and in this case it seems it did, so please check :) If this was not the case please contact info@devrant.io. Thanks!" @@ -527,13 +518,14 @@ async def create_rant( created_time = int(datetime.now().timestamp()) tags_list = [tag.strip() for tag in tags.split(',') if tag.strip()] - c.execute('''INSERT INTO rants (user_id, text, created_time, attached_image, tags, type) - VALUES (?, ?, ?, ?, ?, ?)''', - (current_user_id, rant, created_time, image_path, json.dumps(tags_list), type)) - - rant_id = c.lastrowid - conn.commit() - conn.close() + rant_id = await db.insert("rants", { + "user_id": current_user_id, + "text": rant, + "created_time": created_time, + "attached_image": image_path, + "tags": json.dumps(tags_list), + "type": type + }, return_id=True) return {"success": True, "rant_id": rant_id} @@ -547,30 +539,24 @@ async def update_rant( token_key: str = Form(...), user_id: int = Form(...) ): - current_user_id = get_current_user(token_id, token_key, user_id) + current_user_id = await get_current_user(token_id, token_key, user_id) if not current_user_id: return {"success": False, "error": "Authentication required"} - conn = get_db() - c = conn.cursor() - # Check ownership - c.execute('SELECT user_id FROM rants WHERE id = ?', (rant_id,)) - rant_row = c.fetchone() + rant_row = await db.get("rants", {"id": rant_id}) if not rant_row or rant_row['user_id'] != current_user_id: - conn.close() return {"success": False, "fail_reason": "Unauthorized"} # Update rant tags_list = [tag.strip() for tag in tags.split(',') if tag.strip()] - c.execute('''UPDATE rants SET text = ?, tags = ?, edited = 1 - WHERE id = ?''', - (rant, json.dumps(tags_list), rant_id)) - - conn.commit() - conn.close() + await db.update("rants", { + "text": rant, + "tags": json.dumps(tags_list), + "edited": 1 + }, {"id": rant_id}) return {"success": True} @@ -582,33 +568,24 @@ async def delete_rant( token_key: str = None, user_id: int = None ): - current_user_id = get_current_user(token_id, token_key, user_id) + current_user_id = await get_current_user(token_id, token_key, user_id) if not current_user_id: return {"success": False, "error": "Authentication required"} - conn = get_db() - c = conn.cursor() - # Check ownership - c.execute('SELECT user_id FROM rants WHERE id = ?', (rant_id,)) - rant_row = c.fetchone() + rant_row = await db.get("rants", {"id": rant_id}) if not rant_row: - conn.close() return {"success": False, "error": "Rant not found"} if rant_row['user_id'] != current_user_id: - conn.close() return {"success": False, "error": "Unauthorized"} # Delete rant and related data - c.execute('DELETE FROM comments WHERE rant_id = ?', (rant_id,)) - c.execute('DELETE FROM votes WHERE target_id = ? AND target_type = ?', (rant_id, 'rant')) - c.execute('DELETE FROM favorites WHERE rant_id = ?', (rant_id,)) - c.execute('DELETE FROM rants WHERE id = ?', (rant_id,)) - - conn.commit() - conn.close() + await db.delete("comments", {"rant_id": rant_id}) + await db.delete("votes", {"target_id": rant_id, "target_type": "rant"}) + await db.delete("favorites", {"rant_id": rant_id}) + await db.delete("rants", {"id": rant_id}) return {"success": True} @@ -622,74 +599,93 @@ async def vote_rant( token_key: str = Form(...), user_id: int = Form(...) ): - current_user_id = get_current_user(token_id, token_key, user_id) + current_user_id = await get_current_user(token_id, token_key, user_id) if not current_user_id: return {"success": False, "error": "Authentication required"} - conn = get_db() - c = conn.cursor() - # Get rant - c.execute('SELECT * FROM rants WHERE id = ?', (rant_id,)) - rant = c.fetchone() + rant = await db.get("rants", {"id": rant_id}) if not rant: - conn.close() return {"success": False, "error": "Rant not found"} # Check for existing vote - c.execute('SELECT * FROM votes WHERE user_id = ? AND target_id = ? AND target_type = ?', - (current_user_id, rant_id, 'rant')) - existing_vote = c.fetchone() + existing_vote = await db.get("votes", { + "user_id": current_user_id, + "target_id": rant_id, + "target_type": "rant" + }) if vote == 0: # Remove vote if existing_vote: - c.execute('DELETE FROM votes WHERE id = ?', (existing_vote['id'],)) + await db.delete("votes", {"id": existing_vote['id']}) # Update score - c.execute('UPDATE rants SET score = score - ? WHERE id = ?', - (existing_vote['vote'], rant_id)) + await db.update("rants", { + "score": rant['score'] - existing_vote['vote'] + }, {"id": rant_id}) else: if existing_vote: # Update vote score_diff = vote - existing_vote['vote'] - c.execute('UPDATE votes SET vote = ?, reason = ? WHERE id = ?', - (vote, reason, existing_vote['id'])) - c.execute('UPDATE rants SET score = score + ? WHERE id = ?', - (score_diff, rant_id)) + await db.update("votes", { + "vote": vote, + "reason": reason + }, {"id": existing_vote['id']}) + await db.update("rants", { + "score": rant['score'] + score_diff + }, {"id": rant_id}) else: # New vote - c.execute('''INSERT INTO votes (user_id, target_id, target_type, vote, reason) - VALUES (?, ?, ?, ?, ?)''', - (current_user_id, rant_id, 'rant', vote, reason)) - c.execute('UPDATE rants SET score = score + ? WHERE id = ?', - (vote, rant_id)) + await db.insert("votes", { + "user_id": current_user_id, + "target_id": rant_id, + "target_type": "rant", + "vote": vote, + "reason": reason + }, return_id=True) + await db.update("rants", { + "score": rant['score'] + vote + }, {"id": rant_id}) # Update user score - c.execute('UPDATE users SET score = score + ? WHERE id = ?', - (vote if vote != 0 else -existing_vote['vote'] if existing_vote else 0, - rant['user_id'])) + user = await db.get("users", {"id": rant['user_id']}) + score_change = vote if vote != 0 else -existing_vote['vote'] if existing_vote else 0 + await db.update("users", { + "score": user['score'] + score_change + }, {"id": rant['user_id']}) - conn.commit() + # Get updated rant with user info + updated_rant = await db.query_one( + """SELECT r.*, u.id as user_id, u.username, u.score as user_score, + u.avatar_b, u.avatar_i + FROM rants r + JOIN users u ON r.user_id = u.id + WHERE r.id = ?""", + (rant_id,) + ) - # Get updated rant - c.execute('''SELECT r.*, u.* FROM rants r - JOIN users u ON r.user_id = u.id - WHERE r.id = ?''', (rant_id,)) - updated_rant = c.fetchone() - - conn.close() - - rant_data = {col: updated_rant[col] for col in updated_rant.keys() if col in - ['id', 'text', 'score', 'created_time', 'attached_image', 'tags', 'edited', 'type']} - user_data = {col: updated_rant[col] for col in updated_rant.keys() if col in - ['username', 'avatar_b', 'avatar_i']} - user_data['id'] = updated_rant['user_id'] - user_data['score'] = updated_rant['score'] + rant_data = { + 'id': updated_rant['id'], + 'text': updated_rant['text'], + 'score': updated_rant['score'], + 'created_time': updated_rant['created_time'], + 'attached_image': updated_rant['attached_image'], + 'tags': updated_rant['tags'], + 'edited': updated_rant['edited'], + 'type': updated_rant['type'] + } + user_data = { + 'id': updated_rant['user_id'], + 'username': updated_rant['username'], + 'score': updated_rant['user_score'], + 'avatar_b': updated_rant['avatar_b'], + 'avatar_i': updated_rant['avatar_i'] + } return { "success": True, - "rant": format_rant(rant_data, user_data, current_user_id) + "rant": await format_rant(rant_data, user_data, current_user_id) } @app.post("/api/devrant/rants/{rant_id}/favorite") @@ -700,21 +696,17 @@ async def favorite_rant( token_key: str = Form(...), user_id: int = Form(...) ): - current_user_id = get_current_user(token_id, token_key, user_id) + current_user_id = await get_current_user(token_id, token_key, user_id) if not current_user_id: return {"success": False, "error": "Authentication required"} - conn = get_db() - c = conn.cursor() - try: - c.execute('INSERT INTO favorites (user_id, rant_id) VALUES (?, ?)', - (current_user_id, rant_id)) - conn.commit() - conn.close() + await db.insert("favorites", { + "user_id": current_user_id, + "rant_id": rant_id + }, return_id=True) return {"success": True} - except sqlite3.IntegrityError: - conn.close() + except Exception: return {"success": False, "error": "Already favorited"} @app.post("/api/devrant/rants/{rant_id}/unfavorite") @@ -725,18 +717,14 @@ async def unfavorite_rant( token_key: str = Form(...), user_id: int = Form(...) ): - current_user_id = get_current_user(token_id, token_key, user_id) + current_user_id = await get_current_user(token_id, token_key, user_id) if not current_user_id: return {"success": False, "error": "Authentication required"} - conn = get_db() - c = conn.cursor() - - c.execute('DELETE FROM favorites WHERE user_id = ? AND rant_id = ?', - (current_user_id, rant_id)) - - conn.commit() - conn.close() + await db.delete("favorites", { + "user_id": current_user_id, + "rant_id": rant_id + }) return {"success": True} @@ -750,17 +738,12 @@ async def create_comment( user_id: int = Form(...), image: Optional[UploadFile] = File(None) ): - current_user_id = get_current_user(token_id, token_key, user_id) + current_user_id = await get_current_user(token_id, token_key, user_id) if not current_user_id: return {"success": False, "confirmed": False} - conn = get_db() - c = conn.cursor() - # Check if rant exists - c.execute('SELECT id FROM rants WHERE id = ?', (rant_id,)) - if not c.fetchone(): - conn.close() + if not await db.exists("rants", {"id": rant_id}): return {"success": False, "error": "Rant not found"} # Handle image upload @@ -778,23 +761,26 @@ async def create_comment( # Create comment created_time = int(datetime.now().timestamp()) - c.execute('''INSERT INTO comments (rant_id, user_id, body, created_time, attached_image) - VALUES (?, ?, ?, ?, ?)''', - (rant_id, current_user_id, comment, created_time, image_path)) - - comment_id = c.lastrowid + comment_id = await db.insert("comments", { + "rant_id": rant_id, + "user_id": current_user_id, + "body": comment, + "created_time": created_time, + "attached_image": image_path + }, return_id=True) # Create notification for rant owner - c.execute('SELECT user_id FROM rants WHERE id = ?', (rant_id,)) - rant_owner = c.fetchone() + rant = await db.get("rants", {"id": rant_id}) - if rant_owner and rant_owner['user_id'] != current_user_id: - c.execute('''INSERT INTO notifications (user_id, type, rant_id, comment_id, from_user_id, created_time) - VALUES (?, ?, ?, ?, ?, ?)''', - (rant_owner['user_id'], 'comment', rant_id, comment_id, current_user_id, created_time)) - - conn.commit() - conn.close() + if rant and rant['user_id'] != current_user_id: + await db.insert("notifications", { + "user_id": rant['user_id'], + "type": "comment", + "rant_id": rant_id, + "comment_id": comment_id, + "from_user_id": current_user_id, + "created_time": created_time + }, return_id=True) return {"success": True} @@ -806,32 +792,38 @@ async def get_comment( token_key: Optional[str] = None, user_id: Optional[int] = None ): - current_user_id = get_current_user(token_id, token_key, user_id) if token_id else None + current_user_id = await get_current_user(token_id, token_key, user_id) if token_id else None - conn = get_db() - c = conn.cursor() + row = await db.query_one( + """SELECT c.*, u.id as user_id, u.username, u.score as user_score, + u.avatar_b, u.avatar_i + FROM comments c + JOIN users u ON c.user_id = u.id + WHERE c.id = ?""", + (comment_id,) + ) - c.execute('''SELECT c.*, u.* FROM comments c - JOIN users u ON c.user_id = u.id - WHERE c.id = ?''', (comment_id,)) - - row = c.fetchone() if not row: - conn.close() return {"success": False, "error": "Invalid comment specified in path."} - comment_data = {col: row[col] for col in row.keys() if col in - ['id', 'rant_id', 'body', 'score', 'created_time']} - user_data = {col: row[col] for col in row.keys() if col in - ['username', 'avatar_b', 'avatar_i']} - user_data['id'] = row['user_id'] - user_data['score'] = row['score'] - - conn.close() + comment_data = { + 'id': row['id'], + 'rant_id': row['rant_id'], + 'body': row['body'], + 'score': row['score'], + 'created_time': row['created_time'] + } + user_data = { + 'id': row['user_id'], + 'username': row['username'], + 'score': row['user_score'], + 'avatar_b': row['avatar_b'], + 'avatar_i': row['avatar_i'] + } return { "success": True, - "comment": format_comment(comment_data, user_data, current_user_id) + "comment": await format_comment(comment_data, user_data, current_user_id) } @app.post("/api/comments/{comment_id}") @@ -843,31 +835,24 @@ async def update_comment( token_key: str = Form(...), user_id: int = Form(...) ): - current_user_id = get_current_user(token_id, token_key, user_id) + current_user_id = await get_current_user(token_id, token_key, user_id) if not current_user_id: return {"success": False, "error": "Authentication required"} - conn = get_db() - c = conn.cursor() - # Check ownership - c.execute('SELECT user_id FROM comments WHERE id = ?', (comment_id,)) - comment_row = c.fetchone() + comment_row = await db.get("comments", {"id": comment_id}) if not comment_row: - conn.close() return {"success": False, "error": "Invalid comment specified in path."} if comment_row['user_id'] != current_user_id: - conn.close() return {"success": False, "fail_reason": "Unauthorized"} # Update comment - c.execute('UPDATE comments SET body = ?, edited = 1 WHERE id = ?', - (comment, comment_id)) - - conn.commit() - conn.close() + await db.update("comments", { + "body": comment, + "edited": 1 + }, {"id": comment_id}) return {"success": True} @@ -879,31 +864,22 @@ async def delete_comment( token_key: str = None, user_id: int = None ): - current_user_id = get_current_user(token_id, token_key, user_id) + current_user_id = await get_current_user(token_id, token_key, user_id) if not current_user_id: return {"success": False, "error": "Authentication required"} - conn = get_db() - c = conn.cursor() - # Check ownership - c.execute('SELECT user_id FROM comments WHERE id = ?', (comment_id,)) - comment_row = c.fetchone() + comment_row = await db.get("comments", {"id": comment_id}) if not comment_row: - conn.close() return {"success": False, "error": "Invalid comment specified in path."} if comment_row['user_id'] != current_user_id: - conn.close() return {"success": False, "error": "Unauthorized"} # Delete comment and related data - c.execute('DELETE FROM votes WHERE target_id = ? AND target_type = ?', (comment_id, 'comment')) - c.execute('DELETE FROM comments WHERE id = ?', (comment_id,)) - - conn.commit() - conn.close() + await db.delete("votes", {"target_id": comment_id, "target_type": "comment"}) + await db.delete("comments", {"id": comment_id}) return {"success": True} @@ -917,51 +893,54 @@ async def vote_comment( token_key: str = Form(...), user_id: int = Form(...) ): - current_user_id = get_current_user(token_id, token_key, user_id) + current_user_id = await get_current_user(token_id, token_key, user_id) if not current_user_id: return {"success": False, "error": "Authentication required"} - conn = get_db() - c = conn.cursor() - # Get comment - c.execute('SELECT * FROM comments WHERE id = ?', (comment_id,)) - comment = c.fetchone() + comment = await db.get("comments", {"id": comment_id}) if not comment: - conn.close() return {"success": False, "error": "Invalid comment specified in path."} # Check for existing vote - c.execute('SELECT * FROM votes WHERE user_id = ? AND target_id = ? AND target_type = ?', - (current_user_id, comment_id, 'comment')) - existing_vote = c.fetchone() + existing_vote = await db.get("votes", { + "user_id": current_user_id, + "target_id": comment_id, + "target_type": "comment" + }) if vote == 0: # Remove vote if existing_vote: - c.execute('DELETE FROM votes WHERE id = ?', (existing_vote['id'],)) + await db.delete("votes", {"id": existing_vote['id']}) # Update score - c.execute('UPDATE comments SET score = score - ? WHERE id = ?', - (existing_vote['vote'], comment_id)) + await db.update("comments", { + "score": comment['score'] - existing_vote['vote'] + }, {"id": comment_id}) else: if existing_vote: # Update vote score_diff = vote - existing_vote['vote'] - c.execute('UPDATE votes SET vote = ?, reason = ? WHERE id = ?', - (vote, reason, existing_vote['id'])) - c.execute('UPDATE comments SET score = score + ? WHERE id = ?', - (score_diff, comment_id)) + await db.update("votes", { + "vote": vote, + "reason": reason + }, {"id": existing_vote['id']}) + await db.update("comments", { + "score": comment['score'] + score_diff + }, {"id": comment_id}) else: # New vote - c.execute('''INSERT INTO votes (user_id, target_id, target_type, vote, reason) - VALUES (?, ?, ?, ?, ?)''', - (current_user_id, comment_id, 'comment', vote, reason)) - c.execute('UPDATE comments SET score = score + ? WHERE id = ?', - (vote, comment_id)) - - conn.commit() - conn.close() + await db.insert("votes", { + "user_id": current_user_id, + "target_id": comment_id, + "target_type": "comment", + "vote": vote, + "reason": reason + }, return_id=True) + await db.update("comments", { + "score": comment['score'] + vote + }, {"id": comment_id}) return {"success": True} @@ -973,75 +952,113 @@ async def get_profile( token_key: Optional[str] = None, auth_user_id: Optional[int] = None ): - current_user_id = get_current_user(token_id, token_key, auth_user_id) if token_id else None - - conn = get_db() - c = conn.cursor() + current_user_id = await get_current_user(token_id, token_key, auth_user_id) if token_id else None # Get user - c.execute('SELECT * FROM users WHERE id = ?', (user_id,)) - user = c.fetchone() + user = await db.get("users", {"id": user_id}) if not user: - conn.close() return {"success": False, "error": "User not found"} # Get user's rants - c.execute('''SELECT r.*, u.* FROM rants r - JOIN users u ON r.user_id = u.id - WHERE r.user_id = ? - ORDER BY r.created_time DESC - LIMIT 50''', (user_id,)) + rant_rows = await db.query_raw( + """SELECT r.*, u.id as user_id, u.username, u.score as user_score, + u.avatar_b, u.avatar_i + FROM rants r + JOIN users u ON r.user_id = u.id + WHERE r.user_id = ? + ORDER BY r.created_time DESC + LIMIT 50""", + (user_id,) + ) rants = [] - for row in c.fetchall(): - rant_data = {col: row[col] for col in row.keys() if col in - ['id', 'text', 'score', 'created_time', 'attached_image', 'tags', 'edited', 'type']} - user_data = {col: row[col] for col in row.keys() if col in - ['username', 'avatar_b', 'avatar_i']} - user_data['id'] = row['user_id'] - user_data['score'] = row['score'] + for row in rant_rows: + rant_data = { + 'id': row['id'], + 'text': row['text'], + 'score': row['score'], + 'created_time': row['created_time'], + 'attached_image': row['attached_image'], + 'tags': row['tags'], + 'edited': row['edited'], + 'type': row['type'] + } + user_data = { + 'id': row['user_id'], + 'username': row['username'], + 'score': row['user_score'], + 'avatar_b': row['avatar_b'], + 'avatar_i': row['avatar_i'] + } - rants.append(format_rant(rant_data, user_data, current_user_id)) + rants.append(await format_rant(rant_data, user_data, current_user_id)) # Get user's comments - c.execute('''SELECT c.*, u.* FROM comments c - JOIN users u ON c.user_id = u.id - WHERE c.user_id = ? - ORDER BY c.created_time DESC - LIMIT 50''', (user_id,)) + comment_rows = await db.query_raw( + """SELECT c.*, u.id as user_id, u.username, u.score as user_score, + u.avatar_b, u.avatar_i + FROM comments c + JOIN users u ON c.user_id = u.id + WHERE c.user_id = ? + ORDER BY c.created_time DESC + LIMIT 50""", + (user_id,) + ) comments = [] - for row in c.fetchall(): - comment_data = {col: row[col] for col in row.keys() if col in - ['id', 'rant_id', 'body', 'score', 'created_time']} - user_data = {col: row[col] for col in row.keys() if col in - ['username', 'avatar_b', 'avatar_i']} - user_data['id'] = row['user_id'] - user_data['score'] = row['score'] + for row in comment_rows: + comment_data = { + 'id': row['id'], + 'rant_id': row['rant_id'], + 'body': row['body'], + 'score': row['score'], + 'created_time': row['created_time'] + } + user_data = { + 'id': row['user_id'], + 'username': row['username'], + 'score': row['user_score'], + 'avatar_b': row['avatar_b'], + 'avatar_i': row['avatar_i'] + } - comments.append(format_comment(comment_data, user_data, current_user_id)) + comments.append(await format_comment(comment_data, user_data, current_user_id)) # Get favorited rants - c.execute('''SELECT r.*, u.* FROM rants r - JOIN users u ON r.user_id = u.id - JOIN favorites f ON f.rant_id = r.id - WHERE f.user_id = ? - ORDER BY f.id DESC - LIMIT 50''', (user_id,)) + favorite_rows = await db.query_raw( + """SELECT r.*, u.id as user_id, u.username, u.score as user_score, + u.avatar_b, u.avatar_i + FROM rants r + JOIN users u ON r.user_id = u.id + JOIN favorites f ON f.rant_id = r.id + WHERE f.user_id = ? + ORDER BY f.id DESC + LIMIT 50""", + (user_id,) + ) favorites = [] - for row in c.fetchall(): - rant_data = {col: row[col] for col in row.keys() if col in - ['id', 'text', 'score', 'created_time', 'attached_image', 'tags', 'edited', 'type']} - user_data = {col: row[col] for col in row.keys() if col in - ['username', 'avatar_b', 'avatar_i']} - user_data['id'] = row['user_id'] - user_data['score'] = row['score'] + for row in favorite_rows: + rant_data = { + 'id': row['id'], + 'text': row['text'], + 'score': row['score'], + 'created_time': row['created_time'], + 'attached_image': row['attached_image'], + 'tags': row['tags'], + 'edited': row['edited'], + 'type': row['type'] + } + user_data = { + 'id': row['user_id'], + 'username': row['username'], + 'score': row['user_score'], + 'avatar_b': row['avatar_b'], + 'avatar_i': row['avatar_i'] + } - favorites.append(format_rant(rant_data, user_data, current_user_id)) - - conn.close() + favorites.append(await format_rant(rant_data, user_data, current_user_id)) return { "success": True, @@ -1073,13 +1090,7 @@ async def get_user_id( username: str, app: int = 3 ): - conn = get_db() - c = conn.cursor() - - c.execute('SELECT id FROM users WHERE username = ?', (username,)) - user = c.fetchone() - - conn.close() + user = await db.get("users", {"username": username}) if not user: return {"success": False, "error": "User not found"} @@ -1094,30 +1105,41 @@ async def search( token_key: Optional[str] = None, user_id: Optional[int] = None ): - current_user_id = get_current_user(token_id, token_key, user_id) if token_id else None - - conn = get_db() - c = conn.cursor() + current_user_id = await get_current_user(token_id, token_key, user_id) if token_id else None # Search rants - c.execute('''SELECT r.*, u.* FROM rants r - JOIN users u ON r.user_id = u.id - WHERE r.text LIKE ? OR r.tags LIKE ? - ORDER BY r.score DESC - LIMIT 50''', (f'%{term}%', f'%{term}%')) + rows = await db.query_raw( + """SELECT r.*, u.id as user_id, u.username, u.score as user_score, + u.avatar_b, u.avatar_i + FROM rants r + JOIN users u ON r.user_id = u.id + WHERE r.text LIKE ? OR r.tags LIKE ? + ORDER BY r.score DESC + LIMIT 50""", + (f'%{term}%', f'%{term}%') + ) results = [] - for row in c.fetchall(): - rant_data = {col: row[col] for col in row.keys() if col in - ['id', 'text', 'score', 'created_time', 'attached_image', 'tags', 'edited', 'type']} - user_data = {col: row[col] for col in row.keys() if col in - ['username', 'avatar_b', 'avatar_i']} - user_data['id'] = row['user_id'] - user_data['score'] = row['score'] + for row in rows: + rant_data = { + 'id': row['id'], + 'text': row['text'], + 'score': row['score'], + 'created_time': row['created_time'], + 'attached_image': row['attached_image'], + 'tags': row['tags'], + 'edited': row['edited'], + 'type': row['type'] + } + user_data = { + 'id': row['user_id'], + 'username': row['username'], + 'score': row['user_score'], + 'avatar_b': row['avatar_b'], + 'avatar_i': row['avatar_i'] + } - results.append(format_rant(rant_data, user_data, current_user_id)) - - conn.close() + results.append(await format_rant(rant_data, user_data, current_user_id)) return {"success": True, "results": results} @@ -1130,24 +1152,25 @@ async def get_notifications( token_key: str = None, user_id: int = None ): - current_user_id = get_current_user(token_id, token_key, user_id) + current_user_id = await get_current_user(token_id, token_key, user_id) if not current_user_id: return {"success": False, "error": "Authentication required"} - conn = get_db() - c = conn.cursor() - # Get notifications - c.execute('''SELECT n.*, u.username FROM notifications n - LEFT JOIN users u ON n.from_user_id = u.id - WHERE n.user_id = ? - ORDER BY n.created_time DESC - LIMIT 50''', (current_user_id,)) + rows = await db.query_raw( + """SELECT n.*, u.username + FROM notifications n + LEFT JOIN users u ON n.from_user_id = u.id + WHERE n.user_id = ? + ORDER BY n.created_time DESC + LIMIT 50""", + (current_user_id,) + ) items = [] unread_count = 0 - for row in c.fetchall(): + for row in rows: item = { "type": row['type'], "rant_id": row['rant_id'], @@ -1163,9 +1186,7 @@ async def get_notifications( unread_count += 1 # Mark as read - c.execute('UPDATE notifications SET read = 1 WHERE user_id = ?', (current_user_id,)) - conn.commit() - conn.close() + await db.update("notifications", {"read": 1}, {"user_id": current_user_id}) return { "success": True, @@ -1192,17 +1213,11 @@ async def clear_notifications( token_key: str = Form(...), user_id: int = Form(...) ): - current_user_id = get_current_user(token_id, token_key, user_id) + current_user_id = await get_current_user(token_id, token_key, user_id) if not current_user_id: return {"success": False, "error": "Authentication required"} - conn = get_db() - c = conn.cursor() - - c.execute('DELETE FROM notifications WHERE user_id = ?', (current_user_id,)) - - conn.commit() - conn.close() + await db.delete("notifications", {"user_id": current_user_id}) return {"success": True} @@ -1218,20 +1233,17 @@ async def edit_profile( token_key: str = Form(...), user_id: int = Form(...) ): - current_user_id = get_current_user(token_id, token_key, user_id) + current_user_id = await get_current_user(token_id, token_key, user_id) if not current_user_id: return {"success": False, "error": "Authentication required"} - conn = get_db() - c = conn.cursor() - - c.execute('''UPDATE users SET about = ?, skills = ?, location = ?, website = ?, github = ? - WHERE id = ?''', - (profile_about, profile_skills, profile_location, profile_website, - profile_github, current_user_id)) - - conn.commit() - conn.close() + await db.update("users", { + "about": profile_about, + "skills": profile_skills, + "location": profile_location, + "website": profile_website, + "github": profile_github + }, {"id": current_user_id}) return {"success": True} @@ -1250,7 +1262,7 @@ async def resend_confirmation( token_key: str = Form(...), user_id: int = Form(...) ): - current_user_id = get_current_user(token_id, token_key, user_id) + current_user_id = await get_current_user(token_id, token_key, user_id) if not current_user_id: return {"success": False, "error": "Authentication required"} @@ -1265,7 +1277,7 @@ async def mark_news_read( token_key: str = Form(...), user_id: int = Form(...) ): - current_user_id = get_current_user(token_id, token_key, user_id) + current_user_id = await get_current_user(token_id, token_key, user_id) if not current_user_id: return {"success": False, "error": "Authentication required"}