kryten-api-gate 0.2.0__py3-none-any.whl
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.
- kryten_api_gate/__init__.py +8 -0
- kryten_api_gate/__main__.py +126 -0
- kryten_api_gate/app.py +47 -0
- kryten_api_gate/auth.py +36 -0
- kryten_api_gate/config.py +62 -0
- kryten_api_gate/deps.py +16 -0
- kryten_api_gate/routes/__init__.py +1 -0
- kryten_api_gate/routes/admin.py +187 -0
- kryten_api_gate/routes/chat.py +42 -0
- kryten_api_gate/routes/emotes.py +71 -0
- kryten_api_gate/routes/filters.py +77 -0
- kryten_api_gate/routes/kv.py +82 -0
- kryten_api_gate/routes/library.py +33 -0
- kryten_api_gate/routes/moderation.py +108 -0
- kryten_api_gate/routes/playback.py +61 -0
- kryten_api_gate/routes/playlist.py +105 -0
- kryten_api_gate/routes/polls.py +58 -0
- kryten_api_gate/routes/state.py +46 -0
- kryten_api_gate/routes/system.py +64 -0
- kryten_api_gate/schemas/__init__.py +1 -0
- kryten_api_gate/schemas/responses.py +24 -0
- kryten_api_gate-0.2.0.dist-info/METADATA +113 -0
- kryten_api_gate-0.2.0.dist-info/RECORD +25 -0
- kryten_api_gate-0.2.0.dist-info/WHEEL +4 -0
- kryten_api_gate-0.2.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Entry point for kryten-api-gate service."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import logging
|
|
6
|
+
import signal
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
import uvicorn
|
|
10
|
+
from kryten import KrytenClient
|
|
11
|
+
|
|
12
|
+
from .app import create_app
|
|
13
|
+
from .config import load_config
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("kryten_api_gate")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def parse_args() -> argparse.Namespace:
|
|
19
|
+
parser = argparse.ArgumentParser(description="Kryten API Gate — HTTP REST gateway")
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"--config",
|
|
22
|
+
type=str,
|
|
23
|
+
default=None,
|
|
24
|
+
help="Path to config JSON file",
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"--log-level",
|
|
28
|
+
type=str,
|
|
29
|
+
default=None,
|
|
30
|
+
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
|
|
31
|
+
help="Log level override",
|
|
32
|
+
)
|
|
33
|
+
return parser.parse_args()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def setup_logging(level: str) -> None:
|
|
37
|
+
logging.basicConfig(
|
|
38
|
+
level=getattr(logging, level.upper()),
|
|
39
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
40
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def main_async() -> None:
|
|
45
|
+
args = parse_args()
|
|
46
|
+
config = load_config(args.config)
|
|
47
|
+
|
|
48
|
+
log_level = args.log_level or config.log_level
|
|
49
|
+
setup_logging(log_level)
|
|
50
|
+
|
|
51
|
+
logger.info("Starting kryten-api-gate v0.1.0")
|
|
52
|
+
logger.info(f"Channel: {config.channel} @ {config.domain}")
|
|
53
|
+
logger.info(f"NATS: {config.nats_url}")
|
|
54
|
+
logger.info(f"HTTP: {config.http_host}:{config.http_port}")
|
|
55
|
+
|
|
56
|
+
# Create KrytenClient
|
|
57
|
+
client_config = {
|
|
58
|
+
"nats": {"servers": [config.nats_url]},
|
|
59
|
+
"channels": [{"domain": config.domain, "channel": config.channel}],
|
|
60
|
+
"service": {
|
|
61
|
+
"name": config.service_name,
|
|
62
|
+
"version": "0.1.0",
|
|
63
|
+
"enable_lifecycle": True,
|
|
64
|
+
"enable_heartbeat": True,
|
|
65
|
+
"enable_discovery": True,
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
client = KrytenClient(client_config)
|
|
69
|
+
|
|
70
|
+
# Connect to NATS
|
|
71
|
+
await client.connect()
|
|
72
|
+
logger.info("Connected to NATS")
|
|
73
|
+
|
|
74
|
+
# Create FastAPI app and inject state
|
|
75
|
+
app = create_app()
|
|
76
|
+
app.state.client = client
|
|
77
|
+
app.state.config = config
|
|
78
|
+
|
|
79
|
+
# Setup shutdown event
|
|
80
|
+
shutdown_event = asyncio.Event()
|
|
81
|
+
|
|
82
|
+
def _signal_handler() -> None:
|
|
83
|
+
logger.info("Shutdown signal received")
|
|
84
|
+
shutdown_event.set()
|
|
85
|
+
|
|
86
|
+
loop = asyncio.get_running_loop()
|
|
87
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
88
|
+
try:
|
|
89
|
+
loop.add_signal_handler(sig, _signal_handler)
|
|
90
|
+
except NotImplementedError:
|
|
91
|
+
# Windows doesn't support add_signal_handler
|
|
92
|
+
signal.signal(sig, lambda s, f: _signal_handler())
|
|
93
|
+
|
|
94
|
+
# Run uvicorn
|
|
95
|
+
uv_config = uvicorn.Config(
|
|
96
|
+
app,
|
|
97
|
+
host=config.http_host,
|
|
98
|
+
port=config.http_port,
|
|
99
|
+
log_level=log_level.lower(),
|
|
100
|
+
access_log=log_level.upper() == "DEBUG",
|
|
101
|
+
)
|
|
102
|
+
server = uvicorn.Server(uv_config)
|
|
103
|
+
|
|
104
|
+
# Run server with shutdown monitoring
|
|
105
|
+
server_task = asyncio.create_task(server.serve())
|
|
106
|
+
|
|
107
|
+
await shutdown_event.wait()
|
|
108
|
+
|
|
109
|
+
# Graceful shutdown
|
|
110
|
+
logger.info("Shutting down...")
|
|
111
|
+
server.should_exit = True
|
|
112
|
+
await server_task
|
|
113
|
+
await client.disconnect("API gate shutting down")
|
|
114
|
+
logger.info("Shutdown complete")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def main() -> None:
|
|
118
|
+
try:
|
|
119
|
+
asyncio.run(main_async())
|
|
120
|
+
except KeyboardInterrupt:
|
|
121
|
+
pass
|
|
122
|
+
sys.exit(0)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
main()
|
kryten_api_gate/app.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""FastAPI application factory for kryten-api-gate."""
|
|
2
|
+
|
|
3
|
+
from fastapi import FastAPI
|
|
4
|
+
|
|
5
|
+
from . import __version__
|
|
6
|
+
from .routes import (
|
|
7
|
+
admin,
|
|
8
|
+
chat,
|
|
9
|
+
emotes,
|
|
10
|
+
filters,
|
|
11
|
+
kv,
|
|
12
|
+
library,
|
|
13
|
+
moderation,
|
|
14
|
+
playback,
|
|
15
|
+
playlist,
|
|
16
|
+
polls,
|
|
17
|
+
state,
|
|
18
|
+
system,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def create_app() -> FastAPI:
|
|
23
|
+
"""Create and configure the FastAPI application."""
|
|
24
|
+
app = FastAPI(
|
|
25
|
+
title="kryten-api-gate",
|
|
26
|
+
version=__version__,
|
|
27
|
+
description="HTTP REST gateway for the Kryten ecosystem",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Public routes (no auth)
|
|
31
|
+
app.include_router(system.public_router, prefix="/api/v1/system", tags=["system"])
|
|
32
|
+
|
|
33
|
+
# Protected routes (require API key)
|
|
34
|
+
app.include_router(system.router, prefix="/api/v1/system", tags=["system"])
|
|
35
|
+
app.include_router(chat.router, prefix="/api/v1/chat", tags=["chat"])
|
|
36
|
+
app.include_router(playlist.router, prefix="/api/v1/playlist", tags=["playlist"])
|
|
37
|
+
app.include_router(playback.router, prefix="/api/v1/playback", tags=["playback"])
|
|
38
|
+
app.include_router(moderation.router, prefix="/api/v1/moderation", tags=["moderation"])
|
|
39
|
+
app.include_router(admin.router, prefix="/api/v1/admin", tags=["admin"])
|
|
40
|
+
app.include_router(emotes.router, prefix="/api/v1/emotes", tags=["emotes"])
|
|
41
|
+
app.include_router(filters.router, prefix="/api/v1/filters", tags=["filters"])
|
|
42
|
+
app.include_router(polls.router, prefix="/api/v1/polls", tags=["polls"])
|
|
43
|
+
app.include_router(library.router, prefix="/api/v1/library", tags=["library"])
|
|
44
|
+
app.include_router(kv.router, prefix="/api/v1/kv", tags=["kv"])
|
|
45
|
+
app.include_router(state.router, prefix="/api/v1/state", tags=["state"])
|
|
46
|
+
|
|
47
|
+
return app
|
kryten_api_gate/auth.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Authentication dependency for kryten-api-gate."""
|
|
2
|
+
|
|
3
|
+
from fastapi import Depends, HTTPException, Request, status
|
|
4
|
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
5
|
+
|
|
6
|
+
from .config import Config
|
|
7
|
+
|
|
8
|
+
_bearer_scheme = HTTPBearer(auto_error=False)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def verify_api_key(
|
|
12
|
+
request: Request,
|
|
13
|
+
credentials: HTTPAuthorizationCredentials | None = Depends(_bearer_scheme),
|
|
14
|
+
) -> str:
|
|
15
|
+
"""Validate Bearer token against configured API keys.
|
|
16
|
+
|
|
17
|
+
Returns the validated API key string.
|
|
18
|
+
Raises 401 if missing or invalid.
|
|
19
|
+
"""
|
|
20
|
+
config: Config = request.app.state.config
|
|
21
|
+
|
|
22
|
+
if credentials is None:
|
|
23
|
+
raise HTTPException(
|
|
24
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
25
|
+
detail="Missing authorization header",
|
|
26
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if credentials.credentials not in config.api_keys:
|
|
30
|
+
raise HTTPException(
|
|
31
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
32
|
+
detail="Invalid API key",
|
|
33
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return credentials.credentials
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Configuration model for kryten-api-gate."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
# Config search paths in priority order
|
|
12
|
+
CONFIG_SEARCH_PATHS = [
|
|
13
|
+
Path("./config.json"),
|
|
14
|
+
Path("/etc/kryten/api-gate/config.json"),
|
|
15
|
+
Path("/opt/kryten/api-gate/config.json"),
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Config(BaseModel):
|
|
20
|
+
"""API gate configuration."""
|
|
21
|
+
|
|
22
|
+
nats_url: str = Field(default="nats://localhost:4222")
|
|
23
|
+
channel: str = Field(description="CyTube channel name")
|
|
24
|
+
domain: str = Field(default="cytu.be")
|
|
25
|
+
http_host: str = Field(default="127.0.0.1")
|
|
26
|
+
http_port: int = Field(default=28288)
|
|
27
|
+
api_keys: list[str] = Field(description="Authorized bearer tokens")
|
|
28
|
+
kv_read_only: bool = Field(default=False)
|
|
29
|
+
service_name: str = Field(default="api-gate")
|
|
30
|
+
log_level: str = Field(default="INFO")
|
|
31
|
+
disabled_routes: list[str] = Field(
|
|
32
|
+
default_factory=list,
|
|
33
|
+
description="List of route operation IDs to disable (e.g. 'playback_pause', 'playback_seek')",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_config(config_path: str | None = None) -> Config:
|
|
38
|
+
"""Load configuration from JSON file.
|
|
39
|
+
|
|
40
|
+
Search order:
|
|
41
|
+
1. Explicit path (if provided)
|
|
42
|
+
2. ./config.json (cwd)
|
|
43
|
+
3. /etc/kryten/api-gate/config.json
|
|
44
|
+
4. /opt/kryten/api-gate/config.json
|
|
45
|
+
"""
|
|
46
|
+
if config_path:
|
|
47
|
+
path = Path(config_path)
|
|
48
|
+
if not path.exists():
|
|
49
|
+
raise FileNotFoundError(f"Config file not found: {config_path}")
|
|
50
|
+
logger.info(f"Loading config from: {path}")
|
|
51
|
+
data = json.loads(path.read_text())
|
|
52
|
+
return Config(**data)
|
|
53
|
+
|
|
54
|
+
for path in CONFIG_SEARCH_PATHS:
|
|
55
|
+
if path.exists():
|
|
56
|
+
logger.info(f"Loading config from: {path}")
|
|
57
|
+
data = json.loads(path.read_text())
|
|
58
|
+
return Config(**data)
|
|
59
|
+
|
|
60
|
+
raise FileNotFoundError(
|
|
61
|
+
f"No config file found. Searched: {[str(p) for p in CONFIG_SEARCH_PATHS]}"
|
|
62
|
+
)
|
kryten_api_gate/deps.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Dependency injection for kryten-api-gate routes."""
|
|
2
|
+
|
|
3
|
+
from fastapi import Request
|
|
4
|
+
from kryten import KrytenClient
|
|
5
|
+
|
|
6
|
+
from .config import Config
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_client(request: Request) -> KrytenClient:
|
|
10
|
+
"""Get the shared KrytenClient instance."""
|
|
11
|
+
return request.app.state.client
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_config(request: Request) -> Config:
|
|
15
|
+
"""Get the loaded configuration."""
|
|
16
|
+
return request.app.state.config
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Route modules for kryten-api-gate."""
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Admin routes — MOTD, CSS, JS, options, permissions, ranks, channel log."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends
|
|
4
|
+
from kryten import KrytenClient
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from ..auth import verify_api_key
|
|
8
|
+
from ..config import Config
|
|
9
|
+
from ..deps import get_client, get_config
|
|
10
|
+
|
|
11
|
+
router = APIRouter(dependencies=[Depends(verify_api_key)])
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MotdRequest(BaseModel):
|
|
15
|
+
motd: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CssRequest(BaseModel):
|
|
19
|
+
css: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class JsRequest(BaseModel):
|
|
23
|
+
js: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OptionsRequest(BaseModel):
|
|
27
|
+
options: dict
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PermissionsRequest(BaseModel):
|
|
31
|
+
permissions: dict
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SetRankRequest(BaseModel):
|
|
35
|
+
username: str
|
|
36
|
+
rank: int
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# --- MOTD ---
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@router.get("/motd")
|
|
43
|
+
async def get_motd(
|
|
44
|
+
client: KrytenClient = Depends(get_client),
|
|
45
|
+
config: Config = Depends(get_config),
|
|
46
|
+
) -> dict:
|
|
47
|
+
motd = await client.get_state_motd(config.channel, domain=config.domain)
|
|
48
|
+
return {"motd": motd}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@router.put("/motd")
|
|
52
|
+
async def set_motd(
|
|
53
|
+
body: MotdRequest,
|
|
54
|
+
client: KrytenClient = Depends(get_client),
|
|
55
|
+
config: Config = Depends(get_config),
|
|
56
|
+
) -> dict:
|
|
57
|
+
msg_id = await client.set_motd(config.channel, body.motd, domain=config.domain)
|
|
58
|
+
return {"message_id": msg_id}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# --- CSS ---
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@router.get("/css")
|
|
65
|
+
async def get_channel_css(
|
|
66
|
+
client: KrytenClient = Depends(get_client),
|
|
67
|
+
config: Config = Depends(get_config),
|
|
68
|
+
) -> dict:
|
|
69
|
+
css = await client.get_state_channel_css(config.channel, domain=config.domain)
|
|
70
|
+
return {"css": css}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@router.put("/css")
|
|
74
|
+
async def set_channel_css(
|
|
75
|
+
body: CssRequest,
|
|
76
|
+
client: KrytenClient = Depends(get_client),
|
|
77
|
+
config: Config = Depends(get_config),
|
|
78
|
+
) -> dict:
|
|
79
|
+
msg_id = await client.set_channel_css(config.channel, body.css, domain=config.domain)
|
|
80
|
+
return {"message_id": msg_id}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# --- JS ---
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@router.get("/js")
|
|
87
|
+
async def get_channel_js(
|
|
88
|
+
client: KrytenClient = Depends(get_client),
|
|
89
|
+
config: Config = Depends(get_config),
|
|
90
|
+
) -> dict:
|
|
91
|
+
js = await client.get_state_channel_js(config.channel, domain=config.domain)
|
|
92
|
+
return {"js": js}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@router.put("/js")
|
|
96
|
+
async def set_channel_js(
|
|
97
|
+
body: JsRequest,
|
|
98
|
+
client: KrytenClient = Depends(get_client),
|
|
99
|
+
config: Config = Depends(get_config),
|
|
100
|
+
) -> dict:
|
|
101
|
+
msg_id = await client.set_channel_js(config.channel, body.js, domain=config.domain)
|
|
102
|
+
return {"message_id": msg_id}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# --- Options ---
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@router.get("/options")
|
|
109
|
+
async def get_options(
|
|
110
|
+
client: KrytenClient = Depends(get_client),
|
|
111
|
+
config: Config = Depends(get_config),
|
|
112
|
+
) -> dict:
|
|
113
|
+
options = await client.get_state_channel_options(config.channel, domain=config.domain)
|
|
114
|
+
return {"options": options}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@router.put("/options")
|
|
118
|
+
async def set_options(
|
|
119
|
+
body: OptionsRequest,
|
|
120
|
+
client: KrytenClient = Depends(get_client),
|
|
121
|
+
config: Config = Depends(get_config),
|
|
122
|
+
) -> dict:
|
|
123
|
+
msg_id = await client.set_options(config.channel, body.options, domain=config.domain)
|
|
124
|
+
return {"message_id": msg_id}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# --- Permissions ---
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@router.get("/permissions")
|
|
131
|
+
async def get_permissions(
|
|
132
|
+
client: KrytenClient = Depends(get_client),
|
|
133
|
+
config: Config = Depends(get_config),
|
|
134
|
+
) -> dict:
|
|
135
|
+
permissions = await client.get_state_channel_permissions(
|
|
136
|
+
config.channel, domain=config.domain
|
|
137
|
+
)
|
|
138
|
+
return {"permissions": permissions}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@router.put("/permissions")
|
|
142
|
+
async def set_permissions(
|
|
143
|
+
body: PermissionsRequest,
|
|
144
|
+
client: KrytenClient = Depends(get_client),
|
|
145
|
+
config: Config = Depends(get_config),
|
|
146
|
+
) -> dict:
|
|
147
|
+
msg_id = await client.set_permissions(
|
|
148
|
+
config.channel, body.permissions, domain=config.domain
|
|
149
|
+
)
|
|
150
|
+
return {"message_id": msg_id}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# --- Ranks ---
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@router.get("/ranks")
|
|
157
|
+
async def request_channel_ranks(
|
|
158
|
+
client: KrytenClient = Depends(get_client),
|
|
159
|
+
config: Config = Depends(get_config),
|
|
160
|
+
) -> dict:
|
|
161
|
+
msg_id = await client.request_channel_ranks(config.channel, domain=config.domain)
|
|
162
|
+
return {"message_id": msg_id}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@router.put("/rank")
|
|
166
|
+
async def set_channel_rank(
|
|
167
|
+
body: SetRankRequest,
|
|
168
|
+
client: KrytenClient = Depends(get_client),
|
|
169
|
+
config: Config = Depends(get_config),
|
|
170
|
+
) -> dict:
|
|
171
|
+
msg_id = await client.set_channel_rank(
|
|
172
|
+
config.channel, body.username, body.rank, domain=config.domain
|
|
173
|
+
)
|
|
174
|
+
return {"message_id": msg_id}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# --- Channel Log ---
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@router.get("/log")
|
|
181
|
+
async def read_chan_log(
|
|
182
|
+
count: int = 100,
|
|
183
|
+
client: KrytenClient = Depends(get_client),
|
|
184
|
+
config: Config = Depends(get_config),
|
|
185
|
+
) -> dict:
|
|
186
|
+
msg_id = await client.read_chan_log(config.channel, count, domain=config.domain)
|
|
187
|
+
return {"message_id": msg_id}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Chat routes — send messages and PMs."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends
|
|
4
|
+
from kryten import KrytenClient
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from ..auth import verify_api_key
|
|
8
|
+
from ..config import Config
|
|
9
|
+
from ..deps import get_client, get_config
|
|
10
|
+
|
|
11
|
+
router = APIRouter(dependencies=[Depends(verify_api_key)])
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SendChatRequest(BaseModel):
|
|
15
|
+
message: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SendPmRequest(BaseModel):
|
|
19
|
+
username: str
|
|
20
|
+
message: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@router.post("/send")
|
|
24
|
+
async def send_chat(
|
|
25
|
+
body: SendChatRequest,
|
|
26
|
+
client: KrytenClient = Depends(get_client),
|
|
27
|
+
config: Config = Depends(get_config),
|
|
28
|
+
) -> dict:
|
|
29
|
+
msg_id = await client.send_chat(config.channel, body.message, domain=config.domain)
|
|
30
|
+
return {"message_id": msg_id}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@router.post("/pm")
|
|
34
|
+
async def send_pm(
|
|
35
|
+
body: SendPmRequest,
|
|
36
|
+
client: KrytenClient = Depends(get_client),
|
|
37
|
+
config: Config = Depends(get_config),
|
|
38
|
+
) -> dict:
|
|
39
|
+
msg_id = await client.send_pm(
|
|
40
|
+
config.channel, body.username, body.message, domain=config.domain
|
|
41
|
+
)
|
|
42
|
+
return {"message_id": msg_id}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Emotes routes — update, remove, bulk import/export emotes."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends
|
|
4
|
+
from kryten import KrytenClient
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from ..auth import verify_api_key
|
|
8
|
+
from ..config import Config
|
|
9
|
+
from ..deps import get_client, get_config
|
|
10
|
+
|
|
11
|
+
router = APIRouter(dependencies=[Depends(verify_api_key)])
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UpdateEmoteRequest(BaseModel):
|
|
15
|
+
image: str
|
|
16
|
+
source: str = "imgur"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EmoteImportItem(BaseModel):
|
|
20
|
+
name: str
|
|
21
|
+
image: str
|
|
22
|
+
source: str = "imgur"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BulkImportRequest(BaseModel):
|
|
26
|
+
emotes: list[EmoteImportItem]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@router.get("/")
|
|
30
|
+
async def export_emotes(
|
|
31
|
+
client: KrytenClient = Depends(get_client),
|
|
32
|
+
config: Config = Depends(get_config),
|
|
33
|
+
) -> dict:
|
|
34
|
+
emotes = await client.export_emotes(config.channel, domain=config.domain)
|
|
35
|
+
return {"emotes": emotes}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@router.post("/import")
|
|
39
|
+
async def import_emotes(
|
|
40
|
+
body: BulkImportRequest,
|
|
41
|
+
client: KrytenClient = Depends(get_client),
|
|
42
|
+
config: Config = Depends(get_config),
|
|
43
|
+
) -> dict:
|
|
44
|
+
emote_dicts = [e.model_dump() for e in body.emotes]
|
|
45
|
+
message_ids = await client.import_emotes(
|
|
46
|
+
config.channel, emote_dicts, domain=config.domain
|
|
47
|
+
)
|
|
48
|
+
return {"message_ids": message_ids, "count": len(message_ids)}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@router.put("/{name}")
|
|
52
|
+
async def update_emote(
|
|
53
|
+
name: str,
|
|
54
|
+
body: UpdateEmoteRequest,
|
|
55
|
+
client: KrytenClient = Depends(get_client),
|
|
56
|
+
config: Config = Depends(get_config),
|
|
57
|
+
) -> dict:
|
|
58
|
+
msg_id = await client.update_emote(
|
|
59
|
+
config.channel, name, body.image, body.source, domain=config.domain
|
|
60
|
+
)
|
|
61
|
+
return {"message_id": msg_id}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@router.delete("/{name}")
|
|
65
|
+
async def remove_emote(
|
|
66
|
+
name: str,
|
|
67
|
+
client: KrytenClient = Depends(get_client),
|
|
68
|
+
config: Config = Depends(get_config),
|
|
69
|
+
) -> dict:
|
|
70
|
+
msg_id = await client.remove_emote(config.channel, name, domain=config.domain)
|
|
71
|
+
return {"message_id": msg_id}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Filters routes — add, update, remove chat filters."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends
|
|
4
|
+
from kryten import KrytenClient
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from ..auth import verify_api_key
|
|
8
|
+
from ..config import Config
|
|
9
|
+
from ..deps import get_client, get_config
|
|
10
|
+
|
|
11
|
+
router = APIRouter(dependencies=[Depends(verify_api_key)])
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FilterRequest(BaseModel):
|
|
15
|
+
name: str
|
|
16
|
+
source: str
|
|
17
|
+
flags: str = "gi"
|
|
18
|
+
replace: str = ""
|
|
19
|
+
filterlinks: bool = False
|
|
20
|
+
active: bool = True
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class UpdateFilterRequest(BaseModel):
|
|
24
|
+
source: str
|
|
25
|
+
flags: str = "gi"
|
|
26
|
+
replace: str = ""
|
|
27
|
+
filterlinks: bool = False
|
|
28
|
+
active: bool = True
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@router.post("/")
|
|
32
|
+
async def add_filter(
|
|
33
|
+
body: FilterRequest,
|
|
34
|
+
client: KrytenClient = Depends(get_client),
|
|
35
|
+
config: Config = Depends(get_config),
|
|
36
|
+
) -> dict:
|
|
37
|
+
msg_id = await client.add_filter(
|
|
38
|
+
config.channel,
|
|
39
|
+
body.name,
|
|
40
|
+
body.source,
|
|
41
|
+
body.flags,
|
|
42
|
+
body.replace,
|
|
43
|
+
filterlinks=body.filterlinks,
|
|
44
|
+
active=body.active,
|
|
45
|
+
domain=config.domain,
|
|
46
|
+
)
|
|
47
|
+
return {"message_id": msg_id}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@router.put("/{name}")
|
|
51
|
+
async def update_filter(
|
|
52
|
+
name: str,
|
|
53
|
+
body: UpdateFilterRequest,
|
|
54
|
+
client: KrytenClient = Depends(get_client),
|
|
55
|
+
config: Config = Depends(get_config),
|
|
56
|
+
) -> dict:
|
|
57
|
+
msg_id = await client.update_filter(
|
|
58
|
+
config.channel,
|
|
59
|
+
name,
|
|
60
|
+
body.source,
|
|
61
|
+
body.flags,
|
|
62
|
+
body.replace,
|
|
63
|
+
filterlinks=body.filterlinks,
|
|
64
|
+
active=body.active,
|
|
65
|
+
domain=config.domain,
|
|
66
|
+
)
|
|
67
|
+
return {"message_id": msg_id}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@router.delete("/{name}")
|
|
71
|
+
async def remove_filter(
|
|
72
|
+
name: str,
|
|
73
|
+
client: KrytenClient = Depends(get_client),
|
|
74
|
+
config: Config = Depends(get_config),
|
|
75
|
+
) -> dict:
|
|
76
|
+
msg_id = await client.remove_filter(config.channel, name, domain=config.domain)
|
|
77
|
+
return {"message_id": msg_id}
|