js-api 0.2.99__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {js_api-0.2.99/src/js_api.egg-info → js_api-0.3.0}/PKG-INFO +3 -2
  2. {js_api-0.2.99 → js_api-0.3.0}/pyproject.toml +3 -2
  3. js_api-0.3.0/src/controllers/webhook.py +109 -0
  4. {js_api-0.2.99 → js_api-0.3.0/src/js_api.egg-info}/PKG-INFO +3 -2
  5. {js_api-0.2.99 → js_api-0.3.0}/src/js_api.egg-info/SOURCES.txt +1 -0
  6. {js_api-0.2.99 → js_api-0.3.0}/src/js_api.egg-info/requires.txt +2 -1
  7. js_api-0.3.0/src/util/mail.py +322 -0
  8. js_api-0.2.99/src/controllers/webhook.py +0 -44
  9. {js_api-0.2.99 → js_api-0.3.0}/LICENSE +0 -0
  10. {js_api-0.2.99 → js_api-0.3.0}/README.md +0 -0
  11. {js_api-0.2.99 → js_api-0.3.0}/setup.cfg +0 -0
  12. {js_api-0.2.99 → js_api-0.3.0}/src/__init__.py +0 -0
  13. {js_api-0.2.99 → js_api-0.3.0}/src/api.py +0 -0
  14. {js_api-0.2.99 → js_api-0.3.0}/src/controllers/__init__.py +0 -0
  15. {js_api-0.2.99 → js_api-0.3.0}/src/controllers/docker.py +0 -0
  16. {js_api-0.2.99 → js_api-0.3.0}/src/controllers/health_check.py +0 -0
  17. {js_api-0.2.99 → js_api-0.3.0}/src/controllers/ip.py +0 -0
  18. {js_api-0.2.99 → js_api-0.3.0}/src/controllers/valentines_day.py +0 -0
  19. {js_api-0.2.99 → js_api-0.3.0}/src/database.py +0 -0
  20. {js_api-0.2.99 → js_api-0.3.0}/src/js_api.egg-info/dependency_links.txt +0 -0
  21. {js_api-0.2.99 → js_api-0.3.0}/src/js_api.egg-info/top_level.txt +0 -0
  22. {js_api-0.2.99 → js_api-0.3.0}/src/models.py +0 -0
  23. {js_api-0.2.99 → js_api-0.3.0}/src/util/__init__.py +0 -0
  24. {js_api-0.2.99 → js_api-0.3.0}/src/util/auth.py +0 -0
  25. {js_api-0.2.99 → js_api-0.3.0}/src/util/docker.py +0 -0
  26. {js_api-0.2.99 → js_api-0.3.0}/src/util/healthcheck.py +0 -0
  27. {js_api-0.2.99 → js_api-0.3.0}/src/util/logging.py +0 -0
  28. {js_api-0.2.99 → js_api-0.3.0}/tests/test_docker.py +0 -0
  29. {js_api-0.2.99 → js_api-0.3.0}/tests/test_health_check.py +0 -0
  30. {js_api-0.2.99 → js_api-0.3.0}/tests/test_ip.py +0 -0
  31. {js_api-0.2.99 → js_api-0.3.0}/tests/test_webhook.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: js-api
3
- Version: 0.2.99
3
+ Version: 0.3.0
4
4
  Summary: Custom API written in Python using FastAPI
5
5
  Author-email: Jack Stockley <jack@jstockley.com>
6
6
  Project-URL: Homepage, https://github.com/jnstockley/api
@@ -13,10 +13,11 @@ Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Requires-Dist: fastapi[standard]==0.141.1
15
15
  Requires-Dist: sqlalchemy==2.0.54
16
- Requires-Dist: sqlmodel==0.0.44
16
+ Requires-Dist: sqlmodel==0.0.47
17
17
  Requires-Dist: python-dotenv==1.2.3
18
18
  Requires-Dist: psycopg[binary]==3.3.6
19
19
  Requires-Dist: nctalk==0.0.4
20
+ Requires-Dist: jinja2==3.1.6
20
21
  Dynamic: license-file
21
22
 
22
23
  # API
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "js-api"
3
- version = "0.2.99"
3
+ version = "0.3.0"
4
4
  description = "Custom API written in Python using FastAPI"
5
5
  authors = [
6
6
  {name = "Jack Stockley", email = "jack@jstockley.com"}
@@ -15,10 +15,11 @@ classifiers = [
15
15
  dependencies = [
16
16
  "fastapi[standard]==0.141.1",
17
17
  "sqlalchemy==2.0.54",
18
- "sqlmodel==0.0.44",
18
+ "sqlmodel==0.0.47",
19
19
  "python-dotenv==1.2.3",
20
20
  "psycopg[binary]==3.3.6",
21
21
  "nctalk==0.0.4",
22
+ "jinja2==3.1.6",
22
23
  ]
23
24
  [project.urls]
24
25
  Homepage = "https://github.com/jnstockley/api"
@@ -0,0 +1,109 @@
1
+ import os
2
+
3
+ from fastapi import APIRouter, HTTPException, Security
4
+ from nctalk import TalkMessenger
5
+ from pydantic import BaseModel, Field
6
+
7
+ from util.auth import get_token_header
8
+ from util.logging import logger
9
+ from util.mail import EmailSender, SMTPConfig, TemplateRenderer
10
+
11
+ router = APIRouter(prefix="/webhook", dependencies=[Security(get_token_header)])
12
+
13
+
14
+ class NextcloudTalkWebhook(BaseModel):
15
+ header: str = Field(..., min_length=1, max_length=200)
16
+ message: str = Field(..., min_length=1, max_length=32000)
17
+
18
+
19
+ class PveUpsWebhook(BaseModel):
20
+ subject: str = Field(..., min_length=1, max_length=200)
21
+ body: str = Field(..., min_length=1, max_length=32000)
22
+ severity: str = Field(..., min_length=1, max_length=32000)
23
+ severity_upper: str = Field(..., min_length=1, max_length=32000)
24
+ facts: str = Field(..., min_length=1, max_length=32000)
25
+ timestamp: str = Field(..., min_length=1, max_length=32000)
26
+ version: str = Field(..., min_length=1, max_length=32000)
27
+ to: str = Field(..., min_length=1, max_length=32000)
28
+
29
+
30
+ @router.post("/databasus")
31
+ async def databasus(webhook: NextcloudTalkWebhook):
32
+ if webhook.header.strip() == "":
33
+ raise HTTPException(status_code=422, detail="header cannot be empty")
34
+ if webhook.message.strip() == "":
35
+ raise HTTPException(status_code=422, detail="message cannot be empty")
36
+
37
+ endpoint_url = os.environ.get("NEXTCLOUD_URL", "").strip()
38
+ token = os.environ.get("NEXTCLOUD_TALK_TOKEN", "").strip()
39
+ secret = os.environ.get("NEXTCLOUD_TALK_SECRET", "").strip()
40
+
41
+ if not endpoint_url or not token or not secret:
42
+ raise HTTPException(status_code=500, detail="Nextcloud Talk is not configured")
43
+ if not endpoint_url.startswith("https://"):
44
+ raise HTTPException(status_code=500, detail="NEXTCLOUD_URL must use https://")
45
+
46
+ messenger = TalkMessenger(endpoint_url=endpoint_url, token=token, secret=secret)
47
+
48
+ message = f"**{webhook.header}**\n{webhook.message}"
49
+
50
+ if not messenger.send(message):
51
+ logger.error("Failed to send message to Nextcloud Talk")
52
+ raise HTTPException(
53
+ status_code=502, detail="Failed to send message to Nextcloud Talk"
54
+ )
55
+
56
+ return {"status": "sent"}
57
+
58
+
59
+ @router.post("/pve-ups")
60
+ async def pve_ups(webhook: PveUpsWebhook):
61
+ if not os.environ.get("SMTP_HOST") or not os.environ.get("SMTP_PORT"):
62
+ raise HTTPException(status_code=500, detail="SMTP is not configured")
63
+ if not os.environ.get("SMTP_USERNAME") or not os.environ.get("SMTP_PASSWORD"):
64
+ raise HTTPException(
65
+ status_code=500, detail="SMTP credentials are not configured"
66
+ )
67
+
68
+ if webhook.body.strip() == "":
69
+ raise HTTPException(status_code=422, detail="body cannot be empty")
70
+ if webhook.facts.strip() == "":
71
+ raise HTTPException(status_code=422, detail="facts cannot be empty")
72
+ if webhook.timestamp.strip() == "":
73
+ raise HTTPException(status_code=422, detail="timestamp cannot be empty")
74
+ if webhook.subject.strip() == "":
75
+ raise HTTPException(status_code=422, detail="subject cannot be empty")
76
+ if webhook.to.strip() == "":
77
+ raise HTTPException(status_code=422, detail="to cannot be empty")
78
+ if webhook.severity.strip() == "":
79
+ raise HTTPException(status_code=422, detail="severity cannot be empty")
80
+ if webhook.version.strip() == "":
81
+ raise HTTPException(status_code=422, detail="version cannot be empty")
82
+ if webhook.severity_upper.strip() == "":
83
+ raise HTTPException(status_code=422, detail="severity_upper cannot be empty")
84
+
85
+ host = os.environ.get("SMTP_HOST")
86
+ port = int(os.environ.get("SMTP_PORT", "0"))
87
+ username = os.environ.get("SMTP_USERNAME")
88
+ password = os.environ.get("SMTP_PASSWORD")
89
+
90
+ smtp_config = SMTPConfig(host=host, port=port, username=username, password=password)
91
+
92
+ with EmailSender(smtp_config) as sender:
93
+ renderer = TemplateRenderer(templates_dir="templates")
94
+ sender.send_templated(
95
+ renderer=renderer,
96
+ sender_addr="PVE UPS <no-reply@jstockley.com>",
97
+ template="pve_ups_email.html",
98
+ subject=webhook.subject,
99
+ to=webhook.to,
100
+ context={
101
+ "body": webhook.body,
102
+ "subject": webhook.subject,
103
+ "facts": webhook.facts,
104
+ "timestamp": webhook.timestamp,
105
+ "severity": webhook.severity,
106
+ "severity_upper": webhook.severity.upper(),
107
+ "version": webhook.version,
108
+ },
109
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: js-api
3
- Version: 0.2.99
3
+ Version: 0.3.0
4
4
  Summary: Custom API written in Python using FastAPI
5
5
  Author-email: Jack Stockley <jack@jstockley.com>
6
6
  Project-URL: Homepage, https://github.com/jnstockley/api
@@ -13,10 +13,11 @@ Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Requires-Dist: fastapi[standard]==0.141.1
15
15
  Requires-Dist: sqlalchemy==2.0.54
16
- Requires-Dist: sqlmodel==0.0.44
16
+ Requires-Dist: sqlmodel==0.0.47
17
17
  Requires-Dist: python-dotenv==1.2.3
18
18
  Requires-Dist: psycopg[binary]==3.3.6
19
19
  Requires-Dist: nctalk==0.0.4
20
+ Requires-Dist: jinja2==3.1.6
20
21
  Dynamic: license-file
21
22
 
22
23
  # API
@@ -21,6 +21,7 @@ src/util/auth.py
21
21
  src/util/docker.py
22
22
  src/util/healthcheck.py
23
23
  src/util/logging.py
24
+ src/util/mail.py
24
25
  tests/test_docker.py
25
26
  tests/test_health_check.py
26
27
  tests/test_ip.py
@@ -1,6 +1,7 @@
1
1
  fastapi[standard]==0.141.1
2
2
  sqlalchemy==2.0.54
3
- sqlmodel==0.0.44
3
+ sqlmodel==0.0.47
4
4
  python-dotenv==1.2.3
5
5
  psycopg[binary]==3.3.6
6
6
  nctalk==0.0.4
7
+ jinja2==3.1.6
@@ -0,0 +1,322 @@
1
+ """
2
+ email_sender.py
3
+
4
+ A small, secure, dependency-free utility for sending email through a local
5
+ (or remote) SMTP server using Python's standard library.
6
+
7
+ Design goals:
8
+ - Secure by default (TLS/STARTTLS, no plaintext credentials in logs)
9
+ - Modern Python (type hints, dataclasses, context manager, pathlib)
10
+ - Performant for simple use (single persistent connection per batch via
11
+ the context manager, so you don't reconnect for every message)
12
+
13
+ Templating: HTML/text bodies can be rendered from Jinja2 templates stored
14
+ on disk via TemplateRenderer + EmailSender.send_templated(). Requires the
15
+ 'jinja2' package: pip install jinja2
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import html as html_lib
21
+ import logging
22
+ import mimetypes
23
+ import re
24
+ import smtplib
25
+ import ssl
26
+ from collections.abc import Iterable, Sequence
27
+ from dataclasses import dataclass
28
+ from email.message import EmailMessage
29
+ from pathlib import Path
30
+ from typing import Any, Self
31
+
32
+ import jinja2
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ @dataclass
38
+ class SMTPConfig:
39
+ host: str = "localhost"
40
+ port: int = 587
41
+ username: str | None = None
42
+ password: str | None = None
43
+ use_ssl: bool = False # True -> SMTPS (implicit TLS, usually port 465)
44
+ use_starttls: bool = True # True -> upgrade plaintext connection (usually port 587)
45
+ timeout: float = 10.0
46
+ # Set to False only for local/dev servers with self-signed certs.
47
+ verify_tls: bool = True
48
+ # Refuse to negotiate anything older than TLS 1.2 (TLS 1.3 will be used
49
+ # automatically whenever both sides support it, since it's preferred by
50
+ # the default context's own ordering).
51
+ minimum_tls_version: ssl.TLSVersion = ssl.TLSVersion.TLSv1_2
52
+
53
+
54
+ class EmailSendError(RuntimeError):
55
+ """Raised when a message could not be delivered."""
56
+
57
+
58
+ _TAG_RE = re.compile(r"<[^>]+>")
59
+
60
+
61
+ def _html_to_text(html_body: str) -> str:
62
+ """
63
+ Best-effort plain-text fallback derived from an HTML body, for callers
64
+ who only supply an HTML template. Not a full HTML renderer -- good
65
+ enough for a fallback that non-HTML mail clients will show, not a
66
+ substitute for an intentionally written text template.
67
+ """
68
+ text = re.sub(r"(?is)<(script|style)\b.*?</\1>", "", html_body)
69
+ text = re.sub(r"(?i)<br\s*/?>", "\n", text)
70
+ text = re.sub(r"(?i)</p>", "\n\n", text)
71
+ text = _TAG_RE.sub("", text)
72
+ text = html_lib.unescape(text)
73
+ lines = [line.strip() for line in text.splitlines()]
74
+ return "\n".join(line for line in lines if line) or " "
75
+
76
+
77
+ class TemplateRenderer:
78
+ """
79
+ Renders email bodies from Jinja2 templates stored in a directory.
80
+
81
+ Expected layout, e.g.:
82
+ templates/
83
+ welcome.html # HTML body -- autoescaped
84
+ welcome.txt # optional plain-text counterpart -- not autoescaped
85
+
86
+ HTML autoescaping is always on for .html/.htm templates, so values in
87
+ `context` (a user's name, a comment, anything not written by you) can't
88
+ inject markup or script into the rendered email. Undefined variables
89
+ raise instead of silently rendering as empty strings, so a typo'd or
90
+ missing field is caught at render time rather than mailed out blank.
91
+ """
92
+
93
+ def __init__(self, templates_dir: str | Path, strict: bool = True):
94
+ self.templates_dir = Path(templates_dir)
95
+ self._env = jinja2.Environment(
96
+ loader=jinja2.FileSystemLoader(str(self.templates_dir)),
97
+ autoescape=jinja2.select_autoescape(enabled_extensions=("html", "htm")),
98
+ undefined=jinja2.StrictUndefined if strict else jinja2.Undefined,
99
+ trim_blocks=True,
100
+ lstrip_blocks=True,
101
+ )
102
+
103
+ def render(self, template_name: str, context: dict[str, Any]) -> str:
104
+ try:
105
+ template = self._env.get_template(template_name)
106
+ except jinja2.TemplateNotFound as exc:
107
+ raise FileNotFoundError(
108
+ f"Template {template_name!r} not found in {self.templates_dir}"
109
+ ) from exc
110
+ try:
111
+ return template.render(**context)
112
+ except jinja2.UndefinedError as exc:
113
+ raise ValueError(
114
+ f"Missing field rendering {template_name!r}: {exc}"
115
+ ) from exc
116
+
117
+
118
+ class EmailSender:
119
+ """
120
+ Secure SMTP email sender with connection reuse.
121
+
122
+ Usage:
123
+ config = SMTPConfig(host="localhost", port=587, username="me@example.com",
124
+ password="app-password")
125
+
126
+ with EmailSender(config) as sender:
127
+ sender.send(
128
+ sender_addr="me@example.com",
129
+ to=["you@example.com"],
130
+ subject="Hello",
131
+ body="Plain text body",
132
+ html_body="<p>Optional HTML body</p>",
133
+ attachments=["report.pdf"],
134
+ )
135
+ """
136
+
137
+ def __init__(self, config: SMTPConfig):
138
+ self.config = config
139
+ self._connection: smtplib.SMTP | None = None
140
+
141
+ # -- connection lifecycle -------------------------------------------------
142
+
143
+ def connect(self) -> None:
144
+ if self._connection is not None:
145
+ return
146
+
147
+ if (
148
+ not self.config.use_ssl
149
+ and not self.config.use_starttls
150
+ and self.config.password
151
+ ):
152
+ raise ValueError(
153
+ "Refusing to send credentials over an unencrypted connection: "
154
+ "set use_ssl=True or use_starttls=True, or remove the password "
155
+ "if you really intend to connect in plaintext."
156
+ )
157
+
158
+ context = ssl.create_default_context()
159
+ context.minimum_version = self.config.minimum_tls_version
160
+ if not self.config.verify_tls:
161
+ # Only intended for trusted local/dev SMTP servers.
162
+ context.check_hostname = False
163
+ context.verify_mode = ssl.CERT_NONE
164
+ logger.warning(
165
+ "TLS certificate verification is disabled for %s", self.config.host
166
+ )
167
+
168
+ if self.config.use_ssl:
169
+ conn: smtplib.SMTP = smtplib.SMTP_SSL(
170
+ self.config.host,
171
+ self.config.port,
172
+ timeout=self.config.timeout,
173
+ context=context,
174
+ )
175
+ else:
176
+ conn = smtplib.SMTP(
177
+ self.config.host, self.config.port, timeout=self.config.timeout
178
+ )
179
+ conn.ehlo()
180
+ if self.config.use_starttls:
181
+ conn.starttls(context=context)
182
+ conn.ehlo()
183
+
184
+ if self.config.username and self.config.password:
185
+ conn.login(self.config.username, self.config.password)
186
+
187
+ self._connection = conn
188
+ logger.info(
189
+ "Connected to SMTP server %s:%s", self.config.host, self.config.port
190
+ )
191
+
192
+ def close(self) -> None:
193
+ if self._connection is not None:
194
+ try:
195
+ self._connection.quit()
196
+ except smtplib.SMTPException, OSError:
197
+ self._connection.close()
198
+ finally:
199
+ self._connection = None
200
+
201
+ def __enter__(self) -> Self:
202
+ self.connect()
203
+ return self
204
+
205
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
206
+ self.close()
207
+
208
+ # -- message construction / sending ----------------------------------------
209
+
210
+ @staticmethod
211
+ def _build_message(
212
+ sender_addr: str,
213
+ to: Sequence[str],
214
+ subject: str,
215
+ body: str,
216
+ html_body: str | None = None,
217
+ cc: Sequence[str] | None = None,
218
+ bcc: Sequence[str] | None = None,
219
+ reply_to: str | None = None,
220
+ attachments: Iterable[str | Path] | None = None,
221
+ ) -> tuple[EmailMessage, list[str]]:
222
+ msg = EmailMessage()
223
+ msg["From"] = sender_addr
224
+ msg["To"] = ", ".join(to)
225
+ msg["Subject"] = subject
226
+ if cc:
227
+ msg["Cc"] = ", ".join(cc)
228
+ if reply_to:
229
+ msg["Reply-To"] = reply_to
230
+
231
+ msg.set_content(body)
232
+ if html_body:
233
+ msg.add_alternative(html_body, subtype="html")
234
+
235
+ for path in attachments or []:
236
+ path = Path(path)
237
+ data = path.read_bytes()
238
+ ctype, _ = mimetypes.guess_type(path.name)
239
+ maintype, subtype = (ctype or "application/octet-stream").split("/", 1)
240
+ msg.add_attachment(
241
+ data, maintype=maintype, subtype=subtype, filename=path.name
242
+ )
243
+
244
+ all_recipients = list(to) + list(cc or []) + list(bcc or [])
245
+ return msg, all_recipients
246
+
247
+ def send(
248
+ self,
249
+ sender_addr: str,
250
+ to: Sequence[str],
251
+ subject: str,
252
+ body: str,
253
+ html_body: str | None = None,
254
+ cc: Sequence[str] | None = None,
255
+ bcc: Sequence[str] | None = None,
256
+ reply_to: str | None = None,
257
+ attachments: Iterable[str | Path] | None = None,
258
+ ) -> None:
259
+ """Send a single email. Opens a connection automatically if not already open."""
260
+ opened_here = False
261
+ if self._connection is None:
262
+ self.connect()
263
+ opened_here = True
264
+
265
+ msg, recipients = self._build_message(
266
+ sender_addr, to, subject, body, html_body, cc, bcc, reply_to, attachments
267
+ )
268
+
269
+ try:
270
+ assert self._connection is not None
271
+ self._connection.send_message(
272
+ msg, from_addr=sender_addr, to_addrs=recipients
273
+ )
274
+ logger.info("Sent email to %s recipients", len(recipients))
275
+ except smtplib.SMTPException as exc:
276
+ raise EmailSendError(f"Failed to send email: {exc}") from exc
277
+ finally:
278
+ if opened_here:
279
+ self.close()
280
+
281
+ def send_templated(
282
+ self,
283
+ renderer: TemplateRenderer,
284
+ sender_addr: str,
285
+ to: Sequence[str],
286
+ subject: str,
287
+ template: str,
288
+ context: dict[str, Any],
289
+ text_template: str | None = None,
290
+ cc: Sequence[str] | None = None,
291
+ bcc: Sequence[str] | None = None,
292
+ reply_to: str | None = None,
293
+ attachments: Iterable[str | Path] | None = None,
294
+ ) -> None:
295
+ """
296
+ Render `template` (an HTML file in renderer.templates_dir) with
297
+ `context` and send it.
298
+
299
+ If `text_template` is given, that file is rendered too and used as
300
+ the plain-text part (recommended -- write copy for text clients
301
+ deliberately rather than relying on auto-stripped HTML). Otherwise
302
+ a plain-text fallback is derived automatically from the rendered
303
+ HTML.
304
+ """
305
+ html_body = renderer.render(template, context)
306
+ body = (
307
+ renderer.render(text_template, context)
308
+ if text_template
309
+ else _html_to_text(html_body)
310
+ )
311
+
312
+ self.send(
313
+ sender_addr=sender_addr,
314
+ to=to,
315
+ subject=subject,
316
+ body=body,
317
+ html_body=html_body,
318
+ cc=cc,
319
+ bcc=bcc,
320
+ reply_to=reply_to,
321
+ attachments=attachments,
322
+ )
@@ -1,44 +0,0 @@
1
- import os
2
-
3
- from fastapi import APIRouter, HTTPException, Security
4
- from nctalk import TalkMessenger
5
- from pydantic import BaseModel, Field
6
-
7
- from util.auth import get_token_header
8
- from util.logging import logger
9
-
10
- router = APIRouter(prefix="/webhook", dependencies=[Security(get_token_header)])
11
-
12
-
13
- class NextcloudTalkWebhook(BaseModel):
14
- header: str = Field(..., min_length=1, max_length=200)
15
- message: str = Field(..., min_length=1, max_length=32000)
16
-
17
-
18
- @router.post("/databasus")
19
- async def databasus(webhook: NextcloudTalkWebhook):
20
- if webhook.header.strip() == "":
21
- raise HTTPException(status_code=422, detail="header cannot be empty")
22
- if webhook.message.strip() == "":
23
- raise HTTPException(status_code=422, detail="message cannot be empty")
24
-
25
- endpoint_url = os.environ.get("NEXTCLOUD_URL", "").strip()
26
- token = os.environ.get("NEXTCLOUD_TALK_TOKEN", "").strip()
27
- secret = os.environ.get("NEXTCLOUD_TALK_SECRET", "").strip()
28
-
29
- if not endpoint_url or not token or not secret:
30
- raise HTTPException(status_code=500, detail="Nextcloud Talk is not configured")
31
- if not endpoint_url.startswith("https://"):
32
- raise HTTPException(status_code=500, detail="NEXTCLOUD_URL must use https://")
33
-
34
- messenger = TalkMessenger(endpoint_url=endpoint_url, token=token, secret=secret)
35
-
36
- message = f"**{webhook.header}**\n{webhook.message}"
37
-
38
- if not messenger.send(message):
39
- logger.error("Failed to send message to Nextcloud Talk")
40
- raise HTTPException(
41
- status_code=502, detail="Failed to send message to Nextcloud Talk"
42
- )
43
-
44
- return {"status": "sent"}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes