Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
276 changes: 236 additions & 40 deletions backend/data/blooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,203 @@
from typing import Any, Dict, List, Optional

from data.connection import db_cursor
from data.users import User

from data.users import User, get_user_by_id

@dataclass
class Bloom:
id: int
sender: User
content: str
sent_timestamp: datetime.datetime
rebloom_count: int = 0
original_bloom_id: Optional[int] = None
is_rebloom: bool = False
original_sender: Optional[User] = None


def add_bloom(*, sender: User, content: str) -> Bloom:
def add_bloom(
*,
sender: User,
content: str,
is_rebloom: bool = False,
original_bloom_id: Optional[int] = None
) -> int:
"""Create a new bloom and associate any hashtags."""

hashtags = [word[1:] for word in content.split(" ") if word.startswith("#")]

now = datetime.datetime.now(tz=datetime.UTC)
bloom_id = int(now.timestamp() * 1000000)

with db_cursor() as cur:
cur.execute(
"INSERT INTO blooms (id, sender_id, content, send_timestamp) VALUES (%(bloom_id)s, %(sender_id)s, %(content)s, %(timestamp)s)",
dict(
bloom_id=bloom_id,
sender_id=sender.id,
try:
cur.execute(
"""
INSERT INTO blooms
(id, sender_id, content, send_timestamp, original_bloom_id)
VALUES
(%(bloom_id)s, %(sender_id)s, %(content)s, %(timestamp)s, %(original_bloom_id)s)
""",
dict(
bloom_id=bloom_id,
sender_id=sender.id,
content=content,
timestamp=now, # Pass Python datetime object to resolve SQL type error
original_bloom_id=original_bloom_id,
),
)

for hashtag in hashtags:
cur.execute(
"INSERT INTO hashtags (hashtag, bloom_id) VALUES (%(hashtag)s, %(bloom_id)s)",
dict(hashtag=hashtag, bloom_id=bloom_id),
)

return bloom_id

except Exception as e:
# Keep error logging for debugging purposes
print(f"Error adding bloom: {e}")
# Returning 0 or raising an error are typical ways to indicate failure
return 0

def add_rebloom(user_id: int, original_bloom_id: int, content: str) -> Optional[int]:
"""Create a new rebloom"""

# 💥 FIX: Fetch the complete User object using the user_id
# This prevents the "missing required positional arguments" error
sender_user = get_user_by_id(user_id)

if sender_user is None:
print(f"Error adding rebloom: User with ID {user_id} not found.")
return None

with db_cursor() as cur:
try:
# First create the new bloom
rebloom_bloom_id = add_bloom(
sender=sender_user, # <<< Use the fully hydrated User object
content=content,
timestamp=datetime.datetime.now(datetime.UTC),
),
is_rebloom=True,
original_bloom_id=original_bloom_id
)

if rebloom_bloom_id:
# Create the rebloom relationship
cur.execute(
"INSERT INTO reblooms (user_id, original_bloom_id, rebloom_bloom_id) VALUES (%s, %s, %s) RETURNING id",
(user_id, original_bloom_id, rebloom_bloom_id)
)
result = cur.fetchone()
if result is None:
return None
rebloom_id = result[0]

# Update rebloom count on original bloom
# This step will now execute successfully since the code no longer crashes above
cur.execute(
"UPDATE blooms SET rebloom_count = rebloom_count + 1 WHERE id = %s",
(original_bloom_id,)
)

return rebloom_id
return None
except Exception as e:
# The count should now increase before any exception related to DB occurs
print(f"Error adding rebloom: {e}")
return None

def get_rebloom_by_user_and_bloom(user_id: int, original_bloom_id: int) -> Optional[Dict]:
"""Check if user has already rebloomed a specific bloom"""
with db_cursor() as cur:
cur.execute(
"SELECT * FROM reblooms WHERE user_id = %s AND original_bloom_id = %s",
(user_id, original_bloom_id)
)
for hashtag in hashtags:
row = cur.fetchone()
if row:
return {
'id': row[0],
'user_id': row[1],
'original_bloom_id': row[2],
'rebloom_bloom_id': row[3],
'created_at': row[4]
}
return None


def delete_rebloom(rebloom_id: int) -> bool:
"""Delete a rebloom and its associated bloom"""
with db_cursor() as cur:
try:
# Get the rebloom details first
cur.execute("SELECT rebloom_bloom_id, original_bloom_id FROM reblooms WHERE id = %s", (rebloom_id,))
rebloom_data = cur.fetchone()

if not rebloom_data:
return False

rebloom_bloom_id, original_bloom_id = rebloom_data

# Delete the rebloom bloom
cur.execute("DELETE FROM blooms WHERE id = %s", (rebloom_bloom_id,))

# Delete the rebloom relationship
cur.execute("DELETE FROM reblooms WHERE id = %s", (rebloom_id,))

# Decrement rebloom count on original bloom
cur.execute(
"INSERT INTO hashtags (hashtag, bloom_id) VALUES (%(hashtag)s, %(bloom_id)s)",
dict(hashtag=hashtag, bloom_id=bloom_id),
"UPDATE blooms SET rebloom_count = rebloom_count - 1 WHERE id = %s",
(original_bloom_id,)
)

return True
except Exception as e:
print(f"Error deleting rebloom: {e}")
return False


def has_user_rebloomed(user_id: int, bloom_id: int) -> bool:
"""Check if user has rebloomed a specific bloom"""
return get_rebloom_by_user_and_bloom(user_id, bloom_id) is not None


def get_rebloom_count(bloom_id: int) -> int:
"""Get the number of times a bloom has been rebloomed"""
with db_cursor() as cur:
cur.execute("SELECT rebloom_count FROM blooms WHERE id = %s", (bloom_id,))
result = cur.fetchone()
return result[0] if result else 0


def get_user_reblooms(user_id: int) -> List[Dict]:
"""Get all reblooms by a user"""
with db_cursor() as cur:
cur.execute("""
SELECT r.*, b.content, b.send_timestamp, ob.sender_id as original_sender_id,
ou.username as original_username, ob.id as original_bloom_id
FROM reblooms r
JOIN blooms b ON r.rebloom_bloom_id = b.id
JOIN blooms ob ON r.original_bloom_id = ob.id
JOIN users ou ON ob.sender_id = ou.id
WHERE r.user_id = %s
ORDER BY r.created_at DESC
""", (user_id,))

reblooms = []
for row in cur.fetchall():
reblooms.append({
'id': row[0],
'user_id': row[1],
'original_bloom_id': row[2],
'rebloom_bloom_id': row[3],
'created_at': row[4],
'content': row[5],
'rebloom_timestamp': row[6],
'original_sender_id': row[7],
'original_username': row[8],
'original_bloom_id': row[9]
})
return reblooms


def get_blooms_for_user(
Expand All @@ -54,7 +220,7 @@ def get_blooms_for_user(

cur.execute(
f"""SELECT
blooms.id, users.username, content, send_timestamp
blooms.id, users.username, content, send_timestamp, rebloom_count, original_bloom_id
FROM
blooms INNER JOIN users ON users.id = blooms.sender_id
WHERE
Expand All @@ -66,36 +232,57 @@ def get_blooms_for_user(
kwargs,
)
rows = cur.fetchall()
blooms = []
blooms_list = []
for row in rows:
bloom_id, sender_username, content, timestamp = row
blooms.append(
Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
)
bloom_id, sender_username, content, timestamp, rebloom_count, original_bloom_id = row
bloom = Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
rebloom_count=rebloom_count,
original_bloom_id=original_bloom_id,
is_rebloom=original_bloom_id is not None
)
return blooms

# If this is a rebloom, get original sender info
if original_bloom_id:
original_bloom = get_bloom(original_bloom_id)
if original_bloom:
bloom.original_sender = original_bloom.sender

blooms_list.append(bloom)
return blooms_list


def get_bloom(bloom_id: int) -> Optional[Bloom]:
with db_cursor() as cur:
cur.execute(
"SELECT blooms.id, users.username, content, send_timestamp FROM blooms INNER JOIN users ON users.id = blooms.sender_id WHERE blooms.id = %s",
"SELECT blooms.id, users.username, content, send_timestamp, rebloom_count, original_bloom_id FROM blooms INNER JOIN users ON users.id = blooms.sender_id WHERE blooms.id = %s",
(bloom_id,),
)
row = cur.fetchone()
if row is None:
return None
bloom_id, sender_username, content, timestamp = row
return Bloom(
bloom_id, sender_username, content, timestamp, rebloom_count, original_bloom_id = row

bloom = Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
rebloom_count=rebloom_count,
original_bloom_id=original_bloom_id,
is_rebloom=original_bloom_id is not None
)

# If this is a rebloom, get original sender info
if original_bloom_id:
original_bloom = get_bloom(original_bloom_id)
if original_bloom:
bloom.original_sender = original_bloom.sender

return bloom


def get_blooms_with_hashtag(
Expand All @@ -108,7 +295,7 @@ def get_blooms_with_hashtag(
with db_cursor() as cur:
cur.execute(
f"""SELECT
blooms.id, users.username, content, send_timestamp
blooms.id, users.username, content, send_timestamp, rebloom_count, original_bloom_id
FROM
blooms INNER JOIN hashtags ON blooms.id = hashtags.bloom_id INNER JOIN users ON blooms.sender_id = users.id
WHERE
Expand All @@ -119,18 +306,27 @@ def get_blooms_with_hashtag(
kwargs,
)
rows = cur.fetchall()
blooms = []
blooms_list = []
for row in rows:
bloom_id, sender_username, content, timestamp = row
blooms.append(
Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
)
bloom_id, sender_username, content, timestamp, rebloom_count, original_bloom_id = row
bloom = Bloom(
id=bloom_id,
sender=sender_username,
content=content,
sent_timestamp=timestamp,
rebloom_count=rebloom_count,
original_bloom_id=original_bloom_id,
is_rebloom=original_bloom_id is not None
)
return blooms

# If this is a rebloom, get original sender info
if original_bloom_id:
original_bloom = get_bloom(original_bloom_id)
if original_bloom:
bloom.original_sender = original_bloom.sender

blooms_list.append(bloom)
return blooms_list


def make_limit_clause(limit: Optional[int], kwargs: Dict[Any, Any]) -> str:
Expand All @@ -139,4 +335,4 @@ def make_limit_clause(limit: Optional[int], kwargs: Dict[Any, Any]) -> str:
kwargs["limit"] = limit
else:
limit_clause = ""
return limit_clause
return limit_clause
1 change: 0 additions & 1 deletion backend/data/follows.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ def follow(follower: User, followee: User):
# Already following - treat as idempotent request.
pass


def get_followed_usernames(follower: User) -> List[str]:
"""get_followed_usernames returns a list of usernames followee follows."""
with db_cursor() as cur:
Expand Down
24 changes: 24 additions & 0 deletions backend/data/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,27 @@ def generate_salt() -> bytes:
def lookup_user(header_info, payload_info):
"""lookup_user is a hook for the jwt middleware to look-up authenticated users."""
return get_user(payload_info["sub"])

def get_user_by_id(user_id: int) -> Optional[User]:
"""
Retrieves a complete User object from the database by their ID.
This is necessary for operations like reblooming where only the ID is known.
"""
with db_cursor() as cur:
# NOTE: Adjust column names if they are different in your 'users' table
cur.execute(
"SELECT id, username, password_salt, password_scrypt FROM users WHERE id = %s",
(user_id,)
)
row = cur.fetchone()

if row:
user_id, username, password_salt, password_scrypt = row
# Create and return the full User object
return User(
id=user_id,
username=username,
password_salt=password_salt,
password_scrypt=password_scrypt
)
return None
Loading