This commit is contained in:
PapaTutuWawa 2025-04-21 13:03:16 +02:00
parent 96bee247d6
commit 7e814f4332
7 changed files with 285 additions and 257 deletions

View File

@ -0,0 +1,8 @@
from fastapi import APIRouter
from xmpp_api.api.routers.v1 import bot, user
router = APIRouter(prefix="/api/v1")
router.include_router(bot.router)
router.include_router(user.router)

View File

@ -0,0 +1,244 @@
import uuid
from fastapi import APIRouter, HTTPException, Request, Response
import sqlalchemy
from sqlmodel import select
from slixmpp.jid import JID
from xmpp_api.api.types.bot import (
AddJidRequest,
AddJidResponse,
AllowedJidInformation,
BotCreationResponse,
CreateBotRequest,
GetBotInformation,
SendMessageRequest,
BotConstraint,
BotDomainConstraint,
)
from xmpp_api.db import BotDep, SessionDep, UserDep
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
router = APIRouter(prefix="/bot")
@router.post("/")
def post_create_bot(
bot_request: CreateBotRequest, user: UserDep, session: SessionDep
) -> BotCreationResponse:
# Convert the provided constraints into DB-type bot constraints
try:
constraints: list[db_bot.BotConstraint] = [
bot_constraint_to_db(c) for c in bot_request.constraints
]
except Exception:
raise HTTPException(status_code=500)
bot = Bot(
name=bot_request.name,
description=bot_request.description,
localpart=bot_request.localpart,
token=generate_token(64),
constraints=constraints,
owner_id=user.id,
)
session.add(bot)
# Handle errors caused by the unique constraint
try:
session.commit()
except sqlalchemy.exc.IntegrityError:
raise HTTPException(
status_code=400,
detail="The name is already in use",
)
return BotCreationResponse(
id=bot.id,
name=bot.name,
description=bot.description,
localpart=bot.localpart,
token=bot.token,
constraints=bot_request.constraints,
)
@router.post("/{bot_id}/jid")
async def post_create_bot_jid(
bot_id: str,
creation_request: AddJidRequest,
user: UserDep,
session: SessionDep,
component: XmppApiComponentDep,
) -> AddJidResponse:
# Check if the bot exists and we own it
bot = get_bot_by_id(bot_id, user.id, session)
if bot is None:
raise HTTPException(
status_code=404,
detail="Unknown bot",
)
# Validate the domain constraint
parsed_jid = JID(creation_request.jid)
for constraint in bot.constraints:
if isinstance(constraint, BotDomainConstraint):
if parsed_jid.domain not in constraint.domains:
raise HTTPException(
status_code=400,
detail=f'Domain "{parsed_jid.domain}" is not allowed',
)
# Query the JID for its type
allowed_jid_type: JIDType
if creation_request.type is None:
jid_type = await component.get_entity_type(parsed_jid)
if jid_type is None:
raise HTTPException(
status_code=500,
detail=f"Failed to query entity at {creation_request.jid}",
)
match jid_type:
case "account":
allowed_jid_type = JIDType.DIRECT
case "groupchat":
allowed_jid_type = JIDType.GC
else:
allowed_jid_type = creation_request.type
# Deal with groupchat shenanigans
if allowed_jid_type == JIDType.GC:
# TODO: Join the groupchat
raise HTTPException(
status_code=500,
detail="Groupchats are not yet handled",
)
# Add the JID
jid = AllowedJid(
jid=creation_request.jid,
type=allowed_jid_type,
# This token is only for identification
token=uuid.uuid4().hex,
bot_id=bot.id,
)
session.add(jid)
try:
session.commit()
except sqlalchemy.exc.IntegrityError:
raise HTTPException(
status_code=400,
detail="The JID already exists",
)
return AddJidResponse(
token=jid.token,
)
@router.delete("/{bot_id}/jid/{jid_token}")
def delete_bot_jid_token(
bot_id: str, jid_token: str, user: UserDep, session: SessionDep
):
jid = get_jid_by_jid_token(jid_token, session)
if jid is None or jid.bot_id != bot_id:
raise HTTPException(
status_code=404,
)
session.delete(jid)
session.commit()
@router.post("/message/{jid_token}")
def post_bot_message(
jid_token: str,
message: SendMessageRequest,
request: Request,
bot: BotDep,
session: SessionDep,
component: XmppApiComponentDep,
):
# Is the bot allowed to access this JID?
jid = session.exec(
select(AllowedJid).where(
AllowedJid.token == jid_token, AllowedJid.bot_id == bot.id
)
).first()
if jid is None:
raise HTTPException(
detail="Unknown JID token",
status_code=404,
)
# Validate the domain constraint
parsed_jid = JID(jid.jid)
for constraint in bot.constraints:
if isinstance(constraint, BotDomainConstraint):
if parsed_jid.domain not in constraint.domains:
raise HTTPException(status_code=400)
match jid.type:
case JIDType.DIRECT:
component.send_direct_message(
body=message.body,
localpart=bot.localpart,
nick=bot.name,
recipient=jid.jid,
)
case _:
raise HTTPException(status_code=500)
return Response(status_code=200)
@router.get("/{bot_id}")
def get_bot_information(
bot_id: str, user: UserDep, session: SessionDep
) -> GetBotInformation:
# Find the bot
bot = get_bot_by_id(bot_id, user.id, session)
if bot is None:
raise HTTPException(status_code=404)
try:
constraints: list[BotConstraint] = [
bot_constraint_from_db(c) for c in bot.constraints
]
except Exception:
raise HTTPException(status_code=500)
return GetBotInformation(
id=bot.id,
name=bot.name,
description=bot.description,
localpart=bot.localpart,
jids=[
AllowedJidInformation(
jid=jid.jid,
token=jid.token,
)
for jid in get_jids_by_bot_id(bot.id, session)
],
constraints=constraints,
)
@router.delete("/{bot_id}")
def delete_bot(bot_id: str, user: UserDep, session: SessionDep):
bot = get_bot_by_id(bot_id, user.id, session)
if bot is None:
raise HTTPException(status_code=404)
for jid in get_jids_by_bot_id(bot_id, session):
session.delete(jid)
session.delete(bot)
session.commit()
return Response(status_code=200)

View File

@ -0,0 +1,25 @@
from fastapi import APIRouter
from sqlmodel import select
from xmpp_api.db import UserDep, SessionDep
from xmpp_api.db.bot import Bot
from xmpp_api.api.types.bot import BotInformation
from xmpp_api.util.constraints import bot_constraint_from_db
router = APIRouter(prefix="/user")
@router.get("/bots")
def get_bots(user: UserDep, session: SessionDep) -> list[BotInformation]:
bots = session.exec(select(Bot).where(Bot.owner_id == user.id)).all()
return [
BotInformation(
id=bot.id,
name=bot.name,
description=bot.description,
localpart=bot.localpart,
constraints=[bot_constraint_from_db(c) for c in bot.constraints],
)
for bot in bots
]

View File

@ -40,7 +40,7 @@ def get_by_token(
) )
auth_type, token = authorization.split(" ") auth_type, token = authorization.split(" ")
if auth_type != "Bearer": if auth_type != "Token":
raise HTTPException( raise HTTPException(
detail="Invalid token type provided", detail="Invalid token type provided",
status_code=400, status_code=400,

View File

@ -1,32 +1,14 @@
import uuid from fastapi import FastAPI
from fastapi import FastAPI, HTTPException, Request, Response from sqlmodel import SQLModel
import sqlalchemy
from sqlmodel import SQLModel, select
from slixmpp.jid import JID
from xmpp_api.config.config import load_config from xmpp_api.config.config import load_config
from xmpp_api.api.bot import ( from xmpp_api.db import get_engine
AddJidRequest, from xmpp_api.xmpp.component import XmppApiComponent
AddJidResponse, import xmpp_api.api.routers.v1 as api_v1
AllowedJidInformation,
BotCreationResponse,
BotInformation,
CreateBotRequest,
GetBotInformation,
SendMessageRequest,
BotConstraint,
BotDomainConstraint,
)
from xmpp_api.db import BotDep, SessionDep, UserDep, get_engine
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 XmppApiComponent, XmppApiComponentDep
app = FastAPI() app = FastAPI()
app.include_router(api_v1.router)
@app.on_event("startup") @app.on_event("startup")
@ -39,234 +21,3 @@ def startup():
# App startup is done. Connect to the XMPP server # App startup is done. Connect to the XMPP server
instance = XmppApiComponent.of(config) instance = XmppApiComponent.of(config)
instance.run() instance.run()
@app.post("/api/v1/bot")
def post_create_bot(
bot_request: CreateBotRequest, user: UserDep, session: SessionDep
) -> BotCreationResponse:
# Convert the provided constraints into DB-type bot constraints
try:
constraints: list[db_bot.BotConstraint] = [
bot_constraint_to_db(c) for c in bot_request.constraints
]
except Exception:
raise HTTPException(status_code=500)
bot = Bot(
name=bot_request.name,
description=bot_request.description,
localpart=bot_request.localpart,
token=generate_token(64),
constraints=constraints,
owner_id=user.id,
)
session.add(bot)
# Handle errors caused by the unique constraint
try:
session.commit()
except sqlalchemy.exc.IntegrityError:
raise HTTPException(
status_code=400,
detail="The name is already in use",
)
return BotCreationResponse(
id=bot.id,
name=bot.name,
description=bot.description,
localpart=bot.localpart,
token=bot.token,
constraints=bot_request.constraints,
)
@app.post("/api/v1/bot/{bot_id}/jid")
async def post_create_bot_jid(
bot_id: str,
creation_request: AddJidRequest,
user: UserDep,
session: SessionDep,
component: XmppApiComponentDep,
) -> AddJidResponse:
# Check if the bot exists and we own it
bot = get_bot_by_id(bot_id, user.id, session)
if bot is None:
raise HTTPException(
status_code=404,
detail="Unknown bot",
)
# Validate the domain constraint
parsed_jid = JID(creation_request.jid)
for constraint in bot.constraints:
if isinstance(constraint, BotDomainConstraint):
if parsed_jid.domain not in constraint.domains:
raise HTTPException(
status_code=400,
detail=f'Domain "{parsed_jid.domain}" is not allowed',
)
# Query the JID for its type
allowed_jid_type: JIDType
if creation_request.type is None:
jid_type = await component.get_entity_type(parsed_jid)
if jid_type is None:
raise HTTPException(
status_code=500,
detail=f"Failed to query entity at {creation_request.jid}",
)
match jid_type:
case "account":
allowed_jid_type = JIDType.DIRECT
case "groupchat":
allowed_jid_type = JIDType.GC
else:
allowed_jid_type = creation_request.type
# Deal with groupchat shenanigans
if allowed_jid_type == JIDType.GC:
# TODO: Join the groupchat
raise HTTPException(
status_code=500,
detail="Groupchats are not yet handled",
)
# Add the JID
jid = AllowedJid(
jid=creation_request.jid,
type=allowed_jid_type,
# This token is only for identification
token=uuid.uuid4().hex,
bot_id=bot.id,
)
session.add(jid)
try:
session.commit()
except sqlalchemy.exc.IntegrityError:
raise HTTPException(
status_code=400,
detail="The JID already exists",
)
return AddJidResponse(
token=jid.token,
)
@app.delete("/api/v1/bot/{bot_id}/jid/{jid_token}")
def delete_bot_jid_token(
bot_id: str, jid_token: str, user: UserDep, session: SessionDep
):
jid = get_jid_by_jid_token(jid_token, session)
if jid is None or jid.bot_id != bot_id:
raise HTTPException(
status_code=404,
)
session.delete(jid)
session.commit()
@app.post("/api/v1/bot/message/{jid_token}")
def post_bot_message(
jid_token: str,
message: SendMessageRequest,
request: Request,
bot: BotDep,
session: SessionDep,
component: XmppApiComponentDep,
):
# Is the bot allowed to access this JID?
jid = session.exec(
select(AllowedJid).where(
AllowedJid.token == jid_token, AllowedJid.bot_id == bot.id
)
).first()
if jid is None:
raise HTTPException(
detail="Unknown JID token",
status_code=404,
)
# Validate the domain constraint
parsed_jid = JID(jid.jid)
for constraint in bot.constraints:
if isinstance(constraint, BotDomainConstraint):
if parsed_jid.domain not in constraint.domains:
raise HTTPException(status_code=400)
match jid.type:
case JIDType.DIRECT:
component.send_direct_message(
body=message.body,
localpart=bot.localpart,
nick=bot.name,
recipient=jid.jid,
)
case _:
raise HTTPException(status_code=500)
return Response(status_code=200)
@app.get("/api/v1/bot/{bot_id}")
def get_bot_information(
bot_id: str, user: UserDep, session: SessionDep
) -> GetBotInformation:
# Find the bot
bot = get_bot_by_id(bot_id, user.id, session)
if bot is None:
raise HTTPException(status_code=404)
try:
constraints: list[BotConstraint] = [
bot_constraint_from_db(c) for c in bot.constraints
]
except Exception:
raise HTTPException(status_code=500)
return GetBotInformation(
id=bot.id,
name=bot.name,
description=bot.description,
localpart=bot.localpart,
jids=[
AllowedJidInformation(
jid=jid.jid,
token=jid.token,
)
for jid in get_jids_by_bot_id(bot.id, session)
],
constraints=constraints,
)
@app.get("/api/v1/user/bots")
def get_bots(user: UserDep, session: SessionDep) -> list[BotInformation]:
bots = session.exec(select(Bot).where(Bot.owner_id == user.id)).all()
return [
BotInformation(
id=bot.id,
name=bot.name,
description=bot.description,
localpart=bot.localpart,
constraints=[bot_constraint_from_db(c) for c in bot.constraints],
)
for bot in bots
]
@app.delete("/api/v1/bot/{bot_id}")
def delete_bot(bot_id: str, user: UserDep, session: SessionDep):
bot = get_bot_by_id(bot_id, user.id, session)
if bot is None:
raise HTTPException(status_code=404)
for jid in get_jids_by_bot_id(bot_id, session):
session.delete(jid)
session.delete(bot)
session.commit()
return Response(status_code=200)

View File

@ -1,4 +1,4 @@
import xmpp_api.api.bot as bot_api import xmpp_api.api.types.bot as bot_api
import xmpp_api.db.bot as bot_db import xmpp_api.db.bot as bot_db