initial Commit
This commit is contained in:
134
plugins/http_requests.py
Normal file
134
plugins/http_requests.py
Normal file
@@ -0,0 +1,134 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from typing import Any
|
||||
|
||||
|
||||
TOKEN_RE = re.compile(r"{{\s*([a-zA-Z0-9_.-]+)\s*}}")
|
||||
|
||||
|
||||
class HTTPRequestsPlugin:
|
||||
name = "HTTP Requests"
|
||||
desc = "Send configurable HTTP requests to webhooks, local services, and automation tools."
|
||||
version = "0.1.0"
|
||||
actions = [
|
||||
{
|
||||
"id": "send_request",
|
||||
"name": "Send Request",
|
||||
"desc": "Send a GET, POST, PUT, PATCH, or DELETE request.",
|
||||
"fields": [
|
||||
{
|
||||
"id": "method",
|
||||
"label": "Method",
|
||||
"type": "select",
|
||||
"default": "POST",
|
||||
"options": [
|
||||
{"label": "GET", "value": "GET"},
|
||||
{"label": "POST", "value": "POST"},
|
||||
{"label": "PUT", "value": "PUT"},
|
||||
{"label": "PATCH", "value": "PATCH"},
|
||||
{"label": "DELETE", "value": "DELETE"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "url",
|
||||
"label": "URL",
|
||||
"type": "url",
|
||||
"required": True,
|
||||
"placeholder": "http://127.0.0.1:8080/webhook",
|
||||
},
|
||||
{
|
||||
"id": "headers",
|
||||
"label": "Headers",
|
||||
"type": "key_value",
|
||||
"default": [{"key": "Content-Type", "value": "application/json"}],
|
||||
},
|
||||
{
|
||||
"id": "body",
|
||||
"label": "Body",
|
||||
"type": "json",
|
||||
"default": '{\n "button": "{{event.button}}",\n "event": "{{event.event}}"\n}',
|
||||
},
|
||||
{"id": "timeout", "label": "Timeout Seconds", "type": "number", "default": 5},
|
||||
{"id": "log_response", "label": "Log response", "type": "boolean", "default": True},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
def on_load(self, ctx):
|
||||
ctx.db.add_event("plugin.loaded", {"plugin": self.name})
|
||||
|
||||
def execute_action(self, ctx, action_id, config, event):
|
||||
if action_id != "send_request":
|
||||
raise ValueError(f"Unknown HTTP action '{action_id}'.")
|
||||
|
||||
method = str(config.get("method", "POST")).upper()
|
||||
if method not in {"GET", "POST", "PUT", "PATCH", "DELETE"}:
|
||||
raise ValueError(f"Unsupported HTTP method '{method}'.")
|
||||
|
||||
context = {"event": event or {}}
|
||||
url = self._render(str(config.get("url", "")).strip(), context)
|
||||
if not url.startswith(("http://", "https://")):
|
||||
raise ValueError("HTTP Request URL must start with http:// or https://.")
|
||||
|
||||
timeout = max(1.0, min(30.0, float(config.get("timeout", 5) or 5)))
|
||||
headers = self._headers(config.get("headers"), context)
|
||||
body_text = self._render(str(config.get("body", "") or ""), context).strip()
|
||||
data = None
|
||||
if method not in {"GET", "DELETE"} and body_text:
|
||||
data = body_text.encode("utf-8")
|
||||
headers.setdefault("Content-Type", "application/json")
|
||||
|
||||
request = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=timeout) as response:
|
||||
response_body = response.read(4096).decode("utf-8", errors="replace")
|
||||
status = response.status
|
||||
except urllib.error.HTTPError as exc:
|
||||
response_body = exc.read(4096).decode("utf-8", errors="replace")
|
||||
raise RuntimeError(f"HTTP request failed with {exc.code}: {response_body[:300]}") from exc
|
||||
except urllib.error.URLError as exc:
|
||||
raise RuntimeError(f"HTTP request failed: {exc.reason}") from exc
|
||||
|
||||
if config.get("log_response", True):
|
||||
ctx.db.add_event(
|
||||
"plugin.http_response",
|
||||
{"status": status, "url": url, "method": method, "body": response_body[:1000]},
|
||||
)
|
||||
|
||||
def _headers(self, raw_headers: Any, context: dict[str, Any]) -> dict[str, str]:
|
||||
if isinstance(raw_headers, dict):
|
||||
items = raw_headers.items()
|
||||
elif isinstance(raw_headers, list):
|
||||
items = ((item.get("key"), item.get("value")) for item in raw_headers if isinstance(item, dict))
|
||||
else:
|
||||
items = []
|
||||
headers = {}
|
||||
for key, value in items:
|
||||
rendered_key = self._render(str(key or "").strip(), context)
|
||||
if not rendered_key:
|
||||
continue
|
||||
headers[rendered_key] = self._render(str(value or ""), context)
|
||||
return headers
|
||||
|
||||
def _render(self, template: str, context: dict[str, Any]) -> str:
|
||||
def replace(match):
|
||||
value: Any = context
|
||||
for part in match.group(1).split("."):
|
||||
if isinstance(value, dict):
|
||||
value = value.get(part, "")
|
||||
else:
|
||||
value = getattr(value, part, "")
|
||||
if value is None:
|
||||
return ""
|
||||
if isinstance(value, (dict, list)):
|
||||
return json.dumps(value)
|
||||
return str(value)
|
||||
|
||||
return TOKEN_RE.sub(replace, template)
|
||||
|
||||
|
||||
PLUGIN = HTTPRequestsPlugin()
|
||||
Reference in New Issue
Block a user