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.
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/PKG-INFO +9 -2
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/README.md +6 -0
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/pyproject.toml +3 -2
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/api.py +2 -1
- js_api-20260908012207.dev0/src/controllers/webhook.py +44 -0
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/js_api.egg-info/PKG-INFO +9 -2
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/js_api.egg-info/SOURCES.txt +3 -1
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/js_api.egg-info/requires.txt +2 -1
- js_api-20260908012207.dev0/tests/test_webhook.py +123 -0
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/LICENSE +0 -0
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/setup.cfg +0 -0
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/__init__.py +0 -0
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/controllers/__init__.py +0 -0
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/controllers/docker.py +0 -0
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/controllers/health_check.py +0 -0
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/controllers/ip.py +0 -0
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/controllers/valentines_day.py +0 -0
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/database.py +0 -0
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/js_api.egg-info/dependency_links.txt +0 -0
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/js_api.egg-info/top_level.txt +0 -0
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/models.py +0 -0
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/util/__init__.py +0 -0
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/util/auth.py +0 -0
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/util/docker.py +0 -0
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/util/healthcheck.py +0 -0
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/util/logging.py +0 -0
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/tests/test_docker.py +0 -0
- {js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/tests/test_health_check.py +0 -0
- {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:
|
|
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.
|
|
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 = "
|
|
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.
|
|
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:
|
|
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.
|
|
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
|
|
@@ -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)
|
|
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
|
{js_api-20260830192902.dev0 → js_api-20260908012207.dev0}/src/js_api.egg-info/dependency_links.txt
RENAMED
|
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
|