matrix-ops-bot/ops_bot/main.py

176 lines
4.8 KiB
Python
Raw Normal View History

import asyncio
import logging
2022-12-01 16:31:04 +00:00
from typing import Any, Dict, List, Optional, Protocol, Tuple, cast
2022-12-01 14:20:37 +00:00
import uvicorn
2022-12-01 14:20:37 +00:00
from dotenv import load_dotenv
2022-12-01 16:31:04 +00:00
from fastapi import Depends, FastAPI, HTTPException, Request, status
from fastapi.security import (
HTTPAuthorizationCredentials,
HTTPBasic,
HTTPBasicCredentials,
HTTPBearer,
)
2022-11-30 15:21:09 +00:00
from ops_bot import aws, pagerduty
2022-12-01 16:31:04 +00:00
from ops_bot.config import BotSettings, RoutingKey, load_config
2022-12-01 13:47:27 +00:00
from ops_bot.gitlab import hook as gitlab_hook
2022-12-01 16:31:04 +00:00
from ops_bot.matrix import MatrixClient
2022-12-01 13:47:27 +00:00
load_dotenv()
2022-12-01 14:20:37 +00:00
app = FastAPI()
2022-12-01 16:31:04 +00:00
bearer_security = HTTPBearer(auto_error=False)
basic_security = HTTPBasic(auto_error=False)
async def get_matrix_service(request: Request) -> MatrixClient:
"""A helper to fetch the matrix client from the app state"""
return cast(MatrixClient, request.app.state.matrix_client)
async def matrix_main(matrix_client: MatrixClient) -> None:
"""Execs the matrix client asyncio task"""
workers = [asyncio.create_task(matrix_client.start())]
await asyncio.gather(*workers)
@app.on_event("startup")
async def startup_event() -> None:
2022-12-01 16:31:04 +00:00
bot_settings = load_config()
c = MatrixClient(settings=bot_settings.matrix, join_rooms=bot_settings.get_rooms())
app.state.matrix_client = c
app.state.bot_settings = bot_settings
asyncio.create_task(matrix_main(c))
@app.on_event("shutdown")
async def shutdown_event() -> None:
await app.state.matrix_client.shutdown()
@app.get("/")
async def root() -> Dict[str, str]:
return {"message": "Hello World"}
2022-12-01 16:31:04 +00:00
async def bearer_token_authorizer(
route: RoutingKey,
request: Request,
basic_credentials: Optional[HTTPBasicCredentials],
bearer_credentials: Optional[HTTPAuthorizationCredentials],
) -> bool:
bearer_token: Optional[str] = route.secret_token
return (
bearer_credentials is not None
and bearer_credentials.credentials == bearer_token
)
async def nop_authorizer(
route: RoutingKey,
request: Request,
basic_credentials: Optional[HTTPBasicCredentials],
bearer_credentials: Optional[HTTPAuthorizationCredentials],
) -> bool:
return True
2022-12-01 16:31:04 +00:00
def get_route(bot_settings: BotSettings, path_key: str) -> Optional[RoutingKey]:
# find path_key in bot_settings.routing_keys
for route in bot_settings.routing_keys:
if route.path_key == path_key:
return route
return None
2022-12-01 16:31:04 +00:00
class Authorizer(Protocol):
async def __call__(
self,
route: RoutingKey,
request: Request,
basic_credentials: Optional[HTTPBasicCredentials],
bearer_credentials: Optional[HTTPAuthorizationCredentials],
) -> bool:
...
2022-11-30 15:21:09 +00:00
2022-12-01 16:31:04 +00:00
class ParseHandler(Protocol):
async def __call__(
self,
route: RoutingKey,
payload: Any,
request: Request,
) -> List[Tuple[str, str]]:
...
2022-12-01 16:31:04 +00:00
handlers: Dict[str, Tuple[Authorizer, ParseHandler]] = {
"gitlab": (gitlab_hook.authorize, gitlab_hook.parse_event),
"pagerduty": (bearer_token_authorizer, pagerduty.parse_pagerduty_event),
"aws-sns": (nop_authorizer, aws.parse_sns_event),
}
2022-11-30 15:21:09 +00:00
2022-12-01 14:20:37 +00:00
2022-12-01 16:31:04 +00:00
@app.post("/hook/{routing_key}")
async def webhook_handler(
2022-12-01 13:47:27 +00:00
request: Request,
2022-12-01 16:31:04 +00:00
routing_key: str,
basic_credentials: Optional[HTTPBasicCredentials] = Depends(basic_security),
bearer_credentials: Optional[HTTPAuthorizationCredentials] = Depends(
bearer_security
),
2022-12-01 14:20:37 +00:00
matrix_client: MatrixClient = Depends(get_matrix_service),
2022-12-01 13:47:27 +00:00
) -> Dict[str, str]:
2022-12-01 16:31:04 +00:00
route = get_route(request.app.state.bot_settings, routing_key)
if not route:
logging.error(f"unknown routing key {routing_key}")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Unknown routing key"
)
handler: Optional[Tuple[Authorizer, ParseHandler]] = handlers.get(route.hook_type)
if not handler:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Unknown hook type"
)
authorizer, parse_handler = handler
if not await authorizer(
route,
request=request,
bearer_credentials=bearer_credentials,
basic_credentials=basic_credentials,
):
2022-12-01 13:47:27 +00:00
raise HTTPException(
2022-12-01 16:31:04 +00:00
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials"
2022-12-01 13:47:27 +00:00
)
2022-12-01 16:31:04 +00:00
payload: Any = await request.json()
messages = await parse_handler(route, payload, request=request)
2022-12-01 13:47:27 +00:00
for msg_plain, msg_formatted in messages:
await matrix_client.room_send(
2022-12-01 16:31:04 +00:00
route.room_id,
2022-12-01 13:47:27 +00:00
msg_plain,
message_formatted=msg_formatted,
)
2022-12-01 16:31:04 +00:00
2022-12-01 13:47:27 +00:00
return {"status": "ok"}
2022-11-30 15:21:09 +00:00
def start_dev() -> None:
uvicorn.run("ops_bot.main:app", port=1111, host="127.0.0.1", reload=True)
def start() -> None:
uvicorn.run("ops_bot.main:app", port=1111, host="0.0.0.0") # nosec B104
2022-07-22 12:53:34 +00:00
if __name__ == "__main__":
start()