feat: Add terminal image endpoint and save images

Introduces a new API endpoint `/terminal_images` to serve generated terminal output images.

- Creates a new file `api_service/terminal_images_endpoint.py` to handle the static file serving.
- Modifies `api_service/api_server.py` to mount the new endpoint.
- Updates `cogs/terminal_cog.py` to save generated terminal images to a local directory (`terminal_images`) with unique filenames.
- Adds a base URL constant (`API_BASE_URL`) to `cogs/terminal_cog.py` for potential future use in generating image URLs.
This commit is contained in:
Slipstream 2025-05-17 19:29:05 -06:00
parent e7719406bf
commit c9d6cc40a6
Signed by: slipstream
GPG Key ID: 13E498CE010AC6FD
3 changed files with 122 additions and 33 deletions

View File

@ -567,6 +567,16 @@ app.mount("/dashboard/api", dashboard_api_app) # Mount the new dashboard API
try:
from api_service.webhook_endpoints import router as webhook_router # Relative import
app.mount("/webhook", webhook_router) # Mount directly on the main app for simplicity
# Import and mount terminal images endpoint
try:
from api_service.terminal_images_endpoint import mount_terminal_images
# Mount terminal images directory as static files
mount_terminal_images(app)
log.info("Terminal images endpoint mounted successfully")
except ImportError as e:
log.error(f"Could not import terminal images endpoint: {e}")
log.error("Terminal images endpoint will not be available")
# After mounting the webhook router
log.info("Available routes in webhook_router:")
from fastapi.routing import APIRoute, Mount

View File

@ -0,0 +1,57 @@
"""
Terminal Images Endpoint
This module provides an endpoint for serving terminal images generated by the terminal_cog.py.
"""
import os
from fastapi import APIRouter, HTTPException
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from typing import Optional
# Create a router for the terminal images endpoint
router = APIRouter(tags=["Terminal Images"])
# Path to the terminal_images directory
TERMINAL_IMAGES_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'terminal_images'))
# Ensure the terminal_images directory exists
os.makedirs(TERMINAL_IMAGES_DIR, exist_ok=True)
@router.get("/{filename}")
async def get_terminal_image(filename: str):
"""
Get a terminal image by filename.
Args:
filename: The filename of the terminal image
Returns:
The terminal image file
Raises:
HTTPException: If the file is not found
"""
file_path = os.path.join(TERMINAL_IMAGES_DIR, filename)
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail="Terminal image not found")
return FileResponse(file_path)
# Function to mount the terminal images directory as static files
def mount_terminal_images(app):
"""
Mount the terminal_images directory as static files.
Args:
app: The FastAPI app to mount the static files on
"""
# Check if the directory exists
if os.path.exists(TERMINAL_IMAGES_DIR) and os.path.isdir(TERMINAL_IMAGES_DIR):
# Mount the terminal_images directory as static files
app.mount("/terminal_images", StaticFiles(directory=TERMINAL_IMAGES_DIR), name="terminal_images")
print(f"Mounted terminal images directory: {TERMINAL_IMAGES_DIR}")
else:
print(f"Warning: Terminal images directory '{TERMINAL_IMAGES_DIR}' not found. Terminal images will not be available.")

View File

@ -5,6 +5,8 @@ from PIL import Image, ImageDraw, ImageFont
import subprocess
import os
import io
import uuid
import time
from collections import deque
import shlex # For safer command parsing if not using shell=True for everything
@ -24,6 +26,9 @@ MAX_HISTORY_LINES = 500 # Max lines to keep in history
AUTO_UPDATE_INTERVAL_SECONDS = 3
MAX_OUTPUT_LINES_PER_IMAGE = (IMG_HEIGHT - 2 * PADDING) // (FONT_SIZE + LINE_SPACING)
OWNER_ID = 452666956353503252
TERMINAL_IMAGES_DIR = "terminal_images" # Directory to store terminal images
# Use your actual domain or IP address here
API_BASE_URL = "https://slipstreamm.dev" # Base URL for the API
# --- Helper: Owner Check ---
async def is_owner_check(interaction: discord.Interaction) -> bool:
@ -69,8 +74,11 @@ class TerminalCog(commands.Cog, name="Terminal"):
# Fallback or raise error
# self.owner_id = YOUR_FALLBACK_OWNER_ID # if you have one
def _generate_terminal_image(self) -> io.BytesIO:
"""Generates an image of the current terminal output."""
def _generate_terminal_image(self) -> tuple[io.BytesIO, str]:
"""
Generates an image of the current terminal output.
Returns a tuple of (BytesIO object, filename)
"""
image = Image.new('RGB', (IMG_WIDTH, IMG_HEIGHT), BACKGROUND_COLOR)
draw = ImageDraw.Draw(image)
@ -108,10 +116,22 @@ class TerminalCog(commands.Cog, name="Terminal"):
if y_pos > IMG_HEIGHT - PADDING - FONT_SIZE:
break # Stop if no more space
# Create a unique filename with timestamp
filename = f"terminal_{uuid.uuid4().hex[:8]}_{int(time.time())}.png"
# Ensure the terminal_images directory exists
os.makedirs(TERMINAL_IMAGES_DIR, exist_ok=True)
# Save the image to the terminal_images directory
file_path = os.path.join(TERMINAL_IMAGES_DIR, filename)
image.save(file_path, format='PNG')
# Also return a BytesIO object for backward compatibility
img_byte_arr = io.BytesIO()
image.save(img_byte_arr, format='PNG')
img_byte_arr.seek(0)
return img_byte_arr
return img_byte_arr, filename
async def _update_terminal_message(self, interaction: Interaction | None = None, new_content: str | None = None):
"""Updates the terminal message with a new image and view."""
@ -125,19 +145,18 @@ class TerminalCog(commands.Cog, name="Terminal"):
await self.terminal_message.edit(content="Terminal session ended.", view=None, attachments=[])
return
image_bytes = self._generate_terminal_image()
discord_file = File(fp=image_bytes, filename="terminal_output.png")
# Generate the image and save it to the terminal_images directory
image_bytes, filename = self._generate_terminal_image()
# Create the URL for the image
image_url = f"{API_BASE_URL}/terminal_images/{filename}"
if self.terminal_view:
self.terminal_view.update_button_states(self) # Update button enable/disable
target_message = self.terminal_message
edit_kwargs = {"attachments": [discord_file], "view": self.terminal_view}
if new_content:
edit_kwargs["content"] = new_content
else: # Clear old content if any
edit_kwargs["content"] = None
# Prepare the message content with the image URL
content = f"Terminal Output: [View Image]({image_url})" if not new_content else new_content
edit_kwargs = {"content": content, "view": self.terminal_view, "attachments": []}
try:
if interaction and not interaction.response.is_done():
@ -213,13 +232,16 @@ class TerminalCog(commands.Cog, name="Terminal"):
self.terminal_view = TerminalView(cog=self, owner_id=self.owner_id)
image_bytes = self._generate_terminal_image()
discord_file = File(fp=image_bytes, filename="terminal_output.png")
# Generate the image and save it to the terminal_images directory
_, filename = self._generate_terminal_image()
# Create the URL for the image
image_url = f"{API_BASE_URL}/terminal_images/{filename}"
# Send initial message and store it
# Use followup since we deferred
self.terminal_message = await interaction.followup.send(
file=discord_file,
content=f"Terminal Output: [View Image]({image_url})",
view=self.terminal_view
)
self.terminal_view.message = self.terminal_message # Give view a reference to the message