diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..a44ca60 Binary files /dev/null and b/.DS_Store differ diff --git a/Dockerfile b/Dockerfile index 33730b6..de2fd00 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,8 @@ -# Dockerfile FROM python:3.12-slim # Set environment variables -ENV PYTHONDONTWRITEBYTECODE 1 -ENV PYTHONUNBUFFERED 1 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 # Set working directory WORKDIR /cost-assistant @@ -11,7 +10,14 @@ WORKDIR /cost-assistant # Copy requirements first (for caching) COPY requirements.txt . -# Install dependencies +# Install system dependencies: OpenCV, Poppler, and minimal utils +RUN apt-get update && apt-get install -y \ + libgl1 \ + libglib2.0-0 \ + poppler-utils \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies RUN pip install --no-cache-dir -r requirements.txt # Copy the project files @@ -23,5 +29,5 @@ RUN mkdir -p data/uploads # Expose port EXPOSE 8000 -# Run FastAPI with Uvicorn +# Run FastAPI with Uvicorn (reload for dev) CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/__pycache__/cost_calculator.cpython-313.pyc b/__pycache__/cost_calculator.cpython-313.pyc new file mode 100644 index 0000000..909bf59 Binary files /dev/null and b/__pycache__/cost_calculator.cpython-313.pyc differ diff --git a/__pycache__/mailer.cpython-313.pyc b/__pycache__/mailer.cpython-313.pyc new file mode 100644 index 0000000..53f1596 Binary files /dev/null and b/__pycache__/mailer.cpython-313.pyc differ diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000..2c55984 Binary files /dev/null and b/__pycache__/main.cpython-313.pyc differ diff --git a/archive/result.html b/archive/result.html new file mode 100644 index 0000000..0a9118b --- /dev/null +++ b/archive/result.html @@ -0,0 +1,111 @@ + + + + + + + Print Cost Result + + + + + +
+

Print Cost for {{ result.filename }}

+ + + + + + + + + + + + + + + {% for page in result.pages %} + + + + + + + + + + {% endfor %} + + + + + + + + + + + + + + + + + + + +
PageWidth (m)Height (m)Area (m²)Ink %TypeCost (€)
{{ page.page }}{{ "%.3f"|format(page.width_m) }}{{ "%.3f"|format(page.height_m) }}{{ "%.4f"|format(page.area_m2) }}{{ "%.1f"|format(page.ink_pct) if page.ink_pct is not none else '-' }}{{ 'Color' if page.is_color else 'B&W' }}{{ "%.2f"|format(page.cost) }}
Total Black Pages{{ "%.4f"|format(result.total_area_black) }}{{ "%.2f"|format(result.total_cost_black) }}
Total Color Pages{{ "%.4f"|format(result.total_area_color) }}{{ "%.2f"|format(result.total_cost_color) }}
Grand Total{{ "%.2f"|format(result.grand_total) }}
+ +

+ Upload another PDF +

+
+ + + \ No newline at end of file diff --git a/cost_calculator.py b/cost_calculator.py index 7fc1cf0..a266730 100644 --- a/cost_calculator.py +++ b/cost_calculator.py @@ -3,6 +3,7 @@ import os from PyPDF2 import PdfReader from pdf2image import convert_from_path import numpy as np +from PIL import Image UPLOAD_FOLDER = "data/uploads" ALLOWED_EXTENSIONS = {"pdf"} @@ -13,6 +14,7 @@ def allowed_file(filename: str) -> bool: def points_to_meters(points: float) -> float: + """Convert PDF points to meters (1 point = 1/72 inch)""" return (points / 72.0) * 0.0254 @@ -24,6 +26,21 @@ def get_rate_color() -> float: return float(os.environ.get("RATE_PER_M2_COLOR", "5.0")) +def get_page_size(page): + """Returns width and height in meters, rotation-aware, prefers CropBox""" + box = getattr(page, "cropbox", None) or page.mediabox + width_pts = float(box.width) + height_pts = float(box.height) + + rotation = page.get("/Rotate") or 0 + if rotation in [90, 270]: + width_pts, height_pts = height_pts, width_pts + + width_m = points_to_meters(width_pts) + height_m = points_to_meters(height_pts) + return width_m, height_m + + def analyze_pdf(path, compute_ink=True): reader = PdfReader(path) pages_info = [] @@ -31,9 +48,8 @@ def analyze_pdf(path, compute_ink=True): total_cost_black = total_cost_color = 0.0 for i, page in enumerate(reader.pages): - box = page.mediabox - width_m = points_to_meters(float(box.width)) - height_m = points_to_meters(float(box.height)) + # Get page size robustly + width_m, height_m = get_page_size(page) area = width_m * height_m ink_pct = None @@ -45,14 +61,22 @@ def analyze_pdf(path, compute_ink=True): img = images[0].convert("RGB") arr = np.array(img) - # ink pixels: any channel < 240 - ink_mask = np.any(arr < 240, axis=2) - ink_pct = float(np.count_nonzero(ink_mask)) / (arr.shape[0] * arr.shape[1]) * 100.0 + # Detect ink pixels (anything not near-white) + ink_mask = np.any(arr < 250, axis=2) + num_ink_pixels = np.count_nonzero(ink_mask) + total_pixels = arr.shape[0] * arr.shape[1] + ink_pct = (num_ink_pixels / total_pixels) * 100.0 + + if num_ink_pixels > 0: + # Convert to HSV using Pillow + hsv_img = img.convert("HSV") + hsv_arr = np.array(hsv_img) + saturation = hsv_arr[:, :, 1][ink_mask] + + # Color if even a tiny fraction of ink pixels have saturation > 10 + color_ratio = np.count_nonzero(saturation > 10) / len(saturation) + is_color = color_ratio > 0.001 # 0.1% threshold - # simple color detection: if RGB channels differ significantly - avg_rgb = arr.mean(axis=(0, 1)) - if np.ptp(avg_rgb) > 30: - is_color = True except Exception as e: print(f"Page {i+1} ink/color calc failed: {e}") ink_pct = None diff --git a/data/.DS_Store b/data/.DS_Store new file mode 100644 index 0000000..00c99ef Binary files /dev/null and b/data/.DS_Store differ diff --git a/data/uploads/Poster AD (A3) cmyk.pdf b/data/uploads/Poster AD (A3) cmyk.pdf new file mode 100644 index 0000000..dbc7f5b Binary files /dev/null and b/data/uploads/Poster AD (A3) cmyk.pdf differ diff --git a/data/uploads/Poster AD (A3).pdf b/data/uploads/Poster AD (A3).pdf index c33c828..eb0cfc3 100644 Binary files a/data/uploads/Poster AD (A3).pdf and b/data/uploads/Poster AD (A3).pdf differ diff --git a/data/uploads/einszwovier infographics 2.pdf b/data/uploads/einszwovier infographics 2.pdf new file mode 100644 index 0000000..080a119 Binary files /dev/null and b/data/uploads/einszwovier infographics 2.pdf differ diff --git a/data/uploads/einszwovier infographics.pdf b/data/uploads/einszwovier infographics.pdf new file mode 100644 index 0000000..ebe5059 Binary files /dev/null and b/data/uploads/einszwovier infographics.pdf differ diff --git a/get_room_id.py b/get_room_id.py new file mode 100644 index 0000000..5033669 --- /dev/null +++ b/get_room_id.py @@ -0,0 +1,16 @@ +from nio import AsyncClient +import asyncio, os + +from dotenv import load_dotenv +load_dotenv() # this must run **before** os.environ access + + +async def get_room_id(): + client = AsyncClient("http://localhost:8008", "@einszwovier_bot:localhost") + await client.login(os.environ["MATRIX_PASS"]) + await client.join("#2D-prints:localhost") + rooms = await client.joined_rooms() + print("Joined rooms:", rooms.rooms) + await client.close() + +asyncio.run(get_room_id()) diff --git a/mailer.py b/mailer.py new file mode 100644 index 0000000..afa1b9c --- /dev/null +++ b/mailer.py @@ -0,0 +1,96 @@ +import os +import asyncio +from io import BytesIO +from nio import AsyncClient, UploadResponse, RoomSendResponse + + +async def send_order(pdf_path: str, analysis: dict, room_id: str, comment: str = ""): + matrix_user = os.environ.get("MATRIX_USER") + matrix_pass = os.environ.get("MATRIX_PASS") + homeserver = os.environ.get("MATRIX_HOMESERVER", "http://localhost:8008") + + if not matrix_user or not matrix_pass: + raise RuntimeError("Missing MATRIX_USER or MATRIX_PASS in environment") + + client = AsyncClient(homeserver, matrix_user) + login_resp = await client.login(matrix_pass) + if getattr(login_resp, "access_token", None) is None: + await client.close() + raise RuntimeError(f"Failed to login to Matrix: {login_resp}") + + # Build summary text + summary_lines = [ + f"File: {analysis['filename']}", + f"Grand total: {analysis['grand_total']} €", + f" - Black/White: {analysis['total_area_black']:.2f} m² = {analysis['total_cost_black']} €", + f" - Color: {analysis['total_area_color']:.2f} m² = {analysis['total_cost_color']} €", + "", + "Page details:", + ] + for page in analysis["pages"]: + summary_lines.append( + f"Page {page['page']}: {page['width_m']*1000:.0f}×{page['height_m']*1000:.0f} mm, " + f"{'Color' if page['is_color'] else 'B/W'}, Cost {page['cost']} €" + ) + if comment.strip(): + summary_lines.append("\nUser comment:\n" + comment.strip()) + summary_text = "\n".join(summary_lines) + + # Send text summary + text_resp = await client.room_send( + room_id=room_id, + message_type="m.room.message", + content={ + "msgtype": "m.text", + "body": f"New print order submitted:\n\n{summary_text}", + }, + ) + if not (isinstance(text_resp, RoomSendResponse) and text_resp.event_id): + await client.logout() + await client.close() + raise RuntimeError(f"Failed to send order summary: {text_resp}") + + # Upload PDF + with open(pdf_path, "rb") as f: + pdf_bytes = f.read() + + upload_resp, upload_err = await client.upload( + data_provider=BytesIO(pdf_bytes), + content_type="application/pdf", + filename=os.path.basename(pdf_path), + filesize=len(pdf_bytes), + ) + + if upload_err: + await client.logout() + await client.close() + raise RuntimeError(f"Failed to upload PDF: {upload_err}") + + if not (isinstance(upload_resp, UploadResponse) and upload_resp.content_uri): + await client.logout() + await client.close() + raise RuntimeError(f"Failed to upload PDF: {upload_resp}") + + # Send PDF as separate file message + file_resp = await client.room_send( + room_id=room_id, + message_type="m.room.message", + content={ + "msgtype": "m.file", + "body": os.path.basename(pdf_path), + "url": upload_resp.content_uri, + }, + ) + + if not (isinstance(file_resp, RoomSendResponse) and file_resp.event_id): + await client.logout() + await client.close() + raise RuntimeError(f"Failed to send PDF message: {file_resp}") + + await client.logout() + await client.close() + print(f"✅ Text summary and PDF sent to room {room_id}") + + +def send_order_sync(pdf_path: str, analysis: dict, room_id: str, comment: str = ""): + asyncio.run(send_order(pdf_path, analysis, room_id, comment)) diff --git a/main.py b/main.py index bbe279a..669d24f 100644 --- a/main.py +++ b/main.py @@ -1,12 +1,16 @@ -# main.py import os import shutil -from fastapi import FastAPI, UploadFile, File, Request -from fastapi.responses import HTMLResponse +from fastapi import FastAPI, UploadFile, File, Request, Form +from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates from fastapi.staticfiles import StaticFiles from cost_calculator import allowed_file, analyze_pdf, get_rate_black, get_rate_color, UPLOAD_FOLDER +from mailer import send_order_sync + +from dotenv import load_dotenv +load_dotenv() + app = FastAPI() templates = Jinja2Templates(directory="templates") @@ -32,8 +36,8 @@ async def cost_dashboard(request: Request): { "request": request, "rate_black": get_rate_black(), - "rate_color": get_rate_color() - } + "rate_color": get_rate_color(), + }, ) @@ -57,6 +61,50 @@ async def upload_file(request: Request, file: UploadFile = File(...)): "request": request, "result": result, "rate_black": get_rate_black(), - "rate_color": get_rate_color() + "rate_color": get_rate_color(), }, ) + + +@app.post("/send-order") +def send_order_endpoint( + request: Request, + filename: str = Form(...), + comment: str = Form(""), +): + """ + Handles the 'Send Order' button. Sends PDF + analysis + user comment via Matrix. + """ + path = os.path.join(UPLOAD_FOLDER, filename) + if not os.path.exists(path): + return templates.TemplateResponse( + "cost-calculator.html", + {"request": request, "error": "File not found. Please upload again."}, + ) + + analysis = analyze_pdf(path) + + try: + send_order_sync(path, analysis, room_id="!YokZIMTVFEmSMRmmsb:localhost", comment=comment) + # Render same result.html but include a success banner + return templates.TemplateResponse( + "result.html", + { + "request": request, + "result": analysis, + "rate_black": get_rate_black(), + "rate_color": get_rate_color(), + "success": "✅ Your order has been sent!" + }, + ) + except Exception as e: + return templates.TemplateResponse( + "result.html", + { + "request": request, + "result": analysis, + "rate_black": get_rate_black(), + "rate_color": get_rate_color(), + "error": f"Failed to send order: {e}", + }, + ) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 0d11f90..beab6b8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,7 @@ python-multipart pypdf2 pdf2image Pillow -numpy \ No newline at end of file +numpy +opencv-python +python-dotenv +matrix-nio \ No newline at end of file diff --git a/run_app.py b/run_app.py new file mode 100644 index 0000000..8b1824a --- /dev/null +++ b/run_app.py @@ -0,0 +1,15 @@ +# run_app.py +import os +import uvicorn + +# Ensure upload folder exists +UPLOAD_FOLDER = "data/uploads" +os.makedirs(UPLOAD_FOLDER, exist_ok=True) + +if __name__ == "__main__": + uvicorn.run( + "main:app", # module_name:app_instance + host="0.0.0.0", # accessible on all interfaces + port=8000, + reload=True, # enables auto-reload on code changes + ) diff --git a/static/css/style.css b/static/css/style.css index 881eb3c..b951e0a 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1,3 +1,4 @@ +/* === Fonts === */ @font-face { font-family: "SISTEMAS"; src: url("/static/fonts/SISTEMAS FONT BT.ttf") format("truetype"); @@ -12,15 +13,28 @@ font-style: normal; } -h1, h2, h3, h4, h5, h6 { +/* === Typography === */ +h1, +h2, +h3, +h4, +h5, +h6 { font-family: "SISTEMAS", sans-serif; + color: #001F4D; + /* navy blue */ } -body, p, input, button, label, a { +body, +p, +input, +button, +label, +a { font-family: "BauPro", sans-serif; } - +/* === Layout === */ body { background-color: #F9F9F9; color: #333333; @@ -28,58 +42,15 @@ body { padding: 0; } -header { - background-color: #001F4D; /* navy blue */ - color: #FFD600; /* yellow text */ - padding: 2em; - text-align: center; - box-shadow: 0 2px 8px rgba(0,0,0,0.2); -} - -h1, h2, h3, h4, h5, h6 { - font-family: "SISTEMAS", sans-serif; - color: #001F4D; /* navy blue */ -} - -a.link-card { - display: block; - padding: 1.2em 1em; - background-color: #E6007E; /* magenta */ - color: #FFD600; /* yellow text */ - text-decoration: none; - border-radius: 10px; - text-align: center; - transition: all 0.2s ease-in-out; -} - -a.link-card:hover { - background-color: #FFD600; /* yellow hover */ - color: #001F4D; /* navy text on hover */ - box-shadow: 0 4px 12px rgba(0,0,0,0.2); -} - -button { - padding: 0.7em; - background-color: #E6007E; /* magenta */ - color: #FFD600; /* yellow text */ - border: none; - border-radius: 6px; - font-size: 1em; - cursor: pointer; -} - -button:hover { - background-color: #FFD600; /* yellow hover */ - color: #001F4D; /* navy text */ -} - header { display: flex; align-items: center; gap: 1em; padding: 1em; - background-color: #001f3f; /* navy */ + background-color: #001f3f; + /* navy */ color: #fff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); } .logo { @@ -95,4 +66,106 @@ nav a { nav a:hover { text-decoration: underline; +} + +/* === Links & Buttons === */ +a.link-card { + display: block; + padding: 1.2em 1em; + background-color: #E6007E; + /* magenta */ + color: #FFD600; + /* yellow text */ + text-decoration: none; + border-radius: 10px; + text-align: center; + transition: all 0.2s ease-in-out; +} + +a.link-card:hover { + background-color: #FFD600; + /* yellow hover */ + color: #001F4D; + /* navy text on hover */ + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +button { + padding: 0.7em; + background-color: #E6007E; + /* magenta */ + color: #FFD600; + /* yellow text */ + border: none; + border-radius: 6px; + font-size: 1em; + cursor: pointer; +} + +button:hover { + background-color: #FFD600; + /* yellow hover */ + color: #001F4D; + /* navy text */ +} + +/* === Result Page Table Styles === */ +table { + border-collapse: collapse; + width: 100%; + margin-top: 1em; +} + +th, +td { + border: 1px solid #ccc; + padding: 0.5em; + text-align: center; +} + +th { + background-color: #f2f2f2; +} + +.color { + background-color: #ffdede; +} + +.black { + background-color: #e0e0ff; +} + +.totals { + font-weight: bold; +} + +.container { + max-width: 900px; + margin: auto; +} + +h1, +h2 { + text-align: center; +} + +.alert { + padding: 1em; + border-radius: 6px; + margin-bottom: 1em; + text-align: center; + font-weight: bold; +} + +.alert.success { + background-color: #4CAF50; + /* green */ + color: white; +} + +.alert.error { + background-color: #E6007E; + /* magenta */ + color: #FFD600; + /* yellow text */ } \ No newline at end of file diff --git a/templates/cost-calculator.html b/templates/cost-calculator.html index 548809e..0e61662 100644 --- a/templates/cost-calculator.html +++ b/templates/cost-calculator.html @@ -1,39 +1,44 @@ + Print Cost Calculator +

Print Cost Calculator

{% if error %} -

{{ error }}

+

{{ error }}

{% endif %}
@@ -73,9 +112,59 @@

Rates are fixed via environment variables:
- B&W: {{ rate_black if rate_black else 'RATE_PER_M2_BLACK' }} € / m², + B&W: {{ rate_black if rate_black else 'RATE_PER_M2_BLACK' }} € / m², Color: {{ rate_color if rate_color else 'RATE_PER_M2_COLOR' }} € / m²

+ + {% if result %} +

Results for {{ result.filename }}

+ + + + + + + + + + + + + + + {% for page in result.pages %} + + + + + + + + + + {% endfor %} + + + + + + + + + + + + + + + + + + + +
PageWidth (m)Height (m)Area (m²)Ink %TypeCost (€)
{{ page.page }}{{ "%.3f"|format(page.width_m) }}{{ "%.3f"|format(page.height_m) }}{{ "%.4f"|format(page.area_m2) }}{{ "%.1f"|format(page.ink_pct) if page.ink_pct is not none else '-' }}{{ 'Color' if page.is_color else 'B&W' }}{{ "%.2f"|format(page.cost) }}
Total Black Pages{{ "%.4f"|format(result.total_area_black) }}{{ "%.2f"|format(result.total_cost_black) }}
Total Color Pages{{ "%.4f"|format(result.total_area_color) }}{{ "%.2f"|format(result.total_cost_color) }}
Grand Total{{ "%.2f"|format(result.grand_total) }}
+ {% endif %}
- + + \ No newline at end of file diff --git a/templates/result.html b/templates/result.html index 533943e..c46e511 100644 --- a/templates/result.html +++ b/templates/result.html @@ -1,74 +1,82 @@ - + Print Cost Result - + -
-

Print Cost for {{ result.filename }}

+
+

Print Cost for {{ result.filename }}

- - - - - - - - - - - - - - {% for page in result.pages %} - - - - - - - - - - {% endfor %} - - - - - - - - - - - - - - - - - - - -
PageWidth (m)Height (m)Area (m²)Ink %TypeCost (€)
{{ page.page }}{{ "%.3f"|format(page.width_m) }}{{ "%.3f"|format(page.height_m) }}{{ "%.4f"|format(page.area_m2) }}{{ "%.1f"|format(page.ink_pct) if page.ink_pct is not none else '-' }}{{ 'Color' if page.is_color else 'B&W' }}{{ "%.2f"|format(page.cost) }}
Total Black Pages{{ "%.4f"|format(result.total_area_black) }}{{ "%.2f"|format(result.total_cost_black) }}
Total Color Pages{{ "%.4f"|format(result.total_area_color) }}{{ "%.2f"|format(result.total_cost_color) }}
Grand Total{{ "%.2f"|format(result.grand_total) }}
+ + {% if success %} +
{{ success }}
+ {% endif %} + {% if error %} +
{{ error }}
+ {% endif %} -

- Upload another PDF -

-
+ + + + + + + + + + + + + + {% for page in result.pages %} + + + + + + + + + + {% endfor %} + + + + + + + + + + + + + + + + + + + +
PageWidth (m)Height (m)Area (m²)Ink %TypeCost (€)
{{ page.page }}{{ "%.3f"|format(page.width_m) }}{{ "%.3f"|format(page.height_m) }}{{ "%.4f"|format(page.area_m2) }}{{ "%.1f"|format(page.ink_pct) if page.ink_pct is not none else '-' }}{{ 'Color' if page.is_color else 'B&W' }}{{ "%.2f"|format(page.cost) }}
Total Black Pages{{ "%.4f"|format(result.total_area_black) }}{{ "%.2f"|format(result.total_cost_black) }}
Total Color Pages{{ "%.4f"|format(result.total_area_color) }}{{ "%.2f"|format(result.total_cost_color) }}
Grand Total{{ "%.2f"|format(result.grand_total) }}
+ + + + + + + + + + +
- + + \ No newline at end of file diff --git a/test_send_pdf.py b/test_send_pdf.py new file mode 100644 index 0000000..67a7c61 --- /dev/null +++ b/test_send_pdf.py @@ -0,0 +1,72 @@ +import os +import asyncio +from io import BytesIO +from nio import AsyncClient, UploadResponse, RoomSendResponse +from dotenv import load_dotenv + +load_dotenv() + +async def main(): + # Get credentials from environment (adjust if needed) + matrix_user = os.environ.get("MATRIX_USER", "@einszwovier_bot:localhost") + matrix_pass = os.environ.get("MATRIX_PASS") + homeserver = os.environ.get("MATRIX_HOMESERVER", "http://localhost:8008") + room_id = os.environ.get("MATRIX_ROOM") # e.g. "!abc123:localhost" + + if not all([matrix_user, matrix_pass, room_id]): + raise RuntimeError("Missing MATRIX_USER, MATRIX_PASS or MATRIX_ROOM") + + client = AsyncClient(homeserver, matrix_user) + login_resp = await client.login(matrix_pass) + if getattr(login_resp, "access_token", None) is None: + print("❌ Login failed:", login_resp) + return + + print("✅ Logged in as", matrix_user) + + pdf_path = "data/uploads/einszwovier infographics 2.pdf" # <-- put any small PDF here + with open(pdf_path, "rb") as f: + pdf_bytes = f.read() + + # ✅ Upload PDF (nio returns (resp, err)) + upload_resp, upload_err = await client.upload( + data_provider=BytesIO(pdf_bytes), + content_type="application/pdf", + filename=os.path.basename(pdf_path), + filesize=len(pdf_bytes), + ) + + if upload_err: + print("❌ Upload error:", upload_err) + await client.close() + return + + if isinstance(upload_resp, UploadResponse) and upload_resp.content_uri: + print("✅ Upload succeeded:", upload_resp.content_uri) + else: + print("❌ Upload failed:", upload_resp) + await client.close() + return + + # Send file message to room + file_resp = await client.room_send( + room_id=room_id, + message_type="m.room.message", + content={ + "msgtype": "m.file", + "body": os.path.basename(pdf_path), + "url": upload_resp.content_uri, + }, + ) + + if isinstance(file_resp, RoomSendResponse) and file_resp.event_id: + print("✅ PDF sent to room", room_id) + else: + print("❌ Failed to send PDF:", file_resp) + + await client.logout() + await client.close() + + +if __name__ == "__main__": + asyncio.run(main())