Skip to content
Open
Changes from 1 commit
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
95 changes: 39 additions & 56 deletions src/bot/features/image_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import base64
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, Optional

from telegram import PhotoSize
Expand Down Expand Up @@ -38,36 +39,44 @@ def __init__(self, config: Settings):
async def process_image(
self, photo: PhotoSize, caption: Optional[str] = None
) -> ProcessedImage:
"""Process uploaded image"""
"""Process uploaded image — save to temp file and build a path-based prompt."""
import uuid

# Download image
# Download image bytes
file = await photo.get_file()
image_bytes = await file.download_as_bytearray()

# Detect image type
image_type = self._detect_image_type(image_bytes)
# Detect format and save to temp file so Claude CLI can read it
fmt = self._detect_format(bytes(image_bytes))
ext = fmt if fmt != "unknown" else "jpg"
temp_dir = Path("/tmp/claude_bot_files")
temp_dir.mkdir(exist_ok=True)
image_path = temp_dir / f"image_{uuid.uuid4()}.{ext}"
image_path.write_bytes(bytes(image_bytes))
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

The uploaded bytes are written to a fixed directory under /tmp without restricting permissions. By default this directory/file may be world-readable (depending on umask), which is risky for screenshots that can contain sensitive info. Consider creating the directory with a restrictive mode (e.g., 0o700) and writing the file with 0o600, or using tempfile APIs that create private files.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

If _detect_format() returns "unknown", the code still writes the bytes to disk and forces a .jpg extension. That can produce misleading files and bypass the existing size/format checks in validate_image(). Consider validating (size + known format) before writing, and raising a clear error instead of defaulting to .jpg.

Copilot uses AI. Check for mistakes.

# Create appropriate prompt
# Detect image type for prompt tailoring
image_type = self._detect_image_type(bytes(image_bytes))
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

Minor maintainability: uuid is imported inside the method and the image bytes are repeatedly converted via bytes(image_bytes). Consider importing uuid at module scope (consistent with FileHandler) and assigning bytes_data = bytes(image_bytes) once to reuse for format/type detection and write_bytes.

Copilot uses AI. Check for mistakes.

# Build prompt with actual file path so Claude CLI can see the image
if image_type == "screenshot":
prompt = self._create_screenshot_prompt(caption)
prompt = self._create_screenshot_prompt(caption, image_path)
elif image_type == "diagram":
prompt = self._create_diagram_prompt(caption)
prompt = self._create_diagram_prompt(caption, image_path)
elif image_type == "ui_mockup":
prompt = self._create_ui_prompt(caption)
prompt = self._create_ui_prompt(caption, image_path)
else:
prompt = self._create_generic_prompt(caption)
prompt = self._create_generic_prompt(caption, image_path)

# Convert to base64 for Claude (if supported in future)
base64_image = base64.b64encode(image_bytes).decode("utf-8")

return ProcessedImage(
prompt=prompt,
image_type=image_type,
base64_data=base64_image,
size=len(image_bytes),
metadata={
"format": self._detect_format(image_bytes),
"format": fmt,
"has_caption": caption is not None,
"temp_path": str(image_path),
},
)
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

This introduces a new persistent temp file on disk, but nothing deletes it after Claude finishes. Over time this can fill /tmp and leak old user data. Suggest adding a cleanup path (e.g., have the caller delete ProcessedImage.metadata['temp_path'] in a finally block after run_command, or implement a TTL-based cleanup routine for /tmp/claude_bot_files).

Copilot uses AI. Check for mistakes.

Expand All @@ -93,61 +102,35 @@ def _detect_format(self, image_bytes: bytes) -> str:
else:
return "unknown"

def _create_screenshot_prompt(self, caption: Optional[str]) -> str:
def _create_screenshot_prompt(
self, caption: Optional[str], image_path: Path
) -> str:
"""Create prompt for screenshot analysis"""
base_prompt = """I'm sharing a screenshot with you. Please analyze it and help me with:

1. Identifying what application or website this is from
2. Understanding the UI elements and their purpose
3. Any issues or improvements you notice
4. Answering any specific questions I have

"""
base = f"I'm sharing a screenshot with you. The image is saved at: {image_path}\n\nPlease analyze it and help me with:\n1. Identifying what application or website this is from\n2. Understanding the UI elements and their purpose\n3. Any issues or improvements you notice\n4. Answering any specific questions I have\n"
if caption:
base_prompt += f"Specific request: {caption}"
base += f"\nSpecific request: {caption}"
return base

return base_prompt

def _create_diagram_prompt(self, caption: Optional[str]) -> str:
def _create_diagram_prompt(self, caption: Optional[str], image_path: Path) -> str:
"""Create prompt for diagram analysis"""
base_prompt = """I'm sharing a diagram with you. Please help me:

1. Understand the components and their relationships
2. Identify the type of diagram (flowchart, architecture, etc.)
3. Explain any technical concepts shown
4. Suggest improvements or clarifications

"""
base = f"I'm sharing a diagram with you. The image is saved at: {image_path}\n\nPlease help me:\n1. Understand the components and their relationships\n2. Identify the type of diagram\n3. Explain any technical concepts shown\n4. Suggest improvements or clarifications\n"
if caption:
base_prompt += f"Specific request: {caption}"
base += f"\nSpecific request: {caption}"
return base

return base_prompt

def _create_ui_prompt(self, caption: Optional[str]) -> str:
def _create_ui_prompt(self, caption: Optional[str], image_path: Path) -> str:
"""Create prompt for UI mockup analysis"""
base_prompt = """I'm sharing a UI mockup with you. Please analyze:

1. The layout and visual hierarchy
2. User experience considerations
3. Accessibility aspects
4. Implementation suggestions
5. Any potential improvements

"""
base = f"I'm sharing a UI mockup with you. The image is saved at: {image_path}\n\nPlease analyze:\n1. The layout and visual hierarchy\n2. UX improvements\n3. Accessibility concerns\n"
if caption:
base_prompt += f"Specific request: {caption}"
base += f"\nSpecific request: {caption}"
return base

return base_prompt

def _create_generic_prompt(self, caption: Optional[str]) -> str:
def _create_generic_prompt(self, caption: Optional[str], image_path: Path) -> str:
"""Create generic image analysis prompt"""
base_prompt = """I'm sharing an image with you. Please analyze it and provide relevant insights.

"""
base = f"I'm sharing an image with you. The image is saved at: {image_path}\n\nPlease analyze and describe what you see.\n"
if caption:
base_prompt += f"Context: {caption}"

return base_prompt
base += f"\nSpecific request: {caption}"
return base

def supports_format(self, filename: str) -> bool:
"""Check if image format is supported"""
Expand Down
Loading