webhook.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. import logging
  2. from fastapi import APIRouter, Depends, HTTPException
  3. from sqlalchemy.ext.asyncio import AsyncSession
  4. from sqlalchemy import select
  5. from pydantic import BaseModel
  6. from backend.app.core.database import get_db
  7. from backend.app.core.auth import get_api_key, check_permission, check_printer_access
  8. from backend.app.models.api_key import APIKey
  9. from backend.app.models.archive import PrintArchive
  10. from backend.app.models.print_queue import PrintQueueItem
  11. from backend.app.models.printer import Printer
  12. from backend.app.services.printer_manager import printer_manager
  13. logger = logging.getLogger(__name__)
  14. router = APIRouter(prefix="/webhook", tags=["webhook"])
  15. # Request schemas
  16. class QueueAddRequest(BaseModel):
  17. archive_id: int
  18. printer_id: int
  19. project_id: int | None = None
  20. scheduled_time: str | None = None # ISO format datetime
  21. require_previous_success: bool = False
  22. auto_off_after: bool = False
  23. class QueueAddResponse(BaseModel):
  24. id: int
  25. archive_id: int
  26. printer_id: int
  27. position: int
  28. status: str
  29. message: str
  30. class PrinterStatusResponse(BaseModel):
  31. id: int
  32. name: str
  33. connected: bool
  34. state: str | None
  35. current_print: str | None
  36. progress: float | None
  37. remaining_time: int | None
  38. class QueueStatusResponse(BaseModel):
  39. printer_id: int
  40. printer_name: str
  41. pending: int
  42. printing: int
  43. items: list[dict]
  44. # Webhook endpoints
  45. @router.post("/queue/add", response_model=QueueAddResponse)
  46. async def webhook_add_to_queue(
  47. data: QueueAddRequest,
  48. api_key: APIKey = Depends(get_api_key),
  49. db: AsyncSession = Depends(get_db),
  50. ):
  51. """Add a print to the queue via webhook.
  52. Requires 'can_queue' permission.
  53. """
  54. check_permission(api_key, 'queue')
  55. check_printer_access(api_key, data.printer_id)
  56. # Verify archive exists
  57. result = await db.execute(
  58. select(PrintArchive).where(PrintArchive.id == data.archive_id)
  59. )
  60. archive = result.scalar_one_or_none()
  61. if not archive:
  62. raise HTTPException(status_code=404, detail="Archive not found")
  63. # Verify printer exists
  64. result = await db.execute(
  65. select(Printer).where(Printer.id == data.printer_id)
  66. )
  67. printer = result.scalar_one_or_none()
  68. if not printer:
  69. raise HTTPException(status_code=404, detail="Printer not found")
  70. # Get next position
  71. result = await db.execute(
  72. select(PrintQueueItem.position)
  73. .where(
  74. PrintQueueItem.printer_id == data.printer_id,
  75. PrintQueueItem.status == "pending",
  76. )
  77. .order_by(PrintQueueItem.position.desc())
  78. .limit(1)
  79. )
  80. max_position = result.scalar()
  81. next_position = (max_position or 0) + 1
  82. # Parse scheduled time if provided
  83. scheduled_time = None
  84. if data.scheduled_time:
  85. from datetime import datetime
  86. try:
  87. scheduled_time = datetime.fromisoformat(data.scheduled_time.replace('Z', '+00:00'))
  88. except ValueError:
  89. raise HTTPException(status_code=400, detail="Invalid scheduled_time format")
  90. # Create queue item
  91. queue_item = PrintQueueItem(
  92. printer_id=data.printer_id,
  93. archive_id=data.archive_id,
  94. project_id=data.project_id,
  95. position=next_position,
  96. scheduled_time=scheduled_time,
  97. require_previous_success=data.require_previous_success,
  98. auto_off_after=data.auto_off_after,
  99. )
  100. db.add(queue_item)
  101. await db.flush()
  102. await db.refresh(queue_item)
  103. return QueueAddResponse(
  104. id=queue_item.id,
  105. archive_id=queue_item.archive_id,
  106. printer_id=queue_item.printer_id,
  107. position=queue_item.position,
  108. status=queue_item.status,
  109. message=f"Added to queue at position {queue_item.position}",
  110. )
  111. @router.post("/printer/{printer_id}/start")
  112. async def webhook_start_print(
  113. printer_id: int,
  114. api_key: APIKey = Depends(get_api_key),
  115. db: AsyncSession = Depends(get_db),
  116. ):
  117. """Start the next queued print on a printer.
  118. Requires 'can_control_printer' permission.
  119. """
  120. check_permission(api_key, 'control_printer')
  121. check_printer_access(api_key, printer_id)
  122. # Get printer
  123. result = await db.execute(select(Printer).where(Printer.id == printer_id))
  124. printer = result.scalar_one_or_none()
  125. if not printer:
  126. raise HTTPException(status_code=404, detail="Printer not found")
  127. # Get next pending queue item
  128. result = await db.execute(
  129. select(PrintQueueItem)
  130. .where(
  131. PrintQueueItem.printer_id == printer_id,
  132. PrintQueueItem.status == "pending",
  133. )
  134. .order_by(PrintQueueItem.position)
  135. .limit(1)
  136. )
  137. queue_item = result.scalar_one_or_none()
  138. if not queue_item:
  139. raise HTTPException(status_code=404, detail="No pending prints in queue")
  140. # Check if printer is ready
  141. status = printer_manager.get_status(printer_id)
  142. if not status or not status.get("connected"):
  143. raise HTTPException(status_code=503, detail="Printer not connected")
  144. if status.get("state") not in ["IDLE", "FINISH", "FAILED"]:
  145. raise HTTPException(
  146. status_code=409,
  147. detail=f"Printer is busy (state: {status.get('state')})"
  148. )
  149. # Start the print
  150. try:
  151. await printer_manager.start_print(printer_id, queue_item.archive_id)
  152. except Exception as e:
  153. logger.error(f"Failed to start print: {e}")
  154. raise HTTPException(status_code=500, detail=str(e))
  155. return {"message": "Print started", "queue_item_id": queue_item.id}
  156. @router.post("/printer/{printer_id}/stop")
  157. async def webhook_stop_print(
  158. printer_id: int,
  159. api_key: APIKey = Depends(get_api_key),
  160. ):
  161. """Stop the current print on a printer.
  162. Requires 'can_control_printer' permission.
  163. """
  164. check_permission(api_key, 'control_printer')
  165. check_printer_access(api_key, printer_id)
  166. status = printer_manager.get_status(printer_id)
  167. if not status or not status.get("connected"):
  168. raise HTTPException(status_code=503, detail="Printer not connected")
  169. if status.get("state") != "RUNNING":
  170. raise HTTPException(status_code=409, detail="No print in progress")
  171. try:
  172. await printer_manager.stop_print(printer_id)
  173. except Exception as e:
  174. logger.error(f"Failed to stop print: {e}")
  175. raise HTTPException(status_code=500, detail=str(e))
  176. return {"message": "Print stopped"}
  177. @router.post("/printer/{printer_id}/cancel")
  178. async def webhook_cancel_print(
  179. printer_id: int,
  180. api_key: APIKey = Depends(get_api_key),
  181. ):
  182. """Cancel the current print on a printer.
  183. Requires 'can_control_printer' permission.
  184. """
  185. check_permission(api_key, 'control_printer')
  186. check_printer_access(api_key, printer_id)
  187. status = printer_manager.get_status(printer_id)
  188. if not status or not status.get("connected"):
  189. raise HTTPException(status_code=503, detail="Printer not connected")
  190. if status.get("state") not in ["RUNNING", "PAUSE"]:
  191. raise HTTPException(status_code=409, detail="No print to cancel")
  192. try:
  193. await printer_manager.cancel_print(printer_id)
  194. except Exception as e:
  195. logger.error(f"Failed to cancel print: {e}")
  196. raise HTTPException(status_code=500, detail=str(e))
  197. return {"message": "Print cancelled"}
  198. @router.get("/printer/{printer_id}/status", response_model=PrinterStatusResponse)
  199. async def webhook_get_printer_status(
  200. printer_id: int,
  201. api_key: APIKey = Depends(get_api_key),
  202. db: AsyncSession = Depends(get_db),
  203. ):
  204. """Get status of a printer.
  205. Requires 'can_read_status' permission.
  206. """
  207. check_permission(api_key, 'read_status')
  208. check_printer_access(api_key, printer_id)
  209. # Get printer
  210. result = await db.execute(select(Printer).where(Printer.id == printer_id))
  211. printer = result.scalar_one_or_none()
  212. if not printer:
  213. raise HTTPException(status_code=404, detail="Printer not found")
  214. status = printer_manager.get_status(printer_id)
  215. return PrinterStatusResponse(
  216. id=printer.id,
  217. name=printer.name,
  218. connected=status.get("connected", False) if status else False,
  219. state=status.get("state") if status else None,
  220. current_print=status.get("current_print") if status else None,
  221. progress=status.get("progress") if status else None,
  222. remaining_time=status.get("remaining_time") if status else None,
  223. )
  224. @router.get("/queue", response_model=list[QueueStatusResponse])
  225. async def webhook_get_queue_status(
  226. printer_id: int | None = None,
  227. api_key: APIKey = Depends(get_api_key),
  228. db: AsyncSession = Depends(get_db),
  229. ):
  230. """Get queue status for all printers or a specific printer.
  231. Requires 'can_read_status' permission.
  232. """
  233. check_permission(api_key, 'read_status')
  234. # Get printers
  235. if printer_id:
  236. check_printer_access(api_key, printer_id)
  237. result = await db.execute(select(Printer).where(Printer.id == printer_id))
  238. printers = result.scalars().all()
  239. else:
  240. result = await db.execute(select(Printer))
  241. printers = result.scalars().all()
  242. # Filter by allowed printers if limited
  243. if api_key.printer_ids:
  244. printers = [p for p in printers if p.id in api_key.printer_ids]
  245. response = []
  246. for printer in printers:
  247. # Get queue items
  248. result = await db.execute(
  249. select(PrintQueueItem)
  250. .where(
  251. PrintQueueItem.printer_id == printer.id,
  252. PrintQueueItem.status.in_(["pending", "printing"]),
  253. )
  254. .order_by(PrintQueueItem.position)
  255. )
  256. items = result.scalars().all()
  257. pending_count = sum(1 for i in items if i.status == "pending")
  258. printing_count = sum(1 for i in items if i.status == "printing")
  259. response.append(QueueStatusResponse(
  260. printer_id=printer.id,
  261. printer_name=printer.name,
  262. pending=pending_count,
  263. printing=printing_count,
  264. items=[
  265. {
  266. "id": item.id,
  267. "archive_id": item.archive_id,
  268. "position": item.position,
  269. "status": item.status,
  270. }
  271. for item in items
  272. ],
  273. ))
  274. return response