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 import uuid
from fastapi import APIRouter, HTTPException, Request, Response from fastapi import APIRouter, HTTPException, Response
import sqlalchemy import sqlalchemy
from sqlmodel import select from sqlmodel import select, Session
from slixmpp.jid import JID from slixmpp.jid import JID
from xmpp_api.api.types.v1.bot import ( from xmpp_api.api.types.v1.bot import (
@@ -15,15 +15,15 @@ from xmpp_api.api.types.v1.bot import (
SendMessageRequest, SendMessageRequest,
BotConstraint, BotConstraint,
BotDomainConstraint, 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 from xmpp_api.db.bot import AllowedJid, Bot, JIDType
import xmpp_api.db.bot as db_bot import xmpp_api.db.bot as db_bot
from xmpp_api.util.token import generate_token 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.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.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") router = APIRouter(prefix="/bot")
@@ -157,11 +157,41 @@ def delete_bot_jid_token(
def post_bot_message( def post_bot_message(
jid_token: str, jid_token: str,
message: SendMessageRequest, message: SendMessageRequest,
request: Request,
bot: BotDep, bot: BotDep,
session: SessionDep, session: SessionDep,
component: XmppApiComponentDep, 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? # Is the bot allowed to access this JID?
jid = session.exec( jid = session.exec(
select(AllowedJid).where( select(AllowedJid).where(
@@ -184,7 +214,7 @@ def post_bot_message(
match jid.type: match jid.type:
case JIDType.DIRECT: case JIDType.DIRECT:
component.send_direct_message( component.send_direct_message(
body=message.body, body=body,
localpart=bot.localpart, localpart=bot.localpart,
nick=bot.name, nick=bot.name,
recipient=jid.jid, recipient=jid.jid,

View File

@@ -109,6 +109,18 @@ class SendMessageRequest(BaseModel):
body: str 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): class GetBotInformation(BotInformation):
""" """
Response for an information request about a given bot. 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 typing import Annotated, Generator, Sequence, cast
from fastapi import Depends, Request, HTTPException from fastapi import Depends, Request, HTTPException
@@ -27,10 +28,35 @@ def get_session(engine: EngineDep) -> Generator[Session]:
SessionDep = Annotated[Session, Depends(get_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( 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: ) -> SQLModel:
token: str | None = None
match token_type:
case TokenType.X_TOKEN:
token = request.headers.get("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: if token is None:
raise HTTPException( raise HTTPException(
detail="No token provided", detail="No token provided",
@@ -47,19 +73,28 @@ def get_by_token(
def get_user(request: Request, session: SessionDep) -> User: 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)] UserDep = Annotated[User, Depends(get_user)]
def get_bot(request: Request, session: SessionDep) -> Bot: 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)] 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: def get_bot_by_id(bot_id: str, user_id: str, session: SessionDep) -> Bot | None:
""" """
Fetches the specified bot from the database. Fetches the specified bot from the database.

View File

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