js-api 20260830192902.dev0__tar.gz → 20260908012207.dev0__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 (29) hide show
  1. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/PKG-INFO +9 -2
  2. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/README.md +6 -0
  3. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/pyproject.toml +3 -2
  4. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/api.py +2 -1
  5. js_api-20260908012207.dev0/src/controllers/webhook.py +44 -0
  6. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/js_api.egg-info/PKG-INFO +9 -2
  7. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/js_api.egg-info/SOURCES.txt +3 -1
  8. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/js_api.egg-info/requires.txt +2 -1
  9. js_api-20260908012207.dev0/tests/test_webhook.py +123 -0
  10. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/LICENSE +0 -0
  11. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/setup.cfg +0 -0
  12. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/__init__.py +0 -0
  13. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/controllers/__init__.py +0 -0
  14. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/controllers/docker.py +0 -0
  15. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/controllers/health_check.py +0 -0
  16. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/controllers/ip.py +0 -0
  17. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/controllers/valentines_day.py +0 -0
  18. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/database.py +0 -0
  19. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/js_api.egg-info/dependency_links.txt +0 -0
  20. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/js_api.egg-info/top_level.txt +0 -0
  21. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/models.py +0 -0
  22. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/util/__init__.py +0 -0
  23. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/util/auth.py +0 -0
  24. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/util/docker.py +0 -0
  25. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/util/healthcheck.py +0 -0
  26. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/util/logging.py +0 -0
  27. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/tests/test_docker.py +0 -0
  28. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/tests/test_health_check.py +0 -0
  29. {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/tests/test_ip.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: js-api
3
- Version: 20260830192902.dev0
3
+ Version: 20260908012207.dev0
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
@@ -15,7 +15,8 @@ Requires-Dist: fastapi[standard]==0.141.1
15
15
  Requires-Dist: sqlalchemy==2.0.52
16
16
  Requires-Dist: sqlmodel==0.0.42
17
17
  Requires-Dist: python-dotenv==1.2.3
18
- Requires-Dist: psycopg[binary]==3.3.4
18
+ Requires-Dist: psycopg[binary]==3.3.5
19
+ Requires-Dist: nctalk>=0.0.4
19
20
  Dynamic: license-file
20
21
 
21
22
  # API
@@ -29,6 +30,12 @@ Simplest setup is to start from [compose.yml](https://github.com/jnstockley/api/
29
30
  - `DATABASE_URL` - The URL to connect to postgres DB. Must start with `postgresql+psycopg://`. Should be in the format specifiec in [template.env](https://github.com/jnstockley/api/blob/dev/template.env)
30
31
  - `TZ` - Timezone of the container
31
32
  - `PGTZ` - Timezone the Postgres container should use
33
+ - `NEXTCLOUD_URL` - Base URL (must be `https://`) of the Nextcloud instance used by `/webhook/nextcloud-talk`
34
+ - `NEXTCLOUD_TALK_TOKEN` - Conversation token of the registered [Nextcloud Talk bot](https://nextcloud-talk.readthedocs.io/en/latest/bot-list/) used by `/webhook/nextcloud-talk`
35
+ - `NEXTCLOUD_TALK_SECRET` - Shared secret of the registered Nextcloud Talk bot used by `/webhook/nextcloud-talk`
36
+
37
+ ## Webhooks
38
+ - `POST /webhook/nextcloud-talk` - Forwards a `header` and `message` to a Nextcloud Talk conversation via the [Nextcloud Talk Bot API](https://nextcloud-talk.readthedocs.io/en/latest/bot-list/), using the [nctalk](https://pypi.org/project/nctalk/) package. Requires the `X-API-KEY` header and a JSON body: `{"header": "...", "message": "..."}`
32
39
 
33
40
  ## How to Access
34
41
  Using the [compose.yml](https://github.com/jnstockley/api/blob/dev/compose.yml) file, you can access the API at `http://<IP>:5000/health-check`. If everything is setup correctly, you should see `{"status":"ok"}`
@@ -9,6 +9,12 @@ Simplest setup is to start from [compose.yml](https://github.com/jnstockley/api/
9
9
  - `DATABASE_URL` - The URL to connect to postgres DB. Must start with `postgresql+psycopg://`. Should be in the format specifiec in [template.env](https://github.com/jnstockley/api/blob/dev/template.env)
10
10
  - `TZ` - Timezone of the container
11
11
  - `PGTZ` - Timezone the Postgres container should use
12
+ - `NEXTCLOUD_URL` - Base URL (must be `https://`) of the Nextcloud instance used by `/webhook/nextcloud-talk`
13
+ - `NEXTCLOUD_TALK_TOKEN` - Conversation token of the registered [Nextcloud Talk bot](https://nextcloud-talk.readthedocs.io/en/latest/bot-list/) used by `/webhook/nextcloud-talk`
14
+ - `NEXTCLOUD_TALK_SECRET` - Shared secret of the registered Nextcloud Talk bot used by `/webhook/nextcloud-talk`
15
+
16
+ ## Webhooks
17
+ - `POST /webhook/nextcloud-talk` - Forwards a `header` and `message` to a Nextcloud Talk conversation via the [Nextcloud Talk Bot API](https://nextcloud-talk.readthedocs.io/en/latest/bot-list/), using the [nctalk](https://pypi.org/project/nctalk/) package. Requires the `X-API-KEY` header and a JSON body: `{"header": "...", "message": "..."}`
12
18
 
13
19
  ## How to Access
14
20
  Using the [compose.yml](https://github.com/jnstockley/api/blob/dev/compose.yml) file, you can access the API at `http://<IP>:5000/health-check`. If everything is setup correctly, you should see `{"status":"ok"}`
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "js-api"
3
- version = "20260830192902.dev"
3
+ version = "20260908012207.dev"
4
4
  description = "Custom API written in Python using FastAPI"
5
5
  authors = [
6
6
  {name = "Jack Stockley", email = "jack@jstockley.com"}
@@ -17,7 +17,8 @@ dependencies = [
17
17
  "sqlalchemy==2.0.52",
18
18
  "sqlmodel==0.0.42",
19
19
  "python-dotenv==1.2.3",
20
- "psycopg[binary]==3.3.4",
20
+ "psycopg[binary]==3.3.5",
21
+ "nctalk>=0.0.4",
21
22
  ]
22
23
  [project.urls]
23
24
  Homepage = "https://github.com/jnstockley/api"
@@ -7,7 +7,7 @@ from dotenv import load_dotenv
7
7
  from fastapi import FastAPI
8
8
 
9
9
  import models
10
- from controllers import docker, health_check, ip, valentines_day
10
+ from controllers import docker, health_check, ip, valentines_day, webhook
11
11
  from database import engine
12
12
  from util.healthcheck import healthcheck
13
13
 
@@ -44,6 +44,7 @@ app.include_router(health_check.router)
44
44
  app.include_router(docker.router)
45
45
  app.include_router(ip.router)
46
46
  app.include_router(valentines_day.router)
47
+ app.include_router(webhook.router)
47
48
 
48
49
  if __name__ == "__main__":
49
50
  load_dotenv()
@@ -0,0 +1,44 @@
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"}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: js-api
3
- Version: 20260830192902.dev0
3
+ Version: 20260908012207.dev0
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
@@ -15,7 +15,8 @@ Requires-Dist: fastapi[standard]==0.141.1
15
15
  Requires-Dist: sqlalchemy==2.0.52
16
16
  Requires-Dist: sqlmodel==0.0.42
17
17
  Requires-Dist: python-dotenv==1.2.3
18
- Requires-Dist: psycopg[binary]==3.3.4
18
+ Requires-Dist: psycopg[binary]==3.3.5
19
+ Requires-Dist: nctalk>=0.0.4
19
20
  Dynamic: license-file
20
21
 
21
22
  # API
@@ -29,6 +30,12 @@ Simplest setup is to start from [compose.yml](https://github.com/jnstockley/api/
29
30
  - `DATABASE_URL` - The URL to connect to postgres DB. Must start with `postgresql+psycopg://`. Should be in the format specifiec in [template.env](https://github.com/jnstockley/api/blob/dev/template.env)
30
31
  - `TZ` - Timezone of the container
31
32
  - `PGTZ` - Timezone the Postgres container should use
33
+ - `NEXTCLOUD_URL` - Base URL (must be `https://`) of the Nextcloud instance used by `/webhook/nextcloud-talk`
34
+ - `NEXTCLOUD_TALK_TOKEN` - Conversation token of the registered [Nextcloud Talk bot](https://nextcloud-talk.readthedocs.io/en/latest/bot-list/) used by `/webhook/nextcloud-talk`
35
+ - `NEXTCLOUD_TALK_SECRET` - Shared secret of the registered Nextcloud Talk bot used by `/webhook/nextcloud-talk`
36
+
37
+ ## Webhooks
38
+ - `POST /webhook/nextcloud-talk` - Forwards a `header` and `message` to a Nextcloud Talk conversation via the [Nextcloud Talk Bot API](https://nextcloud-talk.readthedocs.io/en/latest/bot-list/), using the [nctalk](https://pypi.org/project/nctalk/) package. Requires the `X-API-KEY` header and a JSON body: `{"header": "...", "message": "..."}`
32
39
 
33
40
  ## How to Access
34
41
  Using the [compose.yml](https://github.com/jnstockley/api/blob/dev/compose.yml) file, you can access the API at `http://<IP>:5000/health-check`. If everything is setup correctly, you should see `{"status":"ok"}`
@@ -10,6 +10,7 @@ src/controllers/docker.py
10
10
  src/controllers/health_check.py
11
11
  src/controllers/ip.py
12
12
  src/controllers/valentines_day.py
13
+ src/controllers/webhook.py
13
14
  src/js_api.egg-info/PKG-INFO
14
15
  src/js_api.egg-info/SOURCES.txt
15
16
  src/js_api.egg-info/dependency_links.txt
@@ -22,4 +23,5 @@ src/util/healthcheck.py
22
23
  src/util/logging.py
23
24
  tests/test_docker.py
24
25
  tests/test_health_check.py
25
- tests/test_ip.py
26
+ tests/test_ip.py
27
+ tests/test_webhook.py
@@ -2,4 +2,5 @@ fastapi[standard]==0.141.1
2
2
  sqlalchemy==2.0.52
3
3
  sqlmodel==0.0.42
4
4
  python-dotenv==1.2.3
5
- psycopg[binary]==3.3.4
5
+ psycopg[binary]==3.3.5
6
+ nctalk>=0.0.4
@@ -0,0 +1,123 @@
1
+ import os
2
+ from unittest import TestCase
3
+ from unittest.mock import patch
4
+
5
+ from fastapi.testclient import TestClient
6
+ from sqlalchemy.orm import sessionmaker
7
+ from testcontainers.postgres import PostgresContainer
8
+
9
+ import models
10
+ from database import engine, get_db
11
+ from src.api import app
12
+
13
+ postgres = PostgresContainer("postgres:17-alpine").start()
14
+
15
+ DATABASE_URL = postgres.get_connection_url(driver="psycopg")
16
+ os.environ["DATABASE_URL"] = DATABASE_URL
17
+
18
+ models.Base.metadata.create_all(bind=engine)
19
+
20
+ TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
21
+
22
+ client = TestClient(app)
23
+
24
+
25
+ def override_get_db():
26
+ database = TestingSessionLocal()
27
+ yield database
28
+ database.close()
29
+
30
+
31
+ app.dependency_overrides[get_db] = override_get_db
32
+
33
+
34
+ class TestWebhook(TestCase):
35
+ def setUp(self):
36
+ os.environ["NEXTCLOUD_URL"] = "https://nextcloud.example.com"
37
+ os.environ["NEXTCLOUD_TALK_TOKEN"] = "test-token"
38
+ os.environ["NEXTCLOUD_TALK_SECRET"] = "test-secret"
39
+
40
+ def test_missing_body(self):
41
+ api_key = os.environ["API_KEY"]
42
+ header = {"X-API-KEY": api_key}
43
+ response = client.post("/webhook/databasus", headers=header, json={})
44
+ assert response.status_code == 422
45
+
46
+ def test_empty_header(self):
47
+ api_key = os.environ["API_KEY"]
48
+ header = {"X-API-KEY": api_key}
49
+ response = client.post(
50
+ "/webhook/databasus",
51
+ headers=header,
52
+ json={"header": "", "message": "hello"},
53
+ )
54
+ assert response.status_code == 422
55
+
56
+ def test_empty_message(self):
57
+ api_key = os.environ["API_KEY"]
58
+ header = {"X-API-KEY": api_key}
59
+ response = client.post(
60
+ "/webhook/databasus",
61
+ headers=header,
62
+ json={"header": "title", "message": ""},
63
+ )
64
+ assert response.status_code == 422
65
+
66
+ def test_not_configured(self):
67
+ api_key = os.environ["API_KEY"]
68
+ header = {"X-API-KEY": api_key}
69
+ del os.environ["NEXTCLOUD_URL"]
70
+ try:
71
+ response = client.post(
72
+ "/webhook/databasus",
73
+ headers=header,
74
+ json={"header": "title", "message": "hello"},
75
+ )
76
+ assert response.status_code == 500
77
+ finally:
78
+ os.environ["NEXTCLOUD_URL"] = "https://nextcloud.example.com"
79
+
80
+ def test_insecure_url(self):
81
+ api_key = os.environ["API_KEY"]
82
+ header = {"X-API-KEY": api_key}
83
+ os.environ["NEXTCLOUD_URL"] = "http://nextcloud.example.com"
84
+ try:
85
+ response = client.post(
86
+ "/webhook/databasus",
87
+ headers=header,
88
+ json={"header": "title", "message": "hello"},
89
+ )
90
+ assert response.status_code == 500
91
+ finally:
92
+ os.environ["NEXTCLOUD_URL"] = "https://nextcloud.example.com"
93
+
94
+ @patch("controllers.webhook.TalkMessenger.send", return_value=True)
95
+ def test_send_message(self, mock_send):
96
+ api_key = os.environ["API_KEY"]
97
+ header = {"X-API-KEY": api_key}
98
+ response = client.post(
99
+ "/webhook/databasus",
100
+ headers=header,
101
+ json={"header": "title", "message": "hello"},
102
+ )
103
+ assert response.status_code == 200
104
+ assert response.json() == {"status": "sent"}
105
+ mock_send.assert_called_once_with("**title**\nhello")
106
+
107
+ @patch("controllers.webhook.TalkMessenger.send", return_value=False)
108
+ def test_send_message_failure(self, mock_send):
109
+ api_key = os.environ["API_KEY"]
110
+ header = {"X-API-KEY": api_key}
111
+ response = client.post(
112
+ "/webhook/databasus",
113
+ headers=header,
114
+ json={"header": "title", "message": "hello"},
115
+ )
116
+ assert response.status_code == 502
117
+
118
+ def test_missing_api_key(self):
119
+ response = client.post(
120
+ "/webhook/databasus",
121
+ json={"header": "title", "message": "hello"},
122
+ )
123
+ assert response.status_code in (401, 422)