import json import mimetypes import uuid from pathlib import Path from urllib import error, request class WebhookService: def _multipart_parts_for_fields(self, boundary: str, fields: dict[str, str]) -> list[bytes]: parts: list[bytes] = [] for key, value in fields.items(): parts.extend([ f"--{boundary}\r\n".encode(), f'Content-Disposition: form-data; name="{key}"\r\n\r\n'.encode(), str(value).encode(), b"\r\n", ]) return parts def _multipart_parts_for_file(self, boundary: str, file_field: str, file_path: str) -> list[bytes]: path = Path(file_path) mime = mimetypes.guess_type(path.name)[0] or "application/octet-stream" return [ f"--{boundary}\r\n".encode(), f'Content-Disposition: form-data; name="{file_field}"; filename="{path.name}"\r\n'.encode(), f"Content-Type: {mime}\r\n\r\n".encode(), path.read_bytes(), b"\r\n", ] def _multipart_body(self, fields: dict[str, str], file_field: str, file_path: str) -> tuple[bytes, str]: boundary = f"----DorisSchoolhouse{uuid.uuid4().hex}" parts = self._multipart_parts_for_fields(boundary, fields) parts.extend(self._multipart_parts_for_file(boundary, file_field, file_path)) parts.append(f"--{boundary}--\r\n".encode()) return b"".join(parts), boundary def _multipart_body_many(self, fields: dict[str, str], file_field: str, file_paths: list[str]) -> tuple[bytes, str]: boundary = f"----DorisSchoolhouse{uuid.uuid4().hex}" parts = self._multipart_parts_for_fields(boundary, fields) for file_path in file_paths: parts.extend(self._multipart_parts_for_file(boundary, file_field, file_path)) parts.append(f"--{boundary}--\r\n".encode()) return b"".join(parts), boundary def post_multipart(self, url: str, fields: dict[str, str], file_field: str, file_path: str) -> dict: body, boundary = self._multipart_body(fields, file_field, file_path) req = request.Request( url, data=body, headers={"Content-Type": f"multipart/form-data; boundary={boundary}"}, method="POST", ) def parse_raw(raw: str): try: return json.loads(raw) except Exception: return {"raw": raw} try: with request.urlopen(req, timeout=120) as response: raw = response.read().decode("utf-8", errors="replace") return {"ok": True, "status_code": response.status, "body": parse_raw(raw)} except error.HTTPError as exc: raw = exc.read().decode("utf-8", errors="replace") return {"ok": False, "status_code": exc.code, "body": parse_raw(raw), "error": str(exc)} def post_multipart_many(self, url: str, fields: dict[str, str], file_field: str, file_paths: list[str]) -> dict: body, boundary = self._multipart_body_many(fields, file_field, file_paths) req = request.Request( url, data=body, headers={"Content-Type": f"multipart/form-data; boundary={boundary}"}, method="POST", ) def parse_raw(raw: str): try: return json.loads(raw) except Exception: return {"raw": raw} try: with request.urlopen(req, timeout=120) as response: raw = response.read().decode("utf-8", errors="replace") return {"ok": True, "status_code": response.status, "body": parse_raw(raw)} except error.HTTPError as exc: raw = exc.read().decode("utf-8", errors="replace") return {"ok": False, "status_code": exc.code, "body": parse_raw(raw), "error": str(exc)}