Add an endpoint for posting to Grafana

This commit is contained in:
2026-01-11 20:01:58 +01:00
parent fcbb0abd8f
commit 11e4587399
4 changed files with 89 additions and 11 deletions

View File

@@ -1,8 +1,8 @@
import uuid
from fastapi import APIRouter, HTTPException, Request, Response
from fastapi import APIRouter, HTTPException, Response
import sqlalchemy
from sqlmodel import select
from sqlmodel import select, Session
from slixmpp.jid import JID
from xmpp_api.api.types.v1.bot import (
@@ -15,15 +15,15 @@ from xmpp_api.api.types.v1.bot import (
SendMessageRequest,
BotConstraint,
BotDomainConstraint,
SendMessageGrafanaWebhook,
)
from xmpp_api.db import BotDep, SessionDep, UserDep
from xmpp_api.db import BotDep, SessionDep, UserDep, AuthorizationBotDep
from xmpp_api.db.bot import AllowedJid, Bot, JIDType
import xmpp_api.db.bot as db_bot
from xmpp_api.util.token import generate_token
from xmpp_api.db import get_bot_by_id, get_jids_by_bot_id, get_jid_by_jid_token
from xmpp_api.util.constraints import bot_constraint_to_db, bot_constraint_from_db
from xmpp_api.xmpp.component import XmppApiComponentDep
from xmpp_api.xmpp.component import XmppApiComponentDep, XmppApiComponent
router = APIRouter(prefix="/bot")
@@ -157,11 +157,41 @@ def delete_bot_jid_token(
def post_bot_message(
jid_token: str,
message: SendMessageRequest,
request: Request,
bot: BotDep,
session: SessionDep,
component: XmppApiComponentDep,
):
_post_message(jid_token, message.body, bot, session, component)
@router.post("/message/{jid_token}/grafana")
def post_bot_message_grafana_webhook(
jid_token: str,
message: SendMessageGrafanaWebhook,
bot: AuthorizationBotDep,
session: SessionDep,
component: XmppApiComponentDep,
):
_post_message(jid_token, message.body, bot, session, component)
def _post_message(
jid_token: str,
body: str,
bot: Bot,
session: Session,
component: XmppApiComponent,
):
"""
Performs the message sending.
Args
:jid_token The recipient's JID token.
:body The body of the message.
:bot The bot to send the message as.
:session The database session.
:component The XMPP component instance.
"""
# Is the bot allowed to access this JID?
jid = session.exec(
select(AllowedJid).where(
@@ -184,7 +214,7 @@ def post_bot_message(
match jid.type:
case JIDType.DIRECT:
component.send_direct_message(
body=message.body,
body=body,
localpart=bot.localpart,
nick=bot.name,
recipient=jid.jid,

View File

@@ -109,6 +109,18 @@ class SendMessageRequest(BaseModel):
body: str
class SendMessageGrafanaWebhook(BaseModel):
"""
Model for the request to send a grafana webhook to a JID.
"""
# The alert title
title: str
# The alert message
body: str
class GetBotInformation(BotInformation):
"""
Response for an information request about a given bot.

View File

@@ -1,3 +1,4 @@
from enum import auto, Enum
from typing import Annotated, Generator, Sequence, cast
from fastapi import Depends, Request, HTTPException
@@ -27,10 +28,35 @@ def get_session(engine: EngineDep) -> Generator[Session]:
SessionDep = Annotated[Session, Depends(get_session)]
class TokenType(Enum):
"""
Types of authentication tokens.
"""
# Token is put into the X-Token header
X_TOKEN = auto()
# Token is put into the "Authorization" header with type "Bearer".
BEARER = auto()
def get_by_token(
cls: type[SQLModel], request: Request, session: SessionDep
cls: type[SQLModel],
request: Request,
session: SessionDep,
token_type: TokenType = TokenType.X_TOKEN,
) -> SQLModel:
token = request.headers.get("X-Token")
token: str | None = None
match token_type:
case TokenType.X_TOKEN:
token = request.headers.get("X-Token")
case TokenType.BEARER:
token = request.headers.get("Authorization")
if token is not None:
parts = token.split(" ")
if len(parts) == 2 and parts[0] == "Bearer":
token = parts[1]
if token is None:
raise HTTPException(
detail="No token provided",
@@ -47,19 +73,28 @@ def get_by_token(
def get_user(request: Request, session: SessionDep) -> User:
return cast(User, get_by_token(User, request, session))
return cast(
User, get_by_token(User, request, session, token_type=TokenType.X_TOKEN)
)
UserDep = Annotated[User, Depends(get_user)]
def get_bot(request: Request, session: SessionDep) -> Bot:
return cast(Bot, get_by_token(Bot, request, session))
return cast(Bot, get_by_token(Bot, request, session, token_type=TokenType.X_TOKEN))
BotDep = Annotated[Bot, Depends(get_bot)]
def get_authorization_bot(request: Request, session: SessionDep) -> Bot:
return cast(Bot, get_by_token(Bot, request, session, token_type=TokenType.BEARER))
AuthorizationBotDep = Annotated[Bot, Depends(get_authorization_bot)]
def get_bot_by_id(bot_id: str, user_id: str, session: SessionDep) -> Bot | None:
"""
Fetches the specified bot from the database.

View File

@@ -33,6 +33,7 @@ def startup():
if config.sentry is not None:
logging.getLogger("root").info("Setting up Sentry")
import sentry_sdk
sentry_sdk.init(
config.sentry.dsn,
send_default_pii=True,