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
116 changes: 24 additions & 92 deletions backend/app/api/routes/scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from typing import Dict, Any, Optional, List
import uuid

from ...services.parser.renpy_parser import RenPyParser, ChoiceNode, ChoiceNodeType
from ...services.database import DatabaseService
from ...models.exceptions import ResourceNotFoundException, DatabaseException
from ...services.websocket import connection_manager
Expand All @@ -23,24 +22,6 @@

# Initialize services
db_service = DatabaseService()
parser = RenPyParser()

# Helper functions
def node_to_dict(node: ChoiceNode) -> Dict[str, Any]:
"""Convert a ChoiceNode to a dictionary with line references for JSON serialization."""
result = {
"id": str(id(node)), # Generate a unique ID using the object's memory address
"node_type": node.node_type.value if hasattr(node.node_type, "value") else str(node.node_type),
"label_name": node.label_name,
"start_line": node.start_line,
"end_line": node.end_line,
"children": [node_to_dict(child) for child in node.children]
}

if hasattr(node, "false_branch") and node.false_branch:
result["false_branch"] = [node_to_dict(opt) for opt in node.false_branch]

return result

# Routes
@scripts_router.post("/parse", response_model=Dict[str, Any])
Expand All @@ -51,14 +32,15 @@ async def parse_script(
current_user: Dict[str, Any] = Depends(get_current_user)
) -> Dict[str, Any]:
"""
Parse a RenPy script file and return its tree structure with line references.
Save a RenPy script file.
NOTE: Parsing is now handled on the frontend. This endpoint just saves the file.

Args:
file: The uploaded RenPy script file
project_id: ID of the project to associate the script with

Returns:
JSON representation of the parsed script tree with line references
JSON representation of the script metadata. Tree is empty.
"""
try:
# current_user now injected via Depends
Expand All @@ -82,22 +64,11 @@ async def parse_script(
if not has_access:
raise HTTPException(status_code=403, detail="Access denied to the specified project")

# Parse the content to check validity
temp_dir = Path(tempfile.gettempdir()) / "renpy_editor" / str(uuid.uuid4())
temp_dir.mkdir(parents=True, exist_ok=True)

temp_file = temp_dir / file.filename
with open(temp_file, "wb") as f:
f.write(content)

# Parse the file to build the tree
parsed_tree = await parser.parse_async(str(temp_file))

decoded_content = content.decode('utf-8')

# Check if script with this filename already exists in the project
existing_script = db_service.get_script_by_filename(project_id, file.filename)

decoded_content = content.decode('utf-8')

if existing_script:
# Update existing script
script_id = existing_script["id"]
Expand All @@ -115,19 +86,16 @@ async def parse_script(
user_id=current_user["id"]
)

# Clean up temp file
background_tasks.add_task(shutil.rmtree, temp_dir, ignore_errors=True)

# Return result
# Return result with empty tree (frontend handles parsing)
result = {
"script_id": script_id,
"filename": file.filename,
"tree": node_to_dict(parsed_tree)
"tree": {}
}

return result
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error parsing script: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error saving script: {str(e)}")
Comment on lines 97 to +98
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: HTTPException is swallowed and re-raised as 500.

Every other endpoint in this file has except HTTPException: raise before the catch-all except Exception. This endpoint is missing it, so HTTPExceptions raised at Lines 48, 51, 58, and 65 (status codes 400 and 403) will be caught here and re-raised as a generic 500 error.

🐛 Proposed fix
     return result
+  except HTTPException:
+      raise
   except Exception as e:
-      raise HTTPException(status_code=500, detail=f"Error saving script: {str(e)}")
+      raise HTTPException(status_code=500, detail=f"Error saving script: {str(e)}") from e
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 97-97: Do not catch blind exception: Exception

(BLE001)


[warning] 98-98: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


[warning] 98-98: Use explicit conversion flag

Replace with conversion flag

(RUF010)

🤖 Prompt for AI Agents
In `@backend/app/api/routes/scripts.py` around lines 97 - 98, The catch-all except
in backend/app/api/routes/scripts.py currently converts all exceptions into a
500 by doing "except Exception as e: raise HTTPException(...)", which swallows
existing HTTPException errors; add an explicit "except HTTPException: raise"
clause immediately before the generic "except Exception" in the same try/except
block (the endpoint that currently raises HTTPExceptions earlier in the
function) so HTTPException instances raised earlier (e.g., the 400/403 checks)
are re-raised unchanged, leaving the final catch-all to handle unexpected errors
and wrap them as a 500.


@scripts_router.get("/node-content/{script_id}", response_model=Dict[str, Any])
async def get_node_content(
Expand Down Expand Up @@ -240,11 +208,10 @@ async def update_node_content(
# Save changes to database
db_service.update_script(script_id, new_content, current_user["id"])

# Parse updated script and broadcast new structure to collaborators
parsed_tree = parser.parse_text(new_content)
await connection_manager.broadcast_structure_update(
script_id, node_to_dict(parsed_tree)
)
# Broadcast update
# Note: We no longer broadcast the full parsed tree because parsing is moved to frontend.
# Ideally, we should broadcast a "content_updated" event and let clients re-fetch or re-parse.
# For now, we omit the structure update broadcast.
Comment on lines +211 to +214

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Emit a structure/content update after edits

This change removes the only server-side trigger that tells other clients to reload the script graph after edits. The frontend only refreshes its parsed tree when it receives an updateStructure WebSocket message (see CollabContext’s updateStructure case and the reload hook in EditorPage), while node_updated is just logged. By omitting any broadcast here, collaborators editing the same script will never get a reload signal, so their graphs stay stale until a manual refresh. If parsing is now frontend-only, the backend still needs to emit an explicit “content_updated”/“updateStructure” event so other clients re-fetch and re-parse.

Useful? React with 👍 / 👎.

Comment on lines +211 to +214
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Collaborative editing: no broadcast on content mutation.

The comment acknowledges that tree broadcasting was removed, but there's no replacement event (e.g., content_updated) to notify other connected clients that the script changed. After update_node_content and insert_node, collaborators will see stale trees until they manually reload. If collaborative editing is a supported feature, consider broadcasting a lightweight content_updated event so clients know to re-parse.

🤖 Prompt for AI Agents
In `@backend/app/api/routes/scripts.py` around lines 211 - 214, After mutations
such as update_node_content and insert_node the server currently omits any
broadcast so other clients get stale trees; add a lightweight "content_updated"
broadcast after both update_node_content and insert_node calls (or factor into a
helper like broadcast_content_updated) that sends minimal metadata (script_id,
node_id, user_id/timestamp) to connected clients; locate the mutation handlers
in scripts.py and invoke this new broadcast helper immediately after successful
mutation and commit so clients can re-fetch/re-parse the updated content.


# Calculate new end line
new_end_line = start_line + new_line_count - 1
Expand Down Expand Up @@ -314,31 +281,13 @@ async def insert_node(
# Save changes to database
db_service.update_script(script_id, new_content, current_user["id"])

# Re-parse the entire script to update the tree
# Create temp file for parsing
temp_dir = Path(tempfile.gettempdir()) / "renpy_editor" / str(uuid.uuid4())
temp_dir.mkdir(parents=True, exist_ok=True)
temp_file = temp_dir / "temp_script.rpy"

with open(temp_file, "w", encoding="utf-8") as f:
f.write(new_content)

# Parse the updated file
parsed_tree = await parser.parse_async(str(temp_file))

# Clean up temp file
shutil.rmtree(temp_dir, ignore_errors=True)

# Broadcast updated structure to other clients
await connection_manager.broadcast_structure_update(
script_id, node_to_dict(parsed_tree)
)
# Note: We no longer broadcast the full parsed tree.

return {
"start_line": insertion_line,
"end_line": insertion_line + len(new_content_lines) - 1,
"line_count": len(new_content_lines),
"tree": node_to_dict(parsed_tree)
"tree": {} # Empty tree
}
except ResourceNotFoundException as e:
raise HTTPException(status_code=404, detail=str(e))
Expand Down Expand Up @@ -512,13 +461,13 @@ async def load_existing_script(
current_user: Dict[str, Any] = Depends(get_current_user)
) -> Dict[str, Any]:
"""
Load an existing script and return its parsed tree structure.
Load an existing script and return its content and empty tree.

Args:
script_id: The ID of the script to load

Returns:
JSON representation of the script with parsed tree
JSON representation of the script with content. Tree is empty.
"""
try:
# Get script from database
Expand All @@ -534,36 +483,19 @@ async def load_existing_script(
if not has_access:
raise HTTPException(status_code=403, detail="Access denied to this script")

# Parse the script content to build tree
temp_dir = Path(tempfile.gettempdir()) / "renpy_editor" / str(uuid.uuid4())
temp_dir.mkdir(parents=True, exist_ok=True)

temp_file = temp_dir / script["filename"]
with open(temp_file, "w", encoding='utf-8') as f:
f.write(script["content"])
# Return result
result = {
"script_id": script_id,
"filename": script["filename"],
"content": script["content"],
"tree": {} # Empty tree
}

try:
# Parse the file to build the tree
parsed_tree = await parser.parse_async(str(temp_file))

# Return result
result = {
"script_id": script_id,
"filename": script["filename"],
"tree": node_to_dict(parsed_tree)
}

return result
finally:
# Clean up temp file
import shutil
shutil.rmtree(temp_dir, ignore_errors=True)
return result

except ResourceNotFoundException as e:
raise HTTPException(status_code=404, detail=str(e))
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error loading script: {str(e)}")

# TODO: Add endpoints for version history retrieval - #issue/129
Empty file.
Loading